├── .github └── workflows │ ├── coverage.yml │ └── pythonapp.yml ├── .gitignore ├── BaseCollector.py ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── collectors ├── .DS_Store ├── AlertCollector.py ├── ClusterAlertCollector.py ├── ClusterPropertiesCollector.py ├── ClusterStatsCollector.py ├── CustomInfoMetricsGenerator.py ├── DatastoreAlertCollector.py ├── DatastorePropertiesCollector.py ├── DatastoreStatsCollector.py ├── DistributedvSwitchPropertiesCollector.py ├── HostSystemAlertCollector.py ├── HostSystemPropertiesCollector.py ├── HostSystemStatsCollector.py ├── InventoryCollector.py ├── NSXTAdapterAlertCollector.py ├── NSXTLogicalSwitchAlertCollector.py ├── NSXTLogicalSwitchPropertiesCollector.py ├── NSXTMgmtClusterAlertCollector.py ├── NSXTMgmtClusterPropertiesCollector.py ├── NSXTMgmtClusterStatsCollector.py ├── NSXTMgmtNodeAlertCollector.py ├── NSXTMgmtNodePropertiesCollector.py ├── NSXTMgmtNodeStatsCollector.py ├── NSXTMgmtServiceAlertCollector.py ├── NSXTTransportNodeAlertCollector.py ├── NSXTTransportNodePropertiesCollector.py ├── PropertiesCollector.py ├── SDDCAlertCollector.py ├── SDRSPropertiesCollector.py ├── SDRSStatsCollector.py ├── StatsCollector.py ├── VCenterAlertCollector.py ├── VCenterPropertiesCollector.py ├── VCenterStatsCollector.py ├── VMAlertCollector.py ├── VMPropertiesCollector.py ├── VMStatsCPUCollector.py ├── VMStatsCollector.py ├── VMStatsDefaultCollector.py ├── VMStatsMemoryCollector.py ├── VMStatsNetworkCollector.py ├── VMStatsVirtualDiskCollector.py ├── VcopsSelfMonitoringAlertCollector.py ├── VcopsSelfMonitoringPropertiesCollector.py ├── VcopsSelfMonitoringStatsCollector.py └── __init__.py ├── exporter.py ├── images ├── .DS_Store ├── architecture.jpg ├── architecture.png └── collectors.png ├── inventory.py ├── inventory ├── Api.py ├── Builder.py └── __init__.py ├── renovate.json ├── requirements.txt ├── tests ├── TestCollectorInit.py ├── TestCollectors.py ├── TestLaunchExporter.py ├── TestLaunchInventory.py ├── __init__.py ├── collector_config.yaml ├── inventory_config.yaml ├── inventory_launch.py └── metrics.yaml └── tools ├── Vrops.py ├── YamlRead.py ├── __init__.py └── helper.py /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: code test coverage 3 | permissions: 4 | contents: read 5 | pull-requests: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | pull_request: 12 | branches: 13 | - master 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python 3.13.2 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: 3.13.2 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 27 | - name: Install coveralls 28 | run: | 29 | pip install coveralls 30 | - name: vrops-test 31 | env: 32 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | for i in $(ls tests/Test*) 36 | do 37 | LOOPBACK=1 INVENTORY="127.0.0.1:8000" DEBUG=0 USER=FOO PASSWORD=Bar COLLECTOR_CONFIG=tests/collector_config.yaml INVENTORY_CONFIG=tests/inventory_config.yaml TARGET=vrops-vcenter-test.company.com coverage run -a --omit "*.md","*.txt",LICENSE,Makefile,Dockerfile $i 38 | done 39 | - name: Coveralls Parallel 40 | uses: coverallsapp/github-action@v2 41 | with: 42 | github-token: ${{ secrets.github_token }} 43 | flag-name: run-vrops-test 44 | parallel: true 45 | finish: 46 | needs: test 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Coveralls finished 50 | uses: coverallsapp/github-action@master 51 | with: 52 | github-token: ${{ secrets.github_token }} 53 | parallel-finished: true 54 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | permissions: 6 | contents: read 7 | pull-requests: write 8 | 9 | on: 10 | push: 11 | branches: 12 | - master 13 | pull_request: 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Python 3.13.2 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: 3.13.2 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 pytest 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | for i in $(ls tests/Test*) 40 | do 41 | LOOPBACK=1 INVENTORY="127.0.0.1:8000" DEBUG=0 USER=FOO PASSWORD=Bar COLLECTOR_CONFIG=tests/collector_config.yaml INVENTORY_CONFIG=tests/inventory_config.yaml TARGET=vrops-vcenter-test.company.com python3 $i 42 | done 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | db.sqlite3 5 | migrations/ 6 | media/ 7 | settings.py 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # configs 53 | netbox.json 54 | 55 | # vim 56 | *.swo 57 | *.swp 58 | -------------------------------------------------------------------------------- /BaseCollector.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import requests 3 | import time 4 | import os 5 | import re 6 | import logging 7 | from tools.helper import yaml_read 8 | from tools.Vrops import Vrops 9 | from prometheus_client.core import GaugeMetricFamily, InfoMetricFamily 10 | 11 | logger = logging.getLogger('vrops-exporter') 12 | 13 | 14 | class BaseCollector(ABC): 15 | 16 | def __init__(self): 17 | self.vrops_entity_name = 'base' 18 | while os.environ['TARGET'] not in self.get_vrops_target(): 19 | logger.critical(f'Cannot start exporter. Missing inventory pod for {os.environ["TARGET"]}, retry in 60s') 20 | time.sleep(60) 21 | self.target = os.environ.get('TARGET') 22 | self.vrops = Vrops() 23 | self.name = self.__class__.__name__ 24 | self.label_names = [] 25 | self.project_ids = [] 26 | self.collect_running = False 27 | self.nested_value_metric_keys = [] 28 | 29 | @abstractmethod 30 | def collect(self): 31 | pass 32 | 33 | def read_collector_config(self): 34 | config_file = yaml_read(os.environ['COLLECTOR_CONFIG']) 35 | return config_file 36 | 37 | def get_vcenters(self, target): 38 | self.wait_for_inventory_data() 39 | current_iteration = self.get_iteration() 40 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/vcenters/{}".format(current_iteration) 41 | request = requests.get(url) 42 | self.vcenters = request.json() if request else {} 43 | return self.vcenters 44 | 45 | def get_datacenters(self, target): 46 | self.wait_for_inventory_data() 47 | current_iteration = self.get_iteration() 48 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/datacenters/{}".format(current_iteration) 49 | request = requests.get(url) 50 | self.datacenters = request.json() if request else {} 51 | return self.datacenters 52 | 53 | def get_clusters(self, target): 54 | self.wait_for_inventory_data() 55 | current_iteration = self.get_iteration() 56 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/clusters/{}".format(current_iteration) 57 | request = requests.get(url) 58 | self.clusters = request.json() if request else {} 59 | return self.clusters 60 | 61 | def get_hosts(self, target): 62 | self.wait_for_inventory_data() 63 | current_iteration = self.get_iteration() 64 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/hosts/{}".format(current_iteration) 65 | request = requests.get(url) 66 | self.hosts = request.json() if request else {} 67 | return self.hosts 68 | 69 | def get_SDRS_cluster(self, target): 70 | self.wait_for_inventory_data() 71 | current_iteration = self.get_iteration() 72 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/storagepod/{}".format(current_iteration) 73 | request = requests.get(url) 74 | self.sdrs_clusters = request.json() if request else {} 75 | return self.sdrs_clusters 76 | 77 | def get_datastores(self, target): 78 | self.wait_for_inventory_data() 79 | current_iteration = self.get_iteration() 80 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/datastores/{}".format(current_iteration) 81 | request = requests.get(url) 82 | self.datastores = request.json() if request else {} 83 | return self.datastores 84 | 85 | def get_vms(self, target): 86 | self.wait_for_inventory_data() 87 | current_iteration = self.get_iteration() 88 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/vms/{}".format(current_iteration) 89 | request = requests.get(url) 90 | self.vms = request.json() if request else {} 91 | return self.vms 92 | 93 | def get_distributed_vswitches(self, target): 94 | self.wait_for_inventory_data() 95 | current_iteration = self.get_iteration() 96 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/dvs/{}".format(current_iteration) 97 | request = requests.get(url) 98 | self.dvs = request.json() if request else {} 99 | return self.dvs 100 | 101 | def get_nsxt_adapter(self, target): 102 | self.wait_for_inventory_data() 103 | current_iteration = self.get_iteration() 104 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/nsxt_adapter/{}".format(current_iteration) 105 | request = requests.get(url) 106 | self.nsxt_adapter = request.json() if request else {} 107 | return self.nsxt_adapter 108 | 109 | def get_nsxt_mgmt_cluster(self, target): 110 | self.wait_for_inventory_data() 111 | current_iteration = self.get_iteration() 112 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/nsxt_mgmt_cluster/{}".format(current_iteration) 113 | request = requests.get(url) 114 | self.nsxt_mgmt_cluster = request.json() if request else {} 115 | return self.nsxt_mgmt_cluster 116 | 117 | def get_nsxt_mgmt_nodes(self, target): 118 | self.wait_for_inventory_data() 119 | current_iteration = self.get_iteration() 120 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/nsxt_mgmt_nodes/{}".format(current_iteration) 121 | request = requests.get(url) 122 | self.nsxt_mgmt_nodes = request.json() if request else {} 123 | return self.nsxt_mgmt_nodes 124 | 125 | def get_nsxt_mgmt_service(self, target): 126 | self.wait_for_inventory_data() 127 | current_iteration = self.get_iteration() 128 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/nsxt_mgmt_service/{}".format(current_iteration) 129 | request = requests.get(url) 130 | self.nsxt_mgmt_service = request.json() if request else {} 131 | return self.nsxt_mgmt_service 132 | 133 | def get_nsxt_transport_nodes(self, target): 134 | self.wait_for_inventory_data() 135 | current_iteration = self.get_iteration() 136 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/nsxt_transport_nodes/{}".format(current_iteration) 137 | request = requests.get(url) 138 | self.nsxt_transport_nodes = request.json() if request else {} 139 | return self.nsxt_transport_nodes 140 | 141 | def get_nsxt_logical_switches(self, target): 142 | self.wait_for_inventory_data() 143 | current_iteration = self.get_iteration() 144 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/nsxt_logical_switches/{}".format(current_iteration) 145 | request = requests.get(url) 146 | self.nsxt_logical_switches = request.json() if request else {} 147 | return self.nsxt_logical_switches 148 | 149 | def get_vcops_objects(self, target): 150 | self.wait_for_inventory_data() 151 | current_iteration = self.get_iteration() 152 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/vcops_objects/{}".format( 153 | current_iteration) 154 | request = requests.get(url) 155 | self.vcops_objects = request.json() if request else {} 156 | return self.vcops_objects 157 | 158 | def get_sddc_objects(self, target): 159 | self.wait_for_inventory_data() 160 | current_iteration = self.get_iteration() 161 | url = "http://" + os.environ['INVENTORY'] + "/" + target + "/sddc_objects/{}".format( 162 | current_iteration) 163 | request = requests.get(url) 164 | self.sddc_objects = request.json() if request else {} 165 | return self.sddc_objects 166 | 167 | def get_alertdefinition(self, alert_id): 168 | request = requests.get(url="http://" + os.environ['INVENTORY'] + "/alertdefinitions/{}".format(alert_id)) 169 | self.alertdefinition = request.json() if request else {} 170 | return self.alertdefinition 171 | 172 | def get_iteration(self): 173 | self.iteration = self.do_request(url="http://" + os.environ['INVENTORY'] + "/iteration") 174 | return self.iteration 175 | 176 | def get_amount_resources(self): 177 | self.wait_for_inventory_data() 178 | self.amount_resources = self.do_request(url="http://" + os.environ['INVENTORY'] + "/amount_resources") 179 | return self.amount_resources 180 | 181 | def get_collection_times(self): 182 | self.wait_for_inventory_data() 183 | self.collection_times = self.do_request(url="http://" + os.environ['INVENTORY'] + "/collection_times") 184 | return self.collection_times 185 | 186 | def get_inventory_api_responses(self): 187 | self.wait_for_inventory_data() 188 | self.api_responses = self.do_request(url="http://" + os.environ['INVENTORY'] + "/api_response_codes") 189 | self.api_reponse_times = self.do_request(url="http://" + os.environ['INVENTORY'] + "/api_response_times") 190 | return self.api_responses, self.api_reponse_times 191 | 192 | def get_service_states(self): 193 | self.wait_for_inventory_data() 194 | self.service_states = self.do_request(url="http://" + os.environ['INVENTORY'] + "/service_states") 195 | return self.service_states 196 | 197 | def get_target_tokens(self): 198 | self.target_tokens = self.do_request(url="http://" + os.environ['INVENTORY'] + "/target_tokens") 199 | return self.target_tokens 200 | 201 | def get_vrops_target(self): 202 | vrops_target = self.do_request(url="http://" + os.environ['INVENTORY'] + "/target") 203 | return vrops_target 204 | 205 | def do_request(self, url): 206 | try: 207 | request = requests.get(url, timeout=60) 208 | response = request.json() if request else {} 209 | return response 210 | except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout) as e: 211 | logger.critical(f'Connection error to inventory: {os.environ["INVENTORY"]} - Error: {e}') 212 | return {} 213 | 214 | def get_vcenters_by_target(self): 215 | vcenter_dict = self.get_vcenters(self.target) 216 | self.target_vcenters = [vcenter_dict[uuid]['uuid'] for uuid in vcenter_dict] 217 | return self.target_vcenters 218 | 219 | def get_clusters_by_target(self): 220 | cluster_dict = self.get_clusters(self.target) 221 | self.target_clusters = [cluster_dict[uuid]['uuid'] for uuid in cluster_dict] 222 | return self.target_clusters 223 | 224 | def get_hosts_by_target(self): 225 | host_dict = self.get_hosts(self.target) 226 | self.target_hosts = [host_dict[uuid]['uuid'] for uuid in host_dict] 227 | return self.target_hosts 228 | 229 | def get_SDRS_clusters_by_target(self): 230 | SDRS_clusters_dict = self.get_SDRS_cluster(self.target) 231 | self.target_SDRS_clusters = [SDRS_clusters_dict[uuid]['uuid'] for uuid in SDRS_clusters_dict] 232 | return self.target_SDRS_clusters 233 | 234 | def get_datastores_by_target(self): 235 | datastore_dict = self.get_datastores(self.target) 236 | self.target_datastores = [datastore_dict[uuid]['uuid'] for uuid in datastore_dict] 237 | return self.target_datastores 238 | 239 | def get_vms_by_target(self): 240 | vms_dict = self.get_vms(self.target) 241 | self.target_vms = [vms_dict[uuid]['uuid'] for uuid in vms_dict] 242 | return self.target_vms 243 | 244 | def get_dvs_by_target(self): 245 | dvs_dict = self.get_distributed_vswitches(self.target) 246 | self.target_dvs = [dvs_dict[uuid]['uuid'] for uuid in dvs_dict] 247 | return self.target_dvs 248 | 249 | def get_nsxt_adapter_by_target(self): 250 | nsxt_adapter_dict = self.get_nsxt_adapter(self.target) 251 | self.target_nsxt_adapter = [nsxt_adapter_dict[uuid]['uuid'] for uuid in nsxt_adapter_dict] 252 | return self.target_nsxt_adapter 253 | 254 | def get_nsxt_mgmt_cluster_by_target(self): 255 | nsxt_mgmt_cluster_dict = self.get_nsxt_mgmt_cluster(self.target) 256 | self.target_nsxt_mgmt_cluster = [nsxt_mgmt_cluster_dict[uuid]['uuid'] for uuid in nsxt_mgmt_cluster_dict] 257 | return self.target_nsxt_mgmt_cluster 258 | 259 | def get_nsxt_mgmt_nodes_by_target(self): 260 | nsxt_mgmt_nodes_dict = self.get_nsxt_mgmt_nodes(self.target) 261 | self.target_nsxt_mgmt_nodes = [nsxt_mgmt_nodes_dict[uuid]['uuid'] for uuid in nsxt_mgmt_nodes_dict] 262 | return self.target_nsxt_mgmt_nodes 263 | 264 | def get_nsxt_mgmt_service_by_target(self): 265 | nsxt_mgmt_service_dict = self.get_nsxt_mgmt_service(self.target) 266 | self.target_nsxt_mgmt_service = [nsxt_mgmt_service_dict[uuid]['uuid'] for uuid in nsxt_mgmt_service_dict] 267 | return self.target_nsxt_mgmt_service 268 | 269 | def get_nsxt_transport_nodes_by_target(self): 270 | nsxt_transport_nodes_dict = self.get_nsxt_transport_nodes(self.target) 271 | self.target_nsxt_transport_nodes = [nsxt_transport_nodes_dict[uuid]['uuid'] for uuid in 272 | nsxt_transport_nodes_dict] 273 | return self.target_nsxt_transport_nodes 274 | 275 | def get_nsxt_logical_switches_by_target(self): 276 | nsxt_logical_switches_dict = self.get_nsxt_logical_switches(self.target) 277 | self.target_nsxt_logical_switches = [nsxt_logical_switches_dict[uuid]['uuid'] for uuid in 278 | nsxt_logical_switches_dict] 279 | return self.target_nsxt_logical_switches 280 | 281 | def get_vcops_objects_by_target(self): 282 | vcops_objects_dict = self.get_vcops_objects(self.target) 283 | self.target_vcops_objects = [vcops_objects_dict[uuid]['uuid'] for uuid in 284 | vcops_objects_dict] 285 | return self.target_vcops_objects 286 | 287 | def get_sddc_objects_by_target(self): 288 | sddc_objects_dict = self.get_sddc_objects(self.target) 289 | self.target_sddc_objects = [sddc_objects_dict[uuid]['uuid'] for uuid in 290 | sddc_objects_dict] 291 | return self.target_sddc_objects 292 | 293 | def get_project_ids_by_target(self): 294 | try: 295 | token = self.get_target_tokens() 296 | token = token[self.target] 297 | uuids = self.get_vms_by_target() 298 | project_ids = Vrops.get_project_ids(self.target, token, uuids, self.name) 299 | return project_ids 300 | except requests.exceptions.ConnectionError as e: 301 | logger.critical(f'No connection to inventory: {os.environ["INVENTORY"]} - Error: {e}') 302 | return [] 303 | 304 | def wait_for_inventory_data(self): 305 | iteration = self.get_iteration() 306 | while not iteration: 307 | time.sleep(5) 308 | iteration = self.get_iteration() 309 | logger.debug(f'Waiting for initial iteration: {self.name}') 310 | return 311 | 312 | def create_api_response_code_metric(self, collector: str, api_responding: int) -> GaugeMetricFamily: 313 | gauge = GaugeMetricFamily('vrops_api_response', 'vrops-exporter', labels=['target', 'class']) 314 | gauge.add_metric(labels=[self.target, collector.lower()], value=api_responding) 315 | 316 | if api_responding > 200: 317 | logger.critical(f'API response {api_responding} [{collector}, {self.target}], no return') 318 | 319 | return gauge 320 | 321 | def create_api_response_time_metric(self, collector: str, response_time: float) -> GaugeMetricFamily: 322 | gauge = GaugeMetricFamily('vrops_api_response_time_seconds', 'vrops-exporter', 323 | labels=['target', 'class']) 324 | gauge.add_metric(labels=[self.target, collector.lower()], value=response_time) 325 | return gauge 326 | 327 | def number_of_metric_samples_generated(self, collector: str, metric_name: str, 328 | number_of_metric_samples_generated: int) -> GaugeMetricFamily: 329 | gauge = GaugeMetricFamily('vrops_collector_metric_samples_generated_number', 'vrops-exporter', 330 | labels=['target', 'class', 'metric_name']) 331 | gauge.add_metric(labels=[self.target, collector.lower(), metric_name], value=number_of_metric_samples_generated) 332 | return gauge 333 | 334 | def number_of_metrics_to_collect(self, collector: str, number_of_metrics: int) -> GaugeMetricFamily: 335 | gauge = GaugeMetricFamily('vrops_collector_metrics_number', 'vrops-exporter', 336 | labels=['target', 'class']) 337 | gauge.add_metric(labels=[self.target, collector.lower()], value=number_of_metrics) 338 | return gauge 339 | 340 | def number_of_resources(self, collector: str, number_of_resources: int) -> GaugeMetricFamily: 341 | gauge = GaugeMetricFamily('vrops_collector_resources_number', 'vrops-exporter', 342 | labels=['target', 'class']) 343 | gauge.add_metric(labels=[self.target, collector.lower()], value=number_of_resources) 344 | return gauge 345 | 346 | def generate_metrics(self, label_names: list) -> dict: 347 | collector_config = self.read_collector_config() 348 | metrics = {m['key']: {'metric_suffix': m['metric_suffix'], 349 | 'key': m['key'], 350 | 'expected': m.setdefault('expected', None), 351 | 'gauge': GaugeMetricFamily(f'vrops_{self.vrops_entity_name}_{m["metric_suffix"].lower()}', 352 | 'vrops-exporter', labels=label_names) 353 | } for m in collector_config.get(self.name, {})} 354 | if not metrics: 355 | logger.error(f'Cannot find {self.name} in collector_config') 356 | return metrics 357 | 358 | def generate_metrics_enriched_by_api(self, no_match_in_config: list, label_names: list) -> dict: 359 | gauges = dict() 360 | for statkey in no_match_in_config: 361 | new_metric_suffix = re.sub("[^0-9a-zA-Z]+", "_", statkey[0]) 362 | value = statkey[1] 363 | labels = statkey[2] 364 | if new_metric_suffix not in gauges: 365 | gauges[new_metric_suffix] = GaugeMetricFamily( 366 | f'vrops_{self.vrops_entity_name}_{new_metric_suffix.lower()}', 'vrops-exporter', labels=label_names) 367 | gauges[new_metric_suffix].add_metric(labels=labels, value=value) 368 | return gauges 369 | 370 | def generate_alert_metrics(self, label_names: list) -> InfoMetricFamily: 371 | if 'alert_name' not in label_names: 372 | label_names.extend(['alert_name', 'alert_level', 'status', 'alert_impact']) 373 | alert_metric = InfoMetricFamily(f'vrops_{self.vrops_entity_name}_alert', 'vrops-exporter', 374 | labels=label_names) 375 | return alert_metric 376 | 377 | def add_metric_labels(self, metric_object: GaugeMetricFamily, labels): 378 | if labels[0] not in metric_object._labelnames: 379 | for label in labels: 380 | metric_object._labelnames += (label,) 381 | return 382 | 383 | def describe(self): 384 | collector_config = self.read_collector_config() 385 | for metric in collector_config[self.name]: 386 | metric_suffix = metric['metric_suffix'] 387 | yield GaugeMetricFamily(f'vrops_{self.vrops_entity_name}_{metric_suffix.lower()}', 'vrops-exporter') 388 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM keppel.eu-de-1.cloud.sap/ccloud-dockerhub-mirror/library/alpine:latest 2 | 3 | ARG BININFO_BUILD_DATE BININFO_COMMIT_HASH BININFO_VERSION 4 | LABEL source_repository="https://github.com/sapcc/vrops-exporter" \ 5 | org.opencontainers.image.url="https://github.com/sapcc/vrops-exporter" \ 6 | org.opencontainers.image.created=${BININFO_BUILD_DATE} \ 7 | org.opencontainers.image.revision=${BININFO_COMMIT_HASH} \ 8 | org.opencontainers.image.version=${BININFO_VERSION} 9 | 10 | RUN apk --update add python3 openssl ca-certificates bash python3-dev git py3-pip && \ 11 | apk --update add --virtual build-dependencies libffi-dev openssl-dev libxml2 libxml2-dev libxslt libxslt-dev build-base 12 | RUN apk upgrade -U 13 | RUN git config --global http.sslVerify false 14 | RUN git clone https://github.com/sapcc/vrops-exporter.git 15 | RUN python3 -m venv /opt/vrops-env 16 | RUN . /opt/vrops-env/bin/activate 17 | ENV PATH /opt/vrops-env/bin:$PATH 18 | RUN pip3 install --upgrade pip 19 | RUN pip3 install --ignore-installed six 20 | RUN pip install --upgrade cffi 21 | 22 | ADD . vrops-exporter/ 23 | RUN pip3 install --upgrade -r vrops-exporter/requirements.txt 24 | RUN pip3 install --upgrade setuptools 25 | 26 | WORKDIR vrops-exporter/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/sh 2 | IMAGE := keppel.eu-de-1.cloud.sap/ccloud/vrops-exporter 3 | VERSION := 0.2 4 | 5 | ### Executables 6 | DOCKER := docker 7 | 8 | ### Docker Targets 9 | 10 | .PHONY: build 11 | build: 12 | $(DOCKER) build -t $(IMAGE):$(VERSION) --no-cache --rm . 13 | #$(DOCKER) build -t $(IMAGE):$(VERSION) . 14 | 15 | .PHONY: push 16 | push: 17 | $(DOCKER) push $(IMAGE):$(VERSION) 18 | #$(DOCKER) push $(IMAGE):latest 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Python application](https://github.com/sapcc/vrops-exporter/actions/workflows/pythonapp.yml/badge.svg) 2 | [![Coverage Status](https://coveralls.io/repos/github/sapcc/vrops-exporter/badge.svg?branch=master)](https://coveralls.io/github/sapcc/vrops-exporter?branch=master) 3 | 4 | # vrops-exporter 5 | Prometheus exporter for VMware vRealize Operations Manager 6 | 7 | ###### Tested and compatible with vROps 6.5 up to vROps v8.6.2 8 | 9 | ### Table of Contents 10 | 11 | [Design](#design) 12 | 13 | [Supported adapters and resourcekinds](#supported-adapters-and-resourcekinds) 14 | 15 | [Running the software](#running-the-software) 16 | 17 | [Running in Kubernetes](#running-in-kubernetes) 18 | 19 | [Test](#test) 20 | 21 | ## Design 22 | 23 | The exporter is divided in two main components, [inventory](#inventory) and [exporter](#exporter). 24 | The inventory is providing the resource-uuids (unique unit identifier) from vROps via a REST interface to the exporter 25 | 26 | #### inventory 27 | 28 | The inventory collects all supported resourcekinds in their parent-child relation, and makes them available at an internal API. 29 | The resourcekinds are updated by a continuous cycle that can be configured with `--sleep`. The inventory preserves data through iterations. 30 | The last two iterations for these cycles are always provided via the endpoints and in order to know which iteration to fetch, 31 | latest iteration needs to be queried first. 32 | 33 | To have more control over the resources to be collected, they can be filtered by _resourcestatus_, _resourcehealth_ and _resourcestate_ in [inventory-config](tests/inventory_config.yaml). 34 | 35 | ###### inventory endpoints 36 | ```shell 37 | GET 38 | 39 | /target # vrops FQDN 40 | /// # path for each resourcekind 41 | /alertdefinitions/ # vrops integrated alertdefinitions 42 | /iteration # current inventory iteration 43 | /amount_resources # amount of resources for each resourcekind 44 | /collection_times # measured time for a inventory run per vrops 45 | /api_response_codes # HTTP response codes per resourcekind GET request 46 | /target_tokens # dict with vrops: auth token 47 | ``` 48 | #### exporter 49 | 50 | The second component are the collectors that exist for each resourcekind as well as for metrics, properties and alerts. 51 | Each collector performs only one task - one resourcekind and one type from the three different values. First, the resourcekinds in question are 52 | queried at the inventory's internal API. In the second step, the values, properties or alarms are queried. From these, 53 | the Prometheus metrics are generated. To complete the picture, the metrics are enriched with the labels from the resourcekind relationships 54 | created in the inventory. 55 | 56 | ![](images/architecture.png) 57 | 58 | To avoid multiple implementations of functionality, the collectors follow an inheritance structure. 59 | 60 | ![](images/collectors.png) 61 | 62 | ## Supported adapters and resourcekinds 63 | 64 | This sections shows how vROps internal resourcekind relationsships are used in the exporter as a hierarchical tree. 65 | 66 | #### VMware vCenter Server 67 | 68 | Resourcekind relationship: 69 | ```shell 70 | VMwareAdapter Instance: 71 | Datacenter: 72 | VmwareDistributedVirtualSwitch 73 | Datastore 74 | ClusterComputeResource: 75 | HostSystem: 76 | Virtualmachine 77 | ``` 78 | 79 | #### VMware NSX-T Adapter 80 | 81 | Resourcekind relationship: 82 | ```shell 83 | NSXTAdapter: 84 | ManagementCluster: 85 | ManagementNode: 86 | ManagementService 87 | TransportZone: 88 | TransportNode 89 | LogicalSwitch 90 | ``` 91 | 92 | #### SDDC (Software-Defined Data Center) Health Adapter 93 | 94 | SDDC resourcekinds can be defined in [inventory-config](tests/inventory_config.yaml): 95 | 96 | ```yaml 97 | resourcekinds: 98 | sddc_resourcekinds: 99 | - "NSXT Server" 100 | - "VCENTER" 101 | - "NSXVPostgresService" 102 | - "SSHService" 103 | - "NSXReplicatorService" 104 | - "NSXRabbitmqService" 105 | - "NSXManagerService" 106 | - "NSXControllerService" 107 | - "SDDCHealth Instance" 108 | - "vCenterBackupJob" 109 | ``` 110 | 111 | #### VCOPS (vCenter Operations) Adapter 112 | 113 | VCOPS resourcekinds can be defined in [inventory-config](tests/inventory_config.yaml): 114 | 115 | ```yaml 116 | resourcekinds: 117 | vcops_resourcekinds: 118 | - "vC-Ops-Analytics" 119 | - "vC-Ops-CaSA" 120 | - "vC-Ops-Cluster" 121 | - "vC-Ops-Collector" 122 | - "vC-Ops-Node" 123 | - "vC-Ops-Suite-API" 124 | - "vC-Ops-Watchdog" 125 | ``` 126 | 127 | ## Running the software 128 | 129 | #### **inventory** 130 | 131 | The inventory must be started with a specific `target` 132 | 133 | * `--user`: specifiy user to log in 134 | * `--password`: specify password to log in 135 | * `--port`: specify inventory port 136 | * `--target`: define target vrops 137 | * `--config`: path to inventory config to set query filters (and resourcekinds - SDDC & VCOPS only) 138 | * `--v`: logging all level except debug 139 | * `--vv`: logging all level including debug 140 | * `--loopback`: use 127.0.0.1 address instead of listen to 0.0.0.0 (for test purpose) 141 | * `--sleep`: how often the resources are updated, default: 1800s 142 | 143 | 144 | #### **exporter** 145 | 146 | The exporter must be started with a specific `target`. Optionally a specific `collector`, otherwise the `default_collectors` in [collector-config](tests/collector_config.yaml) were used. 147 | 148 | * `--port`: specify exporter port 149 | * `--inventory`: inventory service address 150 | * `--config`: path to config to set default collectors, statkeys and properties for collectors 151 | * `--target`: define target vrops 152 | * `--collector`: enable collector (use multiple times) 153 | * `--rubric`: metric rubric in collector config 154 | * `--v`: logging all level except debug 155 | * `--vv`: logging all level including debug 156 | 157 | An example [collector-config](tests/collector_config.yaml). Add the desired `statkeys` and `properties` that your collectors should collect in a dedicated category. This is where `statkey` mapped to a `metric_suffix`. The `statkey` follows VMware notation (to make the API call) and the `metric_suffix` follows best practices as it should appear as a metric in prometheus. 158 | 159 | Metrics: 160 | [VMWARE Documentation | Metrics for vCenter Server Components](https://docs.vmware.com/en/vRealize-Operations/8.10/com.vmware.vcom.metrics.doc/GUID-9DB18E49-5E00-4534-B5FF-6276948D5A09.html) 161 | 162 | Properties: 163 | [VMWARE Documentation | Properties for vCenter Server Components](https://docs.vmware.com/en/vRealize-Operations/8.10/com.vmware.vcom.metrics.doc/GUID-0199A14B-019B-4EAD-B0AF-59097527ED59.html) 164 | 165 | Prometheus: 166 | [Prometheus | Metric and label naming](https://prometheus.io/docs/practices/naming/) 167 | 168 | In addition, vrops-exporter is able to fetch alerts from supported resource types and wrap them in an info metric containing all symptoms and recommendations. 169 | 170 | ```javascript 171 | vrops_hostsystem_alert_info{ 172 | alert_impact="HEALTH", 173 | alert_level="CRITICAL", 174 | alert_name="The host has lost connectivity to a dvPort", 175 | datacenter="datacenter1", 176 | description="One or more portgroups in the host lost connectivity to the dvPort. As a result, the services associated with the affected dvPorts are 177 | disconnected from the associated physical networks. All physical connections to the dvPort from the associated switch will become unavailable.", 178 | hostsystem="node001-prod1", 179 | recommendation_1="Replace the physical adapter or reset the physical switch. The alert will be canceled when connectivity is restored to the dvPort.", 180 | status="ACTIVE", 181 | symptom_1_data="{'condition': {'faultEvents': ['esx.problem.net.dvport.connectivity.lost'], 'faultKey': 'fault|dvp|conn', 'type': 'CONDITION_FAULT'}, 182 | 'severity': 'CRITICAL'}", 183 | symptom_1_name="Lost network connectivity to DVPorts", 184 | vccluster="prod-cluster1", 185 | vcenter="vcenter1" 186 | } 187 | ``` 188 | 189 | 190 | ###### **1. Build** 191 | To build the container simply run `make` and get the locally created docker container. 192 | 193 | ###### **2. CLI** 194 | 195 | Either specify the vars via environment or cli params. Because the inventory and the exporter are running seperately, 196 | you need to enter the Docker container at least twice. 197 | 198 | Start the container: 199 | 200 | docker run -it keppel.eu-de-1.cloud.sap/ccloud/vrops_exporter /bin/sh 201 | 202 | This will start the inventory container and directly enter the shell. Note, you need to define your vROps target beforehand [tests/inventory_config.yaml](tests/inventory_config.yaml#L1-L2). 203 | 204 | ./inventory.py --user foobaruser --password "foobarpw" --port 80 -m tests/inventory_config.yaml --vv 205 | 206 | Now you need to enter the container a second time: 207 | 208 | docker exec -it /bin/sh 209 | 210 | Now run the exporter: 211 | 212 | ./exporter.py --port 9000 --inventory localhost --config tests/collector_config.yaml --target 'vrops-vcenter-test.company.com' --vv 213 | 214 | You can also enter the container a third time to fetch the prometheus metrics from localhost (i.e. with wget) 215 | 216 | ###### **3. Enviroment variables** 217 | 218 | 219 | USER 220 | PASSWORD 221 | PORT 222 | INVENTORY 223 | LOOPBACK 224 | 225 | 226 | ## Running in Kubernetes 227 | For running this in kubernetes (like we do), you might want to have a look at our [helm chart](https://github.com/sapcc/helm-charts/tree/master/prometheus-exporters/vrops-exporter) 228 | 229 | 230 | ## Test 231 | Test module is called using ENV variables. Specifying these on the fly would look like this: 232 | 233 | Main test: 234 | ```shell 235 | LOOPBACK=0 DEBUG=0 INVENTORY=127.0.0.1:8000 USER=FOO PASSWORD=Bar CONFIG=tests/collector_config.yaml TARGET=vrops-vcenter-test.company.com python3 tests/TestCollectors.py 236 | ``` 237 | 238 | To run all tests you got to loop over it. 239 | ```shell 240 | for i in $(ls tests/Test*) 241 | do 242 | LOOPBACK=1 INVENTORY="127.0.0.1:8000" DEBUG=0 USER=FOO PASSWORD=Bar python3 $i 243 | done 244 | ``` 245 | 246 | Please note that USER and PASSWORD are currently doing nothing at all, they are only passed on because the test 247 | checks whether these are present. 248 | 249 | The test generates dummy return values for the queries to vROps and checks the functionality of the collectors. 250 | It compares whether the metrics as a result of the collector match the expected metrics in `metrics.yaml`. 251 | -------------------------------------------------------------------------------- /collectors/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/collectors/.DS_Store -------------------------------------------------------------------------------- /collectors/AlertCollector.py: -------------------------------------------------------------------------------- 1 | from BaseCollector import BaseCollector 2 | from prometheus_client.core import InfoMetricFamily 3 | import logging 4 | import time 5 | 6 | logger = logging.getLogger('vrops-exporter') 7 | 8 | 9 | class AlertCollector(BaseCollector): 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.resourcekind = list() 14 | self.adapterkind = list() 15 | self.alert_entry_cache = dict() 16 | 17 | def get_resource_uuids(self): 18 | raise NotImplementedError("Please Implement this method") 19 | 20 | def get_labels(self, resource_id: str, project_ids: list): 21 | raise NotImplementedError("Please Implement this method") 22 | 23 | def describe(self): 24 | yield InfoMetricFamily(f'vrops_{self.vrops_entity_name}_alert', 'vrops-exporter', ) 25 | 26 | def collect(self): 27 | logger.info(f'{self.name} starts with collecting the alerts') 28 | 29 | token = self.get_target_tokens() 30 | token = token.setdefault(self.target, '') 31 | if not token: 32 | logger.warning(f'skipping {self.target} in {self.name}, no token') 33 | return 34 | 35 | # Mapping uuids to names 36 | self.get_resource_uuids() 37 | alert_config = self.read_collector_config()['alerts'] 38 | 39 | alert_metric = self.generate_alert_metrics( 40 | label_names=self.label_names) 41 | project_ids = self.get_project_ids_by_target() if self.project_ids else [] 42 | alerts, api_responding, api_response_time = \ 43 | self.vrops.get_alerts(self.target, token, 44 | resourcekinds=self.resourcekind, 45 | alert_criticality=[ 46 | a for a in alert_config.get('alertCriticality')], 47 | active_only=alert_config.get('activeOnly'), 48 | adapterkinds=self.adapterkind if self.adapterkind else []) 49 | 50 | yield self.create_api_response_code_metric(self.name, api_responding) 51 | yield self.create_api_response_time_metric(self.name, api_response_time) 52 | 53 | if not alerts: 54 | logger.warning( 55 | f'No alerts in the response for {self.name}. API code: {api_responding}') 56 | return 57 | 58 | for alert in alerts: 59 | resource_id = alert.get('resourceId') 60 | labels = self.get_labels(resource_id, project_ids) 61 | if not labels: 62 | continue 63 | alert_labels = self.generate_alert_label_values(alert) 64 | if alert_labels: 65 | labels.extend([alert['alertDefinitionName'], 66 | alert['alertLevel'], 67 | alert['status'], 68 | alert["alertImpact"]]) 69 | alert_metric.add_metric(labels=labels, value=alert_labels) 70 | yield alert_metric 71 | 72 | def generate_alert_label_values(self, alert): 73 | alert_id = alert.get('alertDefinitionId', {}) 74 | alert_labels = dict() 75 | alert_entry = self.alert_entry_cache.get( 76 | alert_id) if alert_id in self.alert_entry_cache else self.get_alertdefinition(alert_id) 77 | if not alert_entry: 78 | return {} 79 | self.alert_entry_cache[alert_id] = alert_entry 80 | alert_labels['description'] = alert_entry.get('description', "n/a") 81 | for i, symptom in enumerate(alert_entry.get('symptoms', [])): 82 | alert_labels[f'symptom_{i+1}_name'] = symptom.get('name', "n/a") 83 | alert_labels[f'symptom_{i+1}_data'] = str( 84 | symptom.get('state', 'n/a')) 85 | for i, recommendation in enumerate(alert_entry.get('recommendations', [])): 86 | alert_labels[f'recommendation_{i+1}'] = recommendation.get( 87 | 'description', 'n/a') 88 | return alert_labels 89 | -------------------------------------------------------------------------------- /collectors/ClusterAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class ClusterAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'cluster' 9 | self.label_names = ['vccluster', 'vcenter', 'datacenter'] 10 | self.resourcekind = ["ClusterComputeResource"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_clusters_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | return [self.clusters[resource_id]['name'], 17 | self.clusters[resource_id]['vcenter'], 18 | self.clusters[resource_id]['parent_dc_name'].lower()] if resource_id in self.clusters else [] 19 | -------------------------------------------------------------------------------- /collectors/ClusterPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class ClusterPropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'cluster' 9 | self.label_names = ['vccluster', 'vcenter', 'datacenter'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_clusters_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.clusters[resource_id]['name'], 16 | self.clusters[resource_id]['vcenter'], 17 | self.clusters[resource_id]['parent_dc_name'].lower()] if resource_id in self.clusters else [] 18 | -------------------------------------------------------------------------------- /collectors/ClusterStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class ClusterStatsCollector(StatsCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'cluster' 9 | self.label_names = ['vccluster', 'vcenter', 'datacenter'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_clusters_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.clusters[resource_id]['name'], 16 | self.clusters[resource_id]['vcenter'], 17 | self.clusters[resource_id]['parent_dc_name'].lower()] if resource_id in self.clusters else [] 18 | -------------------------------------------------------------------------------- /collectors/CustomInfoMetricsGenerator.py: -------------------------------------------------------------------------------- 1 | from BaseCollector import BaseCollector 2 | 3 | from prometheus_client.core import InfoMetricFamily 4 | import logging 5 | 6 | logger = logging.getLogger('vrops-exporter') 7 | 8 | 9 | class CustomInfoMetricsGenerator(BaseCollector): 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.name = self.__class__.__name__ 14 | 15 | def describe(self): 16 | custom_metrics = self.read_collector_config().get('CustomInfoMetricsGenerator') 17 | for entry in custom_metrics: 18 | yield InfoMetricFamily(entry['metric'], 'vrops-exporter') 19 | 20 | def collect(self): 21 | logger.info(f'{self.name} starts with collecting the metrics') 22 | 23 | custom_metrics = self.read_collector_config().get('CustomInfoMetricsGenerator') 24 | for entry in custom_metrics: 25 | custom_info_metric = InfoMetricFamily(entry['metric'], 'vrops-exporter', labels=['target']) 26 | custom_info_metric.add_metric(labels=[self.target], value=entry['values_dict']) 27 | yield custom_info_metric 28 | -------------------------------------------------------------------------------- /collectors/DatastoreAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class DatastoreAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'datastore' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'type', 'datacenter', 'storagepod'] 10 | self.resourcekind = ["Datastore"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_datastores_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | label_values = [self.datastores[resource_id]['name'], 17 | self.datastores[resource_id]['vcenter'], 18 | self.datastores[resource_id]['type'], 19 | self.datastores[resource_id]['parent_dc_name'].lower()] if resource_id in self.datastores else [] 20 | 21 | if sc_name := self.datastores[resource_id].get('storage_cluster_name'): 22 | label_values.append(sc_name) 23 | else: 24 | label_values.append("none") 25 | 26 | return label_values 27 | -------------------------------------------------------------------------------- /collectors/DatastorePropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class DatastorePropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'datastore' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'type', 'datacenter', 'storagepod'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_datastores_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | label_values = [self.datastores[resource_id]['name'], 16 | self.datastores[resource_id]['vcenter'], 17 | self.datastores[resource_id]['type'], 18 | self.datastores[resource_id]['parent_dc_name'].lower()] if resource_id in self.datastores else [] 19 | 20 | if sc_name := self.datastores[resource_id].get('storage_cluster_name'): 21 | label_values.append(sc_name) 22 | else: 23 | label_values.append("none") 24 | 25 | return label_values 26 | -------------------------------------------------------------------------------- /collectors/DatastoreStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class DatastoreStatsCollector(StatsCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'datastore' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'type', 'datacenter', 'storagepod'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_datastores_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | label_values = [self.datastores[resource_id]['name'], 16 | self.datastores[resource_id]['vcenter'], 17 | self.datastores[resource_id]['type'], 18 | self.datastores[resource_id]['parent_dc_name'].lower()] if resource_id in self.datastores else [] 19 | 20 | if sc_name := self.datastores[resource_id].get('storage_cluster_name'): 21 | label_values.append(sc_name) 22 | else: 23 | label_values.append("none") 24 | 25 | return label_values 26 | -------------------------------------------------------------------------------- /collectors/DistributedvSwitchPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class DistributedvSwitchPropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'distributed_virtual_switch' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_dvs_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.dvs[resource_id]['name'], 16 | self.dvs[resource_id]['vcenter'], 17 | self.dvs[resource_id]['parent_dc_name'].lower()] if resource_id in self.dvs else [] 18 | -------------------------------------------------------------------------------- /collectors/HostSystemAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class HostSystemAlertCollector(AlertCollector): 5 | def __init__(self): 6 | super().__init__() 7 | self.vrops_entity_name = 'hostsystem' 8 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter', 'vccluster'] 9 | self.resourcekind = ["Hostsystem"] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_hosts_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.hosts[resource_id]['name'], 16 | self.hosts[resource_id]['vcenter'], 17 | self.hosts[resource_id]['datacenter'].lower(), 18 | self.hosts[resource_id]['parent_cluster_name']] if resource_id in self.hosts else [] 19 | -------------------------------------------------------------------------------- /collectors/HostSystemPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class HostSystemPropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'hostsystem' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter', 'vccluster', 'internal_name'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_hosts_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.hosts[resource_id]['name'], 16 | self.hosts[resource_id]['vcenter'], 17 | self.hosts[resource_id]['datacenter'].lower(), 18 | self.hosts[resource_id]['parent_cluster_name'], 19 | self.hosts[resource_id]['internal_name']] if resource_id in self.hosts else [] 20 | -------------------------------------------------------------------------------- /collectors/HostSystemStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class HostSystemStatsCollector(StatsCollector): 5 | def __init__(self): 6 | super().__init__() 7 | self.vrops_entity_name = 'hostsystem' 8 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter', 'vccluster', 'internal_name'] 9 | 10 | def get_resource_uuids(self): 11 | return self.get_hosts_by_target() 12 | 13 | def get_labels(self, resource_id, project_ids): 14 | return [self.hosts[resource_id]['name'], 15 | self.hosts[resource_id]['vcenter'], 16 | self.hosts[resource_id]['datacenter'].lower(), 17 | self.hosts[resource_id]['parent_cluster_name'], 18 | self.hosts[resource_id]['internal_name']] if resource_id in self.hosts else [] 19 | -------------------------------------------------------------------------------- /collectors/InventoryCollector.py: -------------------------------------------------------------------------------- 1 | from BaseCollector import BaseCollector 2 | 3 | from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily 4 | import logging 5 | 6 | logger = logging.getLogger('vrops-exporter') 7 | 8 | 9 | class InventoryCollector(BaseCollector): 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.name = self.__class__.__name__ 14 | 15 | def describe(self): 16 | for resourcekind in self.get_amount_resources().get(self.target, "empty"): 17 | yield GaugeMetricFamily(f'vrops_inventory_{resourcekind}', f'Amount of {resourcekind} in inventory') 18 | yield CounterMetricFamily('vrops_inventory_iteration', 'vrops_inventory') 19 | yield GaugeMetricFamily('vrops_inventory_collection_time_seconds', 'vrops_inventory') 20 | yield GaugeMetricFamily('vrops_api_response', 'vrops_inventory') 21 | yield GaugeMetricFamily('vrops_inventory_target', 'vrops_inventory') 22 | 23 | def collect(self): 24 | logger.info(f'{self.name} starts with collecting the metrics') 25 | 26 | for metric in (self.amount_inventory_resources(self.target) + 27 | self.api_response_metric(self.target) + 28 | self.vrops_node_service_states(self.target)): 29 | yield metric 30 | yield self.iteration_metric(self.target) 31 | yield self.collection_time_metric(self.target) 32 | yield self.inventory_targets_info(self.target) 33 | 34 | def amount_inventory_resources(self, target): 35 | gauges = list() 36 | for resourcekind, amount in self.get_amount_resources().get(target, {"empty": 0}).items(): 37 | if resourcekind == "empty": 38 | logger.warning( 39 | f'InventoryBuilder could not capture resources for {target}') 40 | gauge = GaugeMetricFamily(f'vrops_inventory_{resourcekind}', f'Amount of {resourcekind} in inventory', 41 | labels=["target"]) 42 | gauge.add_metric(labels=[target], value=amount) 43 | gauges.append(gauge) 44 | return gauges 45 | 46 | def iteration_metric(self, target): 47 | iteration_gauge = CounterMetricFamily( 48 | 'vrops_inventory_iteration', 'vrops_inventory', labels=["target"]) 49 | iteration = self.get_iteration() 50 | if not iteration: 51 | return iteration_gauge 52 | iteration_gauge.add_metric(labels=[target], value=iteration) 53 | return iteration_gauge 54 | 55 | def api_response_metric(self, target): 56 | api_response_gauge = GaugeMetricFamily('vrops_api_response', 57 | 'vrops_inventory', 58 | labels=["target", 59 | "class", 60 | "get_request"]) 61 | api_response_time_gauge = GaugeMetricFamily('vrops_api_response_time_seconds', 62 | 'vrops_inventory', 63 | labels=["target", 64 | "class", 65 | "get_request"]) 66 | 67 | status_code_dict, time_dict = self.get_inventory_api_responses() 68 | if not status_code_dict[target]: 69 | return api_response_gauge 70 | if not time_dict[target]: 71 | return api_response_time_gauge 72 | for get_request, status_code in status_code_dict[target].items(): 73 | api_response_gauge.add_metric(labels=[target, self.name.lower(), get_request], 74 | value=status_code) 75 | for get_request, response_time in time_dict[target].items(): 76 | api_response_time_gauge.add_metric(labels=[target, self.name.lower(), get_request], 77 | value=response_time) 78 | return [api_response_gauge, api_response_time_gauge] 79 | 80 | def collection_time_metric(self, target): 81 | collection_time_gauge = GaugeMetricFamily('vrops_inventory_collection_time_seconds', 'vrops_inventory', 82 | labels=["target"]) 83 | collection_time = self.get_collection_times()[target] 84 | if not collection_time: 85 | return collection_time_gauge 86 | collection_time_gauge.add_metric( 87 | labels=[target], value=collection_time) 88 | return collection_time_gauge 89 | 90 | def inventory_targets_info(self, target): 91 | inventory_target_info = GaugeMetricFamily('vrops_inventory_target', 'vrops_inventory', 92 | labels=["target"]) 93 | inventory_target_info.add_metric(labels=[target], value=1) 94 | return inventory_target_info 95 | 96 | def vrops_node_service_states(self, target): 97 | vrops_node_service_health = GaugeMetricFamily('vrops_node_service_health', 98 | 'vrops_inventory', 99 | labels=["target", 100 | "name", 101 | "health", 102 | "details"]) 103 | vrops_node_service_uptime = GaugeMetricFamily('vrops_node_service_uptime', 104 | 'vrops_inventory', 105 | labels=["target", 106 | "name"]) 107 | vrops_node_service_start_time = GaugeMetricFamily('vrops_node_service_start_time', 108 | 'vrops_inventory', 109 | labels=["target", 110 | "name"]) 111 | service_states = self.get_service_states().get("service") 112 | if not service_states: 113 | return [vrops_node_service_health, vrops_node_service_uptime, vrops_node_service_start_time] 114 | 115 | for service in service_states: 116 | vrops_node_service_health.add_metric(labels=[target, 117 | service.get("name", "N/A").lower(), 118 | service.get("health", "N/A").lower(), 119 | service.get("details", "N/A").lower()], 120 | value=1 if service.get("health").lower() == "ok" else 0) 121 | vrops_node_service_uptime.add_metric(labels=[target, 122 | service.get("name", "N/A").lower()], 123 | value=service.get("uptime", 0)) 124 | vrops_node_service_start_time.add_metric(labels=[target, 125 | service.get("name", "N/A").lower()], 126 | value=service.get("startedOn", 0)) 127 | return [vrops_node_service_health, vrops_node_service_uptime, vrops_node_service_start_time] 128 | -------------------------------------------------------------------------------- /collectors/NSXTAdapterAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class NSXTAdapterAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_adapter' 9 | self.label_names = [self.vrops_entity_name, 'target'] 10 | self.resourcekind = ["NSXTAdapterInstance"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_nsxt_adapter_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | return [self.nsxt_adapter[resource_id]['name'], 17 | self.nsxt_adapter[resource_id]['target']] if resource_id in self.nsxt_adapter else [] 18 | -------------------------------------------------------------------------------- /collectors/NSXTLogicalSwitchAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class NSXTLogicalSwitchAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_logical_switch' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', self.vrops_entity_name, 'target'] 10 | self.resourcekind = ["LogicalSwitch"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_nsxt_logical_switches_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | return [self.nsxt_logical_switches[resource_id]['mgmt_cluster_name'], 17 | self.nsxt_logical_switches[resource_id]['nsxt_adapter_name'], 18 | self.nsxt_logical_switches[resource_id]['name'], 19 | self.nsxt_logical_switches[resource_id]['target']] if resource_id in self.nsxt_logical_switches else [] 20 | -------------------------------------------------------------------------------- /collectors/NSXTLogicalSwitchPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class NSXTLogicalSwitchPropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_logical_switch' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', self.vrops_entity_name, 'target'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_nsxt_logical_switches_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.nsxt_logical_switches[resource_id]['mgmt_cluster_name'], 16 | self.nsxt_logical_switches[resource_id]['nsxt_adapter_name'], 17 | self.nsxt_logical_switches[resource_id]['name'], 18 | self.nsxt_logical_switches[resource_id]['target']] if resource_id in self.nsxt_logical_switches else [] 19 | -------------------------------------------------------------------------------- /collectors/NSXTMgmtClusterAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class NSXTMgmtClusterAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_mgmt_cluster' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'target'] 10 | self.resourcekind = ["ManagementCluster"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_nsxt_mgmt_cluster_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | return [self.nsxt_mgmt_cluster[resource_id]['name'], 17 | self.nsxt_mgmt_cluster[resource_id]['nsxt_adapter_name'], 18 | self.nsxt_mgmt_cluster[resource_id]['target']] if resource_id in self.nsxt_mgmt_cluster else [] 19 | -------------------------------------------------------------------------------- /collectors/NSXTMgmtClusterPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class NSXTMgmtClusterPropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_mgmt_cluster' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'target'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_nsxt_mgmt_cluster_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.nsxt_mgmt_cluster[resource_id]['name'], 16 | self.nsxt_mgmt_cluster[resource_id]['nsxt_adapter_name'], 17 | self.nsxt_mgmt_cluster[resource_id]['target']] if resource_id in self.nsxt_mgmt_cluster else [] 18 | -------------------------------------------------------------------------------- /collectors/NSXTMgmtClusterStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class NSXTMgmtClusterStatsCollector(StatsCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_mgmt_cluster' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'target'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_nsxt_mgmt_cluster_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.nsxt_mgmt_cluster[resource_id]['name'], 16 | self.nsxt_mgmt_cluster[resource_id]['nsxt_adapter_name'], 17 | self.nsxt_mgmt_cluster[resource_id]['target']] if resource_id in self.nsxt_mgmt_cluster else [] 18 | -------------------------------------------------------------------------------- /collectors/NSXTMgmtNodeAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class NSXTMgmtNodeAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_mgmt_node' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'nsxt_mgmt_node', 'target'] 10 | self.resourcekind = ["ManagementNode"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_nsxt_mgmt_nodes_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | return [self.nsxt_mgmt_nodes[resource_id]['mgmt_cluster_name'], 17 | self.nsxt_mgmt_nodes[resource_id]['nsxt_adapter_name'], 18 | self.nsxt_mgmt_nodes[resource_id]['name'], 19 | self.nsxt_mgmt_nodes[resource_id]['target']] if resource_id in self.nsxt_mgmt_nodes else [] 20 | -------------------------------------------------------------------------------- /collectors/NSXTMgmtNodePropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class NSXTMgmtNodePropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_mgmt_node' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'nsxt_mgmt_node', 'target'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_nsxt_mgmt_nodes_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.nsxt_mgmt_nodes[resource_id]['mgmt_cluster_name'], 16 | self.nsxt_mgmt_nodes[resource_id]['nsxt_adapter_name'], 17 | self.nsxt_mgmt_nodes[resource_id]['name'], 18 | self.nsxt_mgmt_nodes[resource_id]['target']] if resource_id in self.nsxt_mgmt_nodes else [] 19 | -------------------------------------------------------------------------------- /collectors/NSXTMgmtNodeStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class NSXTMgmtNodeStatsCollector(StatsCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_mgmt_node' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'nsxt_mgmt_node', 'target'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_nsxt_mgmt_nodes_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.nsxt_mgmt_nodes[resource_id]['mgmt_cluster_name'], 16 | self.nsxt_mgmt_nodes[resource_id]['nsxt_adapter_name'], 17 | self.nsxt_mgmt_nodes[resource_id]['name'], 18 | self.nsxt_mgmt_nodes[resource_id]['target']] if resource_id in self.nsxt_mgmt_nodes else [] 19 | -------------------------------------------------------------------------------- /collectors/NSXTMgmtServiceAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class NSXTMgmtServiceAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_mgmt_service' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'nsxt_mgmt_node', 'nsxt_mgmt_service', 'target'] 10 | self.resourcekind = ["ManagementService"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_nsxt_mgmt_service_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | return [self.nsxt_mgmt_service[resource_id]['mgmt_cluster_name'], 17 | self.nsxt_mgmt_service[resource_id]['nsxt_adapter_name'], 18 | self.nsxt_mgmt_service[resource_id]['mgmt_node_name'], 19 | self.nsxt_mgmt_service[resource_id]['name'], 20 | self.nsxt_mgmt_service[resource_id]['target']] if resource_id in self.nsxt_mgmt_service else [] -------------------------------------------------------------------------------- /collectors/NSXTTransportNodeAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class NSXTTransportNodeAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_transport_node' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'nsxt_transport_zone', 'nsxt_transport_node', 'target'] 10 | self.resourcekind = ["TransportNode"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_nsxt_transport_nodes_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | return [self.nsxt_transport_nodes[resource_id]['mgmt_cluster_name'], 17 | self.nsxt_transport_nodes[resource_id]['nsxt_adapter_name'], 18 | self.nsxt_transport_nodes[resource_id]['transport_zone_name'], 19 | self.nsxt_transport_nodes[resource_id]['name'], 20 | self.nsxt_transport_nodes[resource_id]['target']] if resource_id in self.nsxt_transport_nodes else [] 21 | -------------------------------------------------------------------------------- /collectors/NSXTTransportNodePropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class NSXTTransportNodePropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'nsxt_transport_node' 9 | self.label_names = ['nsxt_mgmt_cluster', 'nsxt_adapter', 'nsxt_transport_zone', 'nsxt_transport_node', 'target'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_nsxt_transport_nodes_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.nsxt_transport_nodes[resource_id]['mgmt_cluster_name'], 16 | self.nsxt_transport_nodes[resource_id]['nsxt_adapter_name'], 17 | self.nsxt_transport_nodes[resource_id]['transport_zone_name'], 18 | self.nsxt_transport_nodes[resource_id]['name'], 19 | self.nsxt_transport_nodes[resource_id]['target']] if resource_id in self.nsxt_transport_nodes else [] 20 | -------------------------------------------------------------------------------- /collectors/PropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from BaseCollector import BaseCollector 2 | import logging 3 | 4 | logger = logging.getLogger('vrops-exporter') 5 | 6 | 7 | class PropertiesCollector(BaseCollector): 8 | 9 | def get_resource_uuids(self): 10 | raise NotImplementedError("Please Implement this method") 11 | 12 | def unlock_nested_values(self, s, m): 13 | raise NotImplementedError("Please Implement this method") 14 | 15 | def get_labels(self, resource_id: str, project_ids: list): 16 | raise NotImplementedError("Please Implement this method") 17 | 18 | def collect(self): 19 | self.collect_running = True 20 | logger.info(f'{self.name} starts with collecting the metrics') 21 | if self.nested_value_metric_keys: 22 | logger.info(f'Found nested metric values for: {self.name}, keys: {self.nested_value_metric_keys}') 23 | 24 | token = self.get_target_tokens() 25 | token = token.setdefault(self.target, '') 26 | if not token: 27 | logger.warning(f'skipping {self.target} in {self.name}, no token') 28 | return 29 | 30 | uuids = self.get_resource_uuids() 31 | if not uuids: 32 | logger.warning(f'skipping {self.target} in {self.name}, no resources') 33 | return 34 | 35 | metrics = self.generate_metrics(label_names=self.label_names) 36 | project_ids = self.get_project_ids_by_target() if self.project_ids else [] 37 | values, api_responding, response_time = self.vrops.get_latest_properties_multiple(self.target, 38 | token, 39 | uuids, 40 | [m for m in metrics], 41 | self.name) 42 | yield self.create_api_response_code_metric(self.name, api_responding) 43 | yield self.create_api_response_time_metric(self.name, response_time) 44 | yield self.number_of_metrics_to_collect(self.name, len(metrics)) 45 | yield self.number_of_resources(self.name, len(uuids)) 46 | 47 | if not values: 48 | logger.warning(f'No values in the response for {self.name}. API code: {api_responding}') 49 | return 50 | 51 | values_received = set() 52 | no_match_in_config = list() 53 | 54 | for resource in values: 55 | resource_id = resource.get('resourceId') 56 | 57 | for value_entry in resource.get('property-contents', {}).get('property-content', []): 58 | labels = self.get_labels(resource_id, project_ids) 59 | if not labels: 60 | continue 61 | 62 | statkey = value_entry.get('statKey') 63 | values_received.add(statkey) 64 | 65 | metric_data = value_entry.get('data', [False])[0] 66 | metric_value = value_entry.get('values', [False])[0] 67 | 68 | if statkey in self.nested_value_metric_keys: 69 | 70 | add_labels, add_label_value_list, add_value = self.unlock_nested_values(statkey, metric_value) 71 | 72 | if not add_labels: 73 | metric_suffix = metrics[statkey]['metric_suffix'] 74 | self.add_metric_labels(metrics[statkey]['gauge'], [metric_suffix]) 75 | metrics[statkey]['gauge'].add_metric(labels=labels+[metric_suffix], value=0) 76 | continue 77 | 78 | self.add_metric_labels(metrics[statkey]['gauge'], add_labels) 79 | 80 | for add_label_value in add_label_value_list: 81 | metrics[statkey]['gauge'].add_metric(labels=labels+add_label_value, value=add_value) 82 | continue 83 | 84 | if statkey in metrics: 85 | # enum metrics 86 | if metrics[statkey]['expected']: 87 | self.add_metric_labels(metrics[statkey]['gauge'], ['state']) 88 | 89 | state = metric_value if metric_value else 'n/a' 90 | labels.append(state) 91 | value = 1 if metric_value == metrics[statkey]['expected'] else 0 92 | metrics[statkey]['gauge'].add_metric(labels=labels, value=value) 93 | 94 | # string values 95 | elif metric_value: 96 | metric_suffix = metrics[statkey]['metric_suffix'] 97 | self.add_metric_labels(metrics[statkey]['gauge'], [metric_suffix]) 98 | 99 | labels.append(metric_value) 100 | metrics[statkey]['gauge'].add_metric(labels=labels, value=1) 101 | 102 | # float values 103 | else: 104 | metrics[statkey]['gauge'].add_metric(labels=labels, value=metric_data) 105 | else: 106 | no_match_in_config.append([statkey, metric_data, labels]) 107 | 108 | # no match in config, bring into the right format 109 | created_metrics = self.generate_metrics_enriched_by_api(no_match_in_config, label_names=self.label_names) 110 | 111 | for metric in metrics: 112 | yield self.number_of_metric_samples_generated(self.name, metrics[metric]['gauge'].name, 113 | len(metrics[metric]['gauge'].samples)) 114 | yield metrics[metric]['gauge'] 115 | for metric in created_metrics: 116 | yield self.number_of_metric_samples_generated(self.name, created_metrics[metric].name, 117 | len(created_metrics[metric].samples)) 118 | logger.info(f'Created metrics enriched by API in {self.name}: {created_metrics[metric].name}') 119 | yield created_metrics[metric] 120 | self.collect_running = False 121 | -------------------------------------------------------------------------------- /collectors/SDDCAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class SDDCAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'sddc' 9 | self.label_names = [self.vrops_entity_name, "target"] 10 | self.resourcekind = [] 11 | self.adapterkind = ["SDDCHealthAdapter"] 12 | 13 | def get_resource_uuids(self): 14 | return self.get_sddc_objects_by_target() 15 | 16 | def get_labels(self, resource_id, project_ids): 17 | return [self.sddc_objects[resource_id]['name'], 18 | self.sddc_objects[resource_id]['target']] if resource_id in self.sddc_objects else [] 19 | -------------------------------------------------------------------------------- /collectors/SDRSPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | import json 3 | import logging 4 | 5 | logger = logging.getLogger('vrops-exporter') 6 | 7 | class SDRSPropertiesCollector(PropertiesCollector): 8 | 9 | def __init__(self): 10 | super().__init__() 11 | self.vrops_entity_name = 'storagepod' 12 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter'] 13 | self.nested_value_metric_keys = [ 14 | "config|sdrsconfig|vmStorageAntiAffinityRules" 15 | ] 16 | 17 | def get_resource_uuids(self): 18 | return self.get_SDRS_clusters_by_target() 19 | 20 | def get_labels(self, resource_id, project_ids): 21 | return [self.sdrs_clusters[resource_id]['name'], 22 | self.sdrs_clusters[resource_id]['vcenter'], 23 | self.sdrs_clusters[resource_id]['parent_dc_name'].lower()] if resource_id in self.sdrs_clusters else [] 24 | 25 | def unlock_nested_values(self, statkey, metric_value): 26 | 27 | match statkey: 28 | case "config|sdrsconfig|vmStorageAntiAffinityRules": 29 | return self.config_sdrsconfig_vmStorageAntiAffinityRules(metric_value) 30 | 31 | def config_sdrsconfig_vmStorageAntiAffinityRules(self, metric_value): 32 | 33 | try: 34 | metric_value = json.loads(metric_value) 35 | 36 | except (TypeError, json.decoder.JSONDecodeError) as e: 37 | logger.warning(f'metric_value is not a valid json: {e.args}, {metric_value}') 38 | return [], [], 0 39 | 40 | rules = metric_value.get("rules") or [] 41 | amount_rules = len(rules) 42 | 43 | rule_labels = ['rule', 'rule_name', 'rule_type', 'valid', 'virtualmachine'] 44 | rule_label_values = [] 45 | 46 | for i, rule in enumerate(rules): 47 | mapped_vms = self.vm_mapping_helper(rule.get('virtualMachines', [])) 48 | for vm in mapped_vms: 49 | rule_label_values.append([ 50 | f'{i+1}/{amount_rules}', 51 | rule.get('name'), 52 | rule.get('type'), 53 | str(rule.get('valid')).lower(), 54 | vm 55 | ]) 56 | return rule_labels, rule_label_values, 1 57 | 58 | def vm_mapping_helper(self, vm_list): 59 | mapped_vms = [] 60 | vms = self.get_vms(self.target) 61 | for rule_vm in vm_list: 62 | for vm in vms: 63 | if rule_vm == vms[vm].get('internal_name'): 64 | mapped_vms.append(vms[vm].get('name')) 65 | return mapped_vms 66 | -------------------------------------------------------------------------------- /collectors/SDRSStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class SDRSStatsCollector(StatsCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'storagepod' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_SDRS_clusters_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.sdrs_clusters[resource_id]['name'], 16 | self.sdrs_clusters[resource_id]['vcenter'], 17 | self.sdrs_clusters[resource_id]['parent_dc_name'].lower()] if resource_id in self.sdrs_clusters else [] 18 | -------------------------------------------------------------------------------- /collectors/StatsCollector.py: -------------------------------------------------------------------------------- 1 | from BaseCollector import BaseCollector 2 | import logging 3 | import re 4 | 5 | logger = logging.getLogger('vrops-exporter') 6 | 7 | 8 | class StatsCollector(BaseCollector): 9 | 10 | def get_resource_uuids(self): 11 | raise NotImplementedError("Please Implement this method") 12 | 13 | def get_labels(self, resource_id: str, project_ids: list): 14 | raise NotImplementedError("Please Implement this method") 15 | 16 | def collect(self): 17 | self.collect_running = True 18 | logger.info(f'{self.name} starts with collecting the metrics') 19 | 20 | token = self.get_target_tokens() 21 | token = token.setdefault(self.target, '') 22 | if not token: 23 | logger.warning(f'skipping {self.target} in {self.name}, no token') 24 | return 25 | 26 | uuids = self.get_resource_uuids() 27 | if not uuids: 28 | logger.warning(f'skipping {self.target} in {self.name}, no resources') 29 | return 30 | 31 | metrics = self.generate_metrics(label_names=self.label_names) 32 | project_ids = self.get_project_ids_by_target() if self.project_ids else [] 33 | values, api_responding, response_time = self.vrops.get_latest_stats_multiple(self.target, 34 | token, 35 | uuids, 36 | [m for m in metrics], 37 | self.name) 38 | yield self.create_api_response_code_metric(self.name, api_responding) 39 | yield self.create_api_response_time_metric(self.name, response_time) 40 | yield self.number_of_metrics_to_collect(self.name, len(metrics)) 41 | yield self.number_of_resources(self.name, len(uuids)) 42 | 43 | if not values: 44 | logger.warning(f'No values in the response for {self.name}. API code: {api_responding}') 45 | return 46 | 47 | values_received = set() 48 | no_match_in_config = list() 49 | 50 | for resource in values: 51 | resource_id = resource.get('resourceId') 52 | labels = self.get_labels(resource_id, project_ids) 53 | if not labels: 54 | continue 55 | 56 | for value_entry in resource.get('stat-list', {}).get('stat', []): 57 | statkey = value_entry.get('statKey', {}).get('key') 58 | # Normalisation of keys retrieved from API (e.g. cpu:102|usage_average -> cpu|usage_average) 59 | norm_statkey = re.sub("[^a-zA-Z|_ -]+", "", statkey) 60 | values_received.add(norm_statkey) 61 | 62 | metric_data = value_entry.get('data', [0])[0] 63 | if norm_statkey in metrics: 64 | metrics[norm_statkey]['gauge'].add_metric(labels=labels, value=metric_data) 65 | else: 66 | no_match_in_config.append([statkey, metric_data, labels]) 67 | 68 | # no match in config, bring into the right format 69 | created_metrics = self.generate_metrics_enriched_by_api(no_match_in_config, label_names=self.label_names) 70 | 71 | for metric in metrics: 72 | yield self.number_of_metric_samples_generated(self.name, metrics[metric]['gauge'].name, 73 | len(metrics[metric]['gauge'].samples)) 74 | yield metrics[metric]['gauge'] 75 | 76 | for metric in created_metrics: 77 | yield self.number_of_metric_samples_generated(self.name, created_metrics[metric].name, 78 | len(created_metrics[metric].samples)) 79 | logger.info(f'Created metrics enriched by API in {self.name}: {created_metrics[metric].name}') 80 | yield created_metrics[metric] 81 | self.collect_running = False 82 | -------------------------------------------------------------------------------- /collectors/VCenterAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class VCenterAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'vcenter' 9 | self.label_names = [self.vrops_entity_name, 'datacenter'] 10 | self.resourcekind = ["VMwareAdapter Instance"] 11 | 12 | def get_resource_uuids(self): 13 | return self.get_vcenters_by_target() 14 | 15 | def get_labels(self, resource_id, project_ids): 16 | return [self.vcenters[resource_id]['name'], 17 | self.vcenters[resource_id]['kind_dc_name'].lower()] if resource_id in self.vcenters else [] 18 | -------------------------------------------------------------------------------- /collectors/VCenterPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class VCenterPropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'vcenter' 9 | self.label_names = [self.vrops_entity_name, 'datacenter'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_vcenters_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.vcenters[resource_id]['name'], 16 | self.vcenters[resource_id]['kind_dc_name'].lower()] if resource_id in self.vcenters else [] 17 | -------------------------------------------------------------------------------- /collectors/VCenterStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class VCenterStatsCollector(StatsCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'vcenter' 9 | self.label_names = [self.vrops_entity_name, 'datacenter'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_vcenters_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.vcenters[resource_id]['name'], 16 | self.vcenters[resource_id]['kind_dc_name'].lower()] if resource_id in self.vcenters else [] 17 | -------------------------------------------------------------------------------- /collectors/VMAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class VMAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'virtualmachine' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter', 'vccluster', 'hostsystem', 'project'] 10 | self.resourcekind = ["Virtualmachine"] 11 | self.project_ids = True 12 | 13 | def get_resource_uuids(self): 14 | return self.get_vms_by_target() 15 | 16 | def get_labels(self, resource_id, project_ids): 17 | project_id = [vm_id_project_mapping[resource_id] for vm_id_project_mapping in project_ids if 18 | resource_id in vm_id_project_mapping] if resource_id in self.vms else [] 19 | project_id = project_id[0] if project_id else 'internal' 20 | 21 | return [self.vms[resource_id]['name'], 22 | self.vms[resource_id]['vcenter'], 23 | self.vms[resource_id]['datacenter'].lower(), 24 | self.vms[resource_id]['cluster'], 25 | self.vms[resource_id]['parent_host_name'], 26 | project_id] if resource_id in self.vms else [] 27 | -------------------------------------------------------------------------------- /collectors/VMPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class VMPropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'virtualmachine' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter', 'vccluster', 'hostsystem', 10 | 'internal_name', 'instance_uuid', 'project'] 11 | self.project_ids = True 12 | 13 | def get_resource_uuids(self): 14 | return self.get_vms_by_target() 15 | 16 | def get_labels(self, resource_id, project_ids): 17 | project_id = [vm_id_project_mapping[resource_id] for vm_id_project_mapping in project_ids if 18 | resource_id in vm_id_project_mapping] if resource_id in self.vms else [] 19 | project_id = project_id[0] if project_id else 'internal' 20 | 21 | return [self.vms[resource_id]['name'], 22 | self.vms[resource_id]['vcenter'], 23 | self.vms[resource_id]['datacenter'].lower(), 24 | self.vms[resource_id]['cluster'], 25 | self.vms[resource_id]['parent_host_name'], 26 | self.vms[resource_id]['internal_name'], 27 | self.vms[resource_id]['instance_uuid'], 28 | project_id] if resource_id else [] if resource_id in self.vms else [] 29 | -------------------------------------------------------------------------------- /collectors/VMStatsCPUCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.VMStatsCollector import VMStatsCollector 2 | 3 | 4 | class VMStatsCPUCollector(VMStatsCollector): 5 | def __init__(self): 6 | super().__init__() 7 | -------------------------------------------------------------------------------- /collectors/VMStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class VMStatsCollector(StatsCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'virtualmachine' 9 | self.label_names = [self.vrops_entity_name, 'vcenter', 'datacenter', 'vccluster', 'hostsystem', 10 | 'internal_name', 'instance_uuid', 'project'] 11 | self.project_ids = True 12 | 13 | def get_resource_uuids(self): 14 | return self.get_vms_by_target() 15 | 16 | def get_labels(self, resource_id, project_ids): 17 | project_id = [vm_id_project_mapping[resource_id] for vm_id_project_mapping in project_ids if 18 | resource_id in vm_id_project_mapping] if resource_id in self.vms else [] 19 | project_id = project_id[0] if project_id else 'internal' 20 | 21 | return [self.vms[resource_id]['name'], 22 | self.vms[resource_id]['vcenter'], 23 | self.vms[resource_id]['datacenter'].lower(), 24 | self.vms[resource_id]['cluster'], 25 | self.vms[resource_id]['parent_host_name'], 26 | self.vms[resource_id]['internal_name'], 27 | self.vms[resource_id]['instance_uuid'], 28 | project_id] if resource_id in self.vms else [] 29 | -------------------------------------------------------------------------------- /collectors/VMStatsDefaultCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.VMStatsCollector import VMStatsCollector 2 | 3 | 4 | class VMStatsDefaultCollector(VMStatsCollector): 5 | def __init__(self): 6 | super().__init__() 7 | -------------------------------------------------------------------------------- /collectors/VMStatsMemoryCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.VMStatsCollector import VMStatsCollector 2 | 3 | 4 | class VMStatsMemoryCollector(VMStatsCollector): 5 | def __init__(self): 6 | super().__init__() 7 | -------------------------------------------------------------------------------- /collectors/VMStatsNetworkCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.VMStatsCollector import VMStatsCollector 2 | 3 | 4 | class VMStatsNetworkCollector(VMStatsCollector): 5 | def __init__(self): 6 | super().__init__() 7 | -------------------------------------------------------------------------------- /collectors/VMStatsVirtualDiskCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.VMStatsCollector import VMStatsCollector 2 | 3 | 4 | class VMStatsVirtualDiskCollector(VMStatsCollector): 5 | def __init__(self): 6 | super().__init__() 7 | -------------------------------------------------------------------------------- /collectors/VcopsSelfMonitoringAlertCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.AlertCollector import AlertCollector 2 | 3 | 4 | class VcopsSelfMonitoringAlertCollector(AlertCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'self_object' 9 | self.label_names = [self.vrops_entity_name, "target"] 10 | self.resourcekind = [] 11 | self.adapterkind = ["vCenter Operations Adapter"] 12 | 13 | def get_resource_uuids(self): 14 | return self.get_vcops_objects_by_target() 15 | 16 | def get_labels(self, resource_id, project_ids): 17 | return [self.vcops_objects[resource_id]['name'], 18 | self.vcops_objects[resource_id]['target']] if resource_id in self.vcops_objects else [] 19 | -------------------------------------------------------------------------------- /collectors/VcopsSelfMonitoringPropertiesCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.PropertiesCollector import PropertiesCollector 2 | 3 | 4 | class VcopsSelfMonitoringPropertiesCollector(PropertiesCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'self_object' 9 | self.label_names = [self.vrops_entity_name, 'target'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_vcops_objects_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.vcops_objects[resource_id]['name'], 16 | self.vcops_objects[resource_id]['target']] if resource_id in self.vcops_objects else [] 17 | -------------------------------------------------------------------------------- /collectors/VcopsSelfMonitoringStatsCollector.py: -------------------------------------------------------------------------------- 1 | from collectors.StatsCollector import StatsCollector 2 | 3 | 4 | class VcopsSelfMonitoringStatsCollector(StatsCollector): 5 | 6 | def __init__(self): 7 | super().__init__() 8 | self.vrops_entity_name = 'self_object' 9 | self.label_names = [self.vrops_entity_name, 'target'] 10 | 11 | def get_resource_uuids(self): 12 | return self.get_vcops_objects_by_target() 13 | 14 | def get_labels(self, resource_id, project_ids): 15 | return [self.vcops_objects[resource_id]['name'], 16 | self.vcops_objects[resource_id]['target']] if resource_id in self.vcops_objects else [] 17 | -------------------------------------------------------------------------------- /collectors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/collectors/__init__.py -------------------------------------------------------------------------------- /exporter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import time 4 | import os 5 | import importlib 6 | import requests 7 | import logging 8 | import random 9 | import signal 10 | from prometheus_client import start_http_server 11 | from prometheus_client.core import REGISTRY 12 | from optparse import OptionParser 13 | from tools.helper import yaml_read 14 | 15 | 16 | def default_collectors(): 17 | collector_config = yaml_read(os.environ['COLLECTOR_CONFIG']).get('default_collectors') 18 | return [collector for collector in collector_config] if collector_config else None 19 | 20 | 21 | def parse_params(logger): 22 | # init logging here for setting the log level 23 | formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s') 24 | ConsoleHandler = logging.StreamHandler() 25 | logger.addHandler(ConsoleHandler) 26 | ConsoleHandler.setFormatter(formatter) 27 | 28 | parser = OptionParser() 29 | parser.add_option("-o", "--port", help="specify exporter (exporter.py) or inventory serving port(inventory.py)", 30 | action="store", dest="port") 31 | parser.add_option("-i", "--inventory", help="inventory service address", action="store", dest="inventory") 32 | parser.add_option("-v", "--v", help="logging all level except debug", action="store_true", dest="info", 33 | default=False) 34 | parser.add_option("-d", "--vv", help="logging all level including debug", action="store_true", dest="debug", 35 | default=False) 36 | parser.add_option("-c", "--collector", help="enable collector (use multiple times)", action="append", 37 | dest="collectors") 38 | parser.add_option("-m", "--config", help="path to config to set default collectors, statkeys and properties for " 39 | "collectors", action="store", dest="config") 40 | parser.add_option("-t", "--target", help="define target vrops", action="store", dest="target") 41 | (options, args) = parser.parse_args() 42 | 43 | if options.inventory: 44 | os.environ['INVENTORY'] = options.inventory 45 | if options.info: 46 | logger.setLevel(logging.INFO) 47 | ConsoleHandler.setLevel(logging.INFO) 48 | logger.info('Starting exporter logging on INFO level') 49 | if options.debug: 50 | logger.setLevel(logging.DEBUG) 51 | ConsoleHandler.setLevel(logging.DEBUG) 52 | logger.debug('Starting exporter logging on DEBUG level') 53 | if not options.debug and not options.info: 54 | logger.setLevel(logging.WARNING) 55 | ConsoleHandler.setLevel(logging.WARNING) 56 | logger.warning('Starting exporter logging on WARNING, ERROR and CRITICAL level') 57 | if options.port: 58 | os.environ['PORT'] = options.port 59 | if options.config: 60 | os.environ['COLLECTOR_CONFIG'] = options.config 61 | if not options.collectors: 62 | logger.debug('Exporter using default collectors from config') 63 | options.collectors = default_collectors() 64 | if options.target: 65 | os.environ['TARGET'] = options.target 66 | 67 | if "PORT" not in os.environ and not options.port: 68 | logger.error('Cannot start, please specify port with ENV or -o') 69 | sys.exit(1) 70 | if "INVENTORY" not in os.environ and not options.inventory: 71 | logger.error('Cannot start, please specify inventory with ENV or -i') 72 | sys.exit(1) 73 | if "COLLECTOR_CONFIG" not in os.environ and not options.config: 74 | logger.error('Cannot start, please specify collector config with ENV or -m') 75 | sys.exit(1) 76 | if "TARGET" not in os.environ and not options.target: 77 | logger.error('Cannot start, please specify TARGET with ENV or -a') 78 | sys.exit(1) 79 | if not options.collectors: 80 | logger.error('Cannot start, no default collectors activated in config') 81 | sys.exit(1) 82 | 83 | return options 84 | 85 | 86 | def run_prometheus_server(port, collectors, *args): 87 | start_http_server(int(port)) 88 | for c in collectors: 89 | REGISTRY.register(c) 90 | signal.signal(signal.SIGTERM, exit_gracefully) 91 | while True: 92 | time.sleep(1) 93 | 94 | 95 | def exit_gracefully(no, frm): 96 | logger.warning('SIGTERM received, shutting down gently.') 97 | for c in collectors: 98 | while c.collect_running: 99 | logger.debug(f'Waiting for {c.name} to finish current collect run.') 100 | time.sleep(1) 101 | logger.debug(f'Unregistering {c.name}') 102 | REGISTRY.unregister(c) 103 | sys.exit(0) 104 | 105 | 106 | def initialize_collector_by_name(class_name, logger): 107 | try: 108 | class_module = importlib.import_module(f'collectors.{class_name}') 109 | except ModuleNotFoundError as e: 110 | print('No Collector "BogusCollector" defined. Ignoring...') 111 | logger.error(f'No Collector {class_name} defined. {e}') 112 | return None 113 | 114 | try: 115 | return class_module.__getattribute__(class_name)() 116 | except AttributeError as e: 117 | print('Unable to initialize "ClassNotDefinedCollector". Ignoring...') 118 | logger.error(f'Unable to initialize {class_name}. {e}') 119 | return None 120 | 121 | 122 | if __name__ == '__main__': 123 | logger = logging.getLogger('vrops-exporter') 124 | options = parse_params(logger) 125 | global collectors 126 | collectors = list(map(lambda c: initialize_collector_by_name(c, logger), options.collectors)) 127 | run_prometheus_server(int(os.environ['PORT']), collectors) 128 | -------------------------------------------------------------------------------- /images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/images/.DS_Store -------------------------------------------------------------------------------- /images/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/images/architecture.jpg -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/images/architecture.png -------------------------------------------------------------------------------- /images/collectors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/images/collectors.png -------------------------------------------------------------------------------- /inventory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from inventory.Builder import InventoryBuilder 3 | from optparse import OptionParser 4 | from tools.helper import yaml_read 5 | import sys 6 | import os 7 | import logging 8 | import random 9 | import signal 10 | 11 | 12 | def parse_params(logger): 13 | # init logging here for setting the log level 14 | formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s') 15 | ConsoleHandler = logging.StreamHandler() 16 | logger.addHandler(ConsoleHandler) 17 | ConsoleHandler.setFormatter(formatter) 18 | 19 | parser = OptionParser() 20 | parser.add_option("-u", "--user", help="specify user to log in", action="store", dest="user") 21 | parser.add_option("-p", "--password", help="specify password to log in", action="store", dest="password") 22 | parser.add_option("-o", "--port", help="specify inventory port", action="store", dest="port") 23 | parser.add_option("-m", "--config", help="Path to the configuration to set properties of the resources kept in " 24 | "the inventory", action="store", dest="config") 25 | parser.add_option("-t", "--target", help="define target vrops", action="store", dest="target") 26 | parser.add_option("-v", "--v", help="logging all level except debug", action="store_true", dest="info", 27 | default=False) 28 | parser.add_option("-d", "--vv", help="logging all level including debug", action="store_true", dest="debug", 29 | default=False) 30 | parser.add_option("-l", "--loopback", help="use 127.0.0.1 address instead of listen to 0.0.0.0", 31 | action="store_true", dest="loopback") 32 | parser.add_option("-s", "--sleep", help="specifiy sleep time for inventory builder, default: 1800", action="store", 33 | dest="sleep") 34 | (options, args) = parser.parse_args() 35 | 36 | if options.user: 37 | os.environ['USER'] = options.user 38 | if options.password: 39 | os.environ['PASSWORD'] = options.password 40 | if options.info: 41 | logger.setLevel(logging.INFO) 42 | ConsoleHandler.setLevel(logging.INFO) 43 | logger.info('Starting inventory logging on INFO level') 44 | if options.debug: 45 | logger.setLevel(logging.DEBUG) 46 | ConsoleHandler.setLevel(logging.DEBUG) 47 | logger.debug('Starting inventory logging on DEBUG level') 48 | if not options.debug and not options.info: 49 | logger.setLevel(logging.WARNING) 50 | ConsoleHandler.setLevel(logging.WARNING) 51 | logger.warning('Starting inventory logging on WARNING, ERROR and CRITICAL level') 52 | if options.loopback: 53 | os.environ['LOOPBACK'] = "1" 54 | if options.port: 55 | os.environ['PORT'] = options.port 56 | if options.config: 57 | os.environ['INVENTORY_CONFIG'] = options.config 58 | if options.target: 59 | os.environ['TARGET'] = options.target 60 | if options.sleep: 61 | os.environ['SLEEP'] = options.sleep 62 | if not options.sleep and 'SLEEP' not in os.environ: 63 | logger.info('Defaulting sleep to 60s') 64 | os.environ['SLEEP'] = "60" 65 | 66 | if "PORT" not in os.environ and not options.port: 67 | logger.error('Cannot start, please specify PORT with ENV or -o') 68 | sys.exit(1) 69 | if "USER" not in os.environ and not options.user: 70 | logger.error('Cannot start, please specify USER with ENV or -u') 71 | sys.exit(1) 72 | if "PASSWORD" not in os.environ and not options.password: 73 | logger.error('Cannot start, please specify PASSWORD with ENV or -p') 74 | sys.exit(1) 75 | if "INVENTORY_CONFIG" not in os.environ and not options.config: 76 | logger.error('Cannot start, please specify inventory config with ENV or -m') 77 | sys.exit(1) 78 | if "TARGET" not in os.environ and not options.target: 79 | logger.error('Cannot start, please specify TARGET with ENV or -t') 80 | sys.exit(1) 81 | 82 | return options 83 | 84 | 85 | def exit_gracefully(no, frm): 86 | logger.warning('SIGTERM received, bye.') 87 | import requests 88 | requests.get("http://127.0.0.1/stop") 89 | sys.exit(0) 90 | 91 | 92 | if __name__ == '__main__': 93 | logger = logging.getLogger('vrops-exporter') 94 | options = parse_params(logger) 95 | signal.signal(signal.SIGTERM, exit_gracefully) 96 | InventoryBuilder(os.environ.get('TARGET'), os.environ['PORT'], os.environ['SLEEP']) 97 | -------------------------------------------------------------------------------- /inventory/Api.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from flask import Flask 3 | from gevent.pywsgi import WSGIServer 4 | import logging 5 | import json 6 | import os 7 | 8 | logger = logging.getLogger('vrops-exporter') 9 | 10 | 11 | class InventoryApi: 12 | def __init__(self, builder, port): 13 | self.builder = builder 14 | self.port = int(port) 15 | self.wsgi_address = '0.0.0.0' 16 | if 'LOOPBACK' in os.environ: 17 | if os.environ['LOOPBACK'] == '1': 18 | self.wsgi_address = '127.0.0.1' 19 | thread = Thread(target=self.run_rest_server) 20 | thread.start() 21 | 22 | def run_rest_server(self): 23 | 24 | app = Flask(__name__) 25 | logger.info(f'serving /target on {self.port}') 26 | 27 | @app.route('/target', methods=['GET']) 28 | def target(): 29 | return json.dumps(self.builder.target) 30 | 31 | logger.info(f'serving /inventory on {self.port}') 32 | 33 | @app.route('//vcenters/', methods=['GET']) 34 | def vcenters(target, iteration): 35 | return self.builder.iterated_inventory.get(str(iteration), {}).get('vcenters', {}).get(target, {}) 36 | 37 | @app.route('//datacenters/', methods=['GET']) 38 | def datacenters(target, iteration): 39 | return self.builder.iterated_inventory.get(str(iteration), {}).get('datacenters', {}).get(target, {}) 40 | 41 | @app.route('//clusters/', methods=['GET']) 42 | def clusters(target, iteration): 43 | return self.builder.iterated_inventory.get(str(iteration), {}).get('clusters', {}).get(target, {}) 44 | 45 | @app.route('//hosts/', methods=['GET']) 46 | def hosts(target, iteration): 47 | return self.builder.iterated_inventory.get(str(iteration), {}).get('hosts', {}).get(target, {}) 48 | 49 | @app.route('//datastores/', methods=['GET']) 50 | def datastores(target, iteration): 51 | return self.builder.iterated_inventory.get(str(iteration), {}).get('datastores', {}).get(target, {}) 52 | 53 | @app.route('//storagepod/', methods=['GET']) 54 | def storagepod(target, iteration): 55 | return self.builder.iterated_inventory.get(str(iteration), {}).get('storagepod', {}).get(target, {}) 56 | 57 | @app.route('//vms/', methods=['GET']) 58 | def vms(target, iteration): 59 | return self.builder.iterated_inventory.get(str(iteration), {}).get('vms', {}).get(target, {}) 60 | 61 | @app.route('//dvs/', methods=['GET']) 62 | def distributed_virtual_switches(target, iteration): 63 | return self.builder.iterated_inventory.get(str(iteration), {}).get('distributed_virtual_switches', {}).get( 64 | target, {}) 65 | 66 | @app.route('//nsxt_adapter/', methods=['GET']) 67 | def nsxt_adapter(target, iteration): 68 | return self.builder.iterated_inventory.get(str(iteration), {}).get('nsxt_adapter', {}).get(target, {}) 69 | 70 | @app.route('//nsxt_mgmt_cluster/', methods=['GET']) 71 | def nsxt_mgmt_cluster(target, iteration): 72 | return self.builder.iterated_inventory.get(str(iteration), {}).get('nsxt_mgmt_cluster', {}).get(target, {}) 73 | 74 | @app.route('//nsxt_mgmt_nodes/', methods=['GET']) 75 | def nsxt_mgmt_nodes(target, iteration): 76 | return self.builder.iterated_inventory.get(str(iteration), {}).get('nsxt_mgmt_nodes', {}).get(target, {}) 77 | 78 | @app.route('//nsxt_mgmt_service/', methods=['GET']) 79 | def nsxt_mgmt_service(target, iteration): 80 | return self.builder.iterated_inventory.get(str(iteration), {}).get('nsxt_mgmt_service', {}).get(target, {}) 81 | 82 | @app.route('//nsxt_transport_nodes/', methods=['GET']) 83 | def nsxt_transport_nodes(target, iteration): 84 | return self.builder.iterated_inventory.get(str(iteration), {}).get('nsxt_transport_nodes', {}).get(target, 85 | {}) 86 | 87 | @app.route('//nsxt_logical_switches/', methods=['GET']) 88 | def nsxt_logical_switches(target, iteration): 89 | return self.builder.iterated_inventory.get(str(iteration), {}).get('nsxt_logical_switches', {}).get(target, 90 | {}) 91 | 92 | @app.route('//vcops_objects/', methods=['GET']) 93 | def vcops_self_monitoring_objects(target, iteration): 94 | return self.builder.iterated_inventory.get(str(iteration), {}).get('vcops_objects', {}).get(target, {}) 95 | 96 | @app.route('//sddc_objects/', methods=['GET']) 97 | def sddc_health_objects(target, iteration): 98 | return self.builder.iterated_inventory.get(str(iteration), {}).get('sddc_objects', {}).get(target, {}) 99 | 100 | @app.route('/alertdefinitions/', methods=['GET']) 101 | def alert_alertdefinition(alert_id): 102 | return self.builder.alertdefinitions.get(alert_id, {}) 103 | 104 | @app.route('/alertdefinitions', methods=['GET']) 105 | def alert_alertdefinitions(): 106 | return self.builder.alertdefinitions 107 | 108 | @app.route('/iteration', methods=['GET']) 109 | def iteration(): 110 | return_iteration = self.builder.successful_iteration_list[-1] 111 | return str(return_iteration) 112 | 113 | @app.route('/amount_resources', methods=['GET']) 114 | def amount_resources(): 115 | amount_resources = self.builder.amount_resources 116 | return json.dumps(amount_resources) 117 | 118 | @app.route('/collection_times', methods=['GET']) 119 | def collection_times(): 120 | vrops_collection_times = self.builder.vrops_collection_times 121 | return json.dumps(vrops_collection_times) 122 | 123 | @app.route('/api_response_codes', methods=['GET']) 124 | def api_response_codes(): 125 | response_codes = self.builder.response_codes 126 | return json.dumps(response_codes) 127 | 128 | @app.route('/api_response_times', methods=['GET']) 129 | def api_response_times(): 130 | response_times = self.builder.response_times 131 | return json.dumps(response_times) 132 | 133 | @app.route('/service_states', methods=['GET']) 134 | def service_states(): 135 | service_states = self.builder.service_states 136 | return json.dumps(service_states) 137 | 138 | # debugging purpose 139 | @app.route('/iteration_store', methods=['GET']) 140 | def iteration_store(): 141 | return_iteration = self.builder.successful_iteration_list 142 | return json.dumps(return_iteration) 143 | 144 | @app.route('/stop') 145 | def stop(): 146 | self.builder.am_i_killed = True 147 | self.WSGIServer.stop() 148 | return "Bye" 149 | 150 | # FIXME: this could basically be the always active token list. no active token? refresh! 151 | @app.route('/target_tokens', methods=['GET']) 152 | def token(): 153 | return json.dumps(self.builder.target_tokens) 154 | 155 | try: 156 | if logger.level == 10: 157 | # WSGi is logging on DEBUG Level 158 | self.WSGIServer = WSGIServer((self.wsgi_address, self.port), app) 159 | else: 160 | self.WSGIServer = WSGIServer((self.wsgi_address, self.port), app, log=None) 161 | self.WSGIServer.serve_forever() 162 | except TypeError as e: 163 | logger.error('Problem starting server, you might want to try LOOPBACK=0 or LOOPBACK=1') 164 | logger.error(f'Current used options: {self.wsgi_address} on port {self.port}') 165 | logger.error(f'TypeError: {e}') 166 | -------------------------------------------------------------------------------- /inventory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/inventory/__init__.py -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | prometheus-client 3 | requests 4 | six 5 | PyYAML 6 | Flask==3.1.0 7 | Werkzeug==3.1.3 8 | gevent 9 | cffi 10 | beautifulsoup4==4.13.4 11 | lxml 12 | itsdangerous==2.2.0 13 | jinja2==3.1.6 14 | simplejson 15 | -------------------------------------------------------------------------------- /tests/TestCollectorInit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('.') 4 | import os 5 | import unittest 6 | import logging 7 | from unittest import TestCase 8 | from unittest.mock import call, patch, MagicMock 9 | import importlib 10 | import collectors.HostSystemStatsCollector 11 | from exporter import initialize_collector_by_name 12 | 13 | logger = logging.getLogger('test-logger') 14 | 15 | 16 | class TestCollectorInitialization(TestCase): 17 | print(f"Running TestCollectorInitialization") 18 | os.environ.setdefault('TARGET', "vrops-vcenter-test.company.com") 19 | collectors.HostSystemStatsCollector.StatsCollector.get_vrops_target = MagicMock( 20 | return_value='vrops-vcenter-test.company.com') 21 | collectors.HostSystemStatsCollector.StatsCollector.read_collector_config = MagicMock(return_value={}) 22 | 23 | @patch('BaseCollector.BaseCollector.wait_for_inventory_data') 24 | def test_valid_collector2(self, mocked_wait): 25 | mocked_wait.return_value = None 26 | collector = initialize_collector_by_name('HostSystemStatsCollector', logger) 27 | self.assertIsInstance(collector, collectors.HostSystemStatsCollector.HostSystemStatsCollector) 28 | 29 | @patch('builtins.print') 30 | def test_with_bogus_collector(self, mocked_print): 31 | collector = initialize_collector_by_name('BogusCollector', logger) 32 | self.assertIsNone(collector) 33 | self.assertEqual(mocked_print.mock_calls, [call('No Collector "BogusCollector" defined. Ignoring...')]) 34 | 35 | @patch('builtins.print') 36 | def test_with_invalid_collector(self, mocked_print): 37 | importlib.import_module = MagicMock(return_value=collectors.HostSystemStatsCollector) 38 | collector = initialize_collector_by_name('ClassNotDefinedCollector', logger) 39 | self.assertIsNone(collector) 40 | self.assertEqual(mocked_print.mock_calls, 41 | [call('Unable to initialize "ClassNotDefinedCollector". Ignoring...')]) 42 | 43 | 44 | if __name__ == '__main__': 45 | unittest.main() 46 | -------------------------------------------------------------------------------- /tests/TestCollectors.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('.') 4 | from unittest.mock import MagicMock, patch 5 | from threading import Thread 6 | from exporter import run_prometheus_server 7 | from tools.helper import yaml_read 8 | from tools.Vrops import Vrops 9 | from inventory.Builder import InventoryBuilder 10 | from BaseCollector import BaseCollector 11 | from prometheus_client.core import REGISTRY 12 | import unittest 13 | import random 14 | import http.client 15 | import os 16 | import time 17 | import importlib 18 | 19 | 20 | class TestCollectors(unittest.TestCase): 21 | print(f"Running TestCollectors") 22 | os.environ.setdefault('TARGET', "vrops-vcenter-test.company.com") 23 | 24 | def test_environment(self): 25 | self.assertTrue(os.getenv('USER'), 'no dummy USER set') 26 | self.assertTrue(os.getenv('PASSWORD'), 'no dummy PASSWORD set') 27 | self.assertTrue(os.getenv('COLLECTOR_CONFIG'), 'no COLLECTOR CONFIG set') 28 | self.assertTrue(os.getenv('INVENTORY_CONFIG'), 'no INVENTORY CONFIG set') 29 | self.assertTrue(os.getenv('TARGET'), 'no target set') 30 | self.assertEqual(os.getenv('TARGET'), "vrops-vcenter-test.company.com", "The test must be run with the target: vrops-vcenter-test.company.com") 31 | 32 | @patch('exporter.signal.signal') 33 | def test_collector_metrics(self, mock_signal): 34 | self.metrics_yaml = yaml_read('tests/metrics.yaml') 35 | self.collector_config = yaml_read(os.environ['COLLECTOR_CONFIG']) 36 | self.target = os.getenv('TARGET') 37 | self.token = '2ed214d523-235f-h283-4566-6sf356124fd62::f234234-234' 38 | # every collector got to be tested in here 39 | self.random_prometheus_port = random.randrange(9000, 9700, 1) 40 | print("chosen testport: " + str(self.random_prometheus_port)) 41 | 42 | BaseCollector.get_target_tokens = MagicMock( 43 | return_value={self.target: self.token}) 44 | Vrops.get_token = MagicMock(return_value=("2ed214d523-235f-h283-4566-6sf356124fd62::f234234-234", 200, 0.282561)) 45 | 46 | def create_adapter_objects(adapterkind) -> list: 47 | uuids = ["3628-93a1-56e84634050814", "3628-93a1-56e84634050814"] 48 | object_list = list() 49 | for i, _ in enumerate(uuids): 50 | resource_object = type(adapterkind.capitalize(), (object,), { 51 | "name": f'{adapterkind.lower()}_{i + 1}', 52 | "uuid": uuids[0], 53 | "target": self.target, 54 | "token": self.token 55 | }) 56 | object_list.append(resource_object) 57 | return object_list 58 | 59 | Vrops.get_adapter = MagicMock(return_value=None) 60 | Vrops.get_vcenter_adapter = MagicMock(return_value=(create_adapter_objects("Vcenter"), 200, 0.282561)) 61 | Vrops.get_nsxt_adapter = MagicMock(return_value=(create_adapter_objects("NSXTAdapterInstance"), 200, 0.282561)) 62 | Vrops.get_vcenter_operations_adapter_intance = MagicMock( 63 | return_value=(create_adapter_objects("VcopsAdapterInstance"), 200, 0.282561)) 64 | Vrops.get_sddc_health_adapter_intance = MagicMock( 65 | return_value=(create_adapter_objects("SDDCAdapterInstance"), 200, 0.282561)) 66 | 67 | # test tool get_resources to create resource objects 68 | def create_resource_objects(resourcekind) -> list: 69 | uuids = ["3628-93a1-56e84634050814", "7422-91h7-52s842060815", "5628-9ba1-55e847050815"] 70 | object_list = list() 71 | for i, _ in enumerate(uuids): 72 | resource_object = type(resourcekind.capitalize(), (object,), { 73 | "name": f'{resourcekind.capitalize()}_{i + 1}', 74 | "uuid": uuids[i], 75 | "resourcekind": resourcekind, 76 | "parent": uuids[0], 77 | "internal_name": f'{resourcekind.lower()}_1234', 78 | "instance_uuid": f'{resourcekind.lower()}_12345678' 79 | }) 80 | object_list.append(resource_object) 81 | return object_list 82 | 83 | Vrops.get_nsxt_mgmt_cluster = MagicMock(return_value=(create_resource_objects("ManagementCluster"), 200, 0.282561)) 84 | Vrops.get_nsxt_mgmt_nodes = MagicMock(return_value=(create_resource_objects("ManagementNode"), 200, 0.282561)) 85 | Vrops.get_nsxt_mgmt_service = MagicMock(return_value=(create_resource_objects("ManagementService"), 200, 0.282561)) 86 | Vrops.get_nsxt_transport_zone = MagicMock(return_value=(create_resource_objects("TransportZone"), 200, 0.282561)) 87 | Vrops.get_nsxt_transport_node = MagicMock(return_value=(create_resource_objects("TransportNode"), 200, 0.282561)) 88 | Vrops.get_nsxt_logical_switch = MagicMock(return_value=(create_resource_objects("LogicalSwitch"), 200, 0.282561)) 89 | Vrops.get_datacenter = MagicMock(return_value=(create_resource_objects("Datacenter"), 200, 0.282561)) 90 | Vrops.get_cluster = MagicMock(return_value=(create_resource_objects("ClusterComputeResource"), 200, 0.282561)) 91 | Vrops.get_SDRS_cluster = MagicMock(return_value=(create_resource_objects("StoragePod"), 200, 0.282561)) 92 | datastores = create_resource_objects("Datastore") 93 | for ds in datastores: 94 | ds.type = 'other' 95 | Vrops.get_datastores = MagicMock(return_value=(datastores, 200, 0.282561)) 96 | Vrops.get_hosts = MagicMock(return_value=(create_resource_objects("HostSystem"), 200, 0.282561)) 97 | Vrops.get_vms = MagicMock(return_value=(create_resource_objects("VirtualMachine"), 200, 0.282561)) 98 | Vrops.get_dis_virtual_switch = MagicMock(return_value=(create_resource_objects("VmwareDistributedSwitch"), 200, 0.282561)) 99 | Vrops.get_vcops_instances = MagicMock(return_value=(create_resource_objects("vcops_object"), 200, 0.282561)) 100 | Vrops.get_sddc_instances = MagicMock(return_value=(create_resource_objects("sddc_object"), 200, 0.282561)) 101 | 102 | Vrops.get_project_ids = MagicMock(return_value=[{"3628-93a1-56e84634050814": "0815"}, 103 | {"7422-91h7-52s842060815": "0815"}, 104 | {"5628-9ba1-55e847050815": "internal"}]) 105 | Vrops.get_alertdefinitions = MagicMock(return_value={'id': 'test-id', 'name': 'test-alert', 106 | 107 | 'symptoms': [{'name': 'test_symptom', 108 | 'state': 'test-state'}], 109 | 'recommendation': [{'id': 'test-re', 110 | 'description': 'test-description'}]}) 111 | Vrops.get_service_states = MagicMock(return_value=[ 112 | {'service': [{'details': 'Success, Service LOCATOR is running and responding', 113 | 'health': 'OK', 114 | 'name': 'LOCATOR', 115 | 'startedOn': 1702541189387, 116 | 'uptime': 6412774450}, 117 | {'details': 'Success, Service ANALYTICS is running and responding', 118 | 'health': 'OK', 119 | 'name': 'ANALYTICS', 120 | 'startedOn': 1702541205556, 121 | 'uptime': 6412762377}, 122 | {'details': 'Success, Service COLLECTOR is running and responding', 123 | 'health': 'OK', 124 | 'name': 'COLLECTOR', 125 | 'startedOn': 1702541203086, 126 | 'uptime': 6412760790}, 127 | {'details': 'Success, Service API is running and responding', 128 | 'health': 'OK', 129 | 'name': 'API', 130 | 'startedOn': 1702541204544, 131 | 'uptime': 6412759317}, 132 | {'details': 'Success, Service CASA is running and responding', 133 | 'health': 'OK', 134 | 'name': 'CASA', 135 | 'startedOn': 1702541071734, 136 | 'uptime': 6412892126}, 137 | {'details': 'Success, Service ADMINUI is running and responding', 138 | 'health': 'OK', 139 | 'name': 'ADMINUI', 140 | 'startedOn': 1702541195420, 141 | 'uptime': 6412768497}, 142 | {'details': 'Success, Service UI is running and responding', 143 | 'health': 'OK', 144 | 'name': 'UI', 145 | 'startedOn': 1702541195420, 146 | 'uptime': 6412768503}]}, 200, 0.282561]) 147 | 148 | thread = Thread(target=InventoryBuilder, args=(self.target, 8000, 180)) 149 | thread.daemon = True 150 | thread.start() 151 | 152 | for collector in self.metrics_yaml.keys(): 153 | print("\nTesting " + collector) 154 | class_module = importlib.import_module(f'collectors.{collector}') 155 | collector_instance = class_module.__getattribute__(collector)() 156 | 157 | if "Stats" in collector: 158 | multiple_metrics_generated = list() 159 | for metric in self.collector_config[collector]: 160 | multiple_metrics_generated.append( 161 | {"resourceId": "7422-91h7-52s842060815", "stat-list": {"stat": [ 162 | {"timestamps": [1582797716394], "statKey": {"key": metric['key']}, "data": [88.0]}]}}) 163 | multiple_metrics_generated.append( 164 | {"resourceId": "3628-93a1-56e84634050814", "stat-list": {"stat": [ 165 | {"timestamps": [1582797716394], "statKey": {"key": metric['key']}, "data": [44.0]}]}}) 166 | multiple_metrics_generated.append( 167 | {"resourceId": "5628-9ba1-55e847050815", "stat-list": {"stat": [ 168 | {"timestamps": [1582797716394], "statKey": {"key": metric['key']}, "data": [55.0]}]}}) 169 | Vrops.get_latest_stats_multiple = MagicMock(return_value=(multiple_metrics_generated, 200, 0.5)) 170 | 171 | if "Properties" in collector: 172 | multiple_metrics_generated = list() 173 | for metric in self.collector_config[collector]: 174 | multiple_metrics_generated.append( 175 | {"resourceId": "7422-91h7-52s842060815", "property-contents": { 176 | "property-content": [ 177 | {"timestamps": [1582797716394], "statKey": metric['key'], 178 | "data": [88.0]}]}}) 179 | multiple_metrics_generated.append( 180 | {"resourceId": "3628-93a1-56e84634050814", "property-contents": { 181 | "property-content": [ 182 | {"timestamps": [1582797716394], "statKey": metric['key'], 183 | "values": ["test"]}]}}) 184 | multiple_metrics_generated.append( 185 | {"resourceId": "5628-9ba1-55e847050815", "property-contents": { 186 | "property-content": [ 187 | {"timestamps": [1582797716394], "statKey": metric['key'], 188 | "values": ["test"]}]}}) 189 | Vrops.get_latest_properties_multiple = MagicMock(return_value=(multiple_metrics_generated, 200, 0.5)) 190 | 191 | thread_list = list() 192 | 193 | # start prometheus server to provide metrics later on 194 | 195 | thread1 = Thread(target=run_prometheus_server, args=(self.random_prometheus_port, [collector_instance], mock_signal)) 196 | thread1.daemon = True 197 | thread1.start() 198 | thread_list.append(thread1) 199 | # give grandpa thread some time to get prometheus started and run a couple intervals of InventoryBuilder 200 | time.sleep(3) 201 | 202 | print("prometheus query port " + str(self.random_prometheus_port)) 203 | c = http.client.HTTPConnection("localhost:" + str(self.random_prometheus_port)) 204 | c.request("GET", "/") 205 | r = c.getresponse() 206 | self.assertEqual(r.status, 200, "HTTP server return code should be 200") 207 | self.assertEqual(r.reason, "OK", "HTTP status should be OK") 208 | 209 | data = r.read().decode() 210 | data_array = data.split('\n') 211 | metrics = set() 212 | for entry in data_array: 213 | if entry.startswith('#'): 214 | continue 215 | if entry.startswith('python_gc'): 216 | continue 217 | if entry.startswith('process_'): 218 | continue 219 | if entry.startswith('python_info'): 220 | continue 221 | split_entry = entry.split("}") 222 | if len(split_entry) != 2: 223 | continue 224 | metrics.add(split_entry[0] + "}") 225 | 226 | metrics_yaml_list = self.metrics_yaml[collector] 227 | self.assertTrue(metrics_yaml_list, msg=collector + " has no metrics defined, FIX IT!") 228 | self.assertTrue(metrics, msg=collector + " is not producing any metrics at all, how should I continue?") 229 | 230 | # check if there are more metrics being produced and they are not listed in metrics.yaml?! 231 | issubsetdifference = metrics.difference(metrics_yaml_list) 232 | self.assertTrue(metrics.issubset(metrics_yaml_list), 233 | msg=collector + ": metric not covered by testcase, probably missing in yaml\n" + "\n".join( 234 | issubsetdifference)) 235 | # check if all metrics from yaml are here 236 | supersetdifference = set(metrics_yaml_list).difference(metrics) 237 | self.assertTrue(set(metrics).issuperset(metrics_yaml_list), 238 | msg=collector + ": missing metrics from yaml:\n" + "\n".join(supersetdifference)) 239 | 240 | for t in thread_list: 241 | t.join(timeout=5) 242 | 243 | # we don't want to have any port locks if prometheus server thread is not shutting down 244 | self.random_prometheus_port += 1 245 | REGISTRY.unregister(collector_instance) 246 | 247 | 248 | if __name__ == '__main__': 249 | unittest.main() 250 | -------------------------------------------------------------------------------- /tests/TestLaunchExporter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('.') 4 | from unittest import TestCase 5 | from exporter import parse_params 6 | from exporter import default_collectors 7 | import os 8 | import unittest 9 | import logging 10 | 11 | 12 | logger = logging.getLogger('test-logger') 13 | # Level Numeric value 14 | # CRITICAL 50 15 | # ERROR 40 16 | # WARNING 30 17 | # INFO 20 18 | # DEBUG 10 19 | # NOTSET 0 20 | 21 | 22 | class TestLaunchExporter(TestCase): 23 | print(f"Running TestLaunchExporter") 24 | 25 | # test with debug option on 26 | def test_with_cli_params_1(self): 27 | sys.argv = ['prog', '-o', '1234', '-i', 'inventory.some.url', '-m', 'tests/collector_config.yaml', '-t', 28 | 'vrops-vcenter-test.company.com', '-d'] 29 | parse_params(logger) 30 | self.assertEqual(os.getenv('PORT'), '1234', 'The port was not set correctly!') 31 | self.assertEqual(os.getenv('INVENTORY'), 'inventory.some.url', 'Inventory was not set correctly') 32 | self.assertEqual(os.getenv('COLLECTOR_CONFIG'), 'tests/collector_config.yaml', 'Config was not set') 33 | self.assertEqual(os.getenv('TARGET'), 'vrops-vcenter-test.company.com', 'Target was not set') 34 | self.assertEqual(logger.level, 10) 35 | 36 | # test with debug option off 37 | def test_with_cli_params_2(self): 38 | os.environ.clear() 39 | sys.argv = ['prog', '--port', '1234', '-i', 'inventory.some.url', '-m', 'tests/collector_config.yaml', '-t', 40 | 'vrops-vcenter-test.company.com'] 41 | parse_params(logger) 42 | self.assertEqual(os.getenv('PORT'), '1234', 'The port was not set correctly!') 43 | self.assertEqual(os.getenv('INVENTORY'), 'inventory.some.url', 'Inventory was not set correctly') 44 | self.assertEqual(os.getenv('COLLECTOR_CONFIG'), 'tests/collector_config.yaml', 'Config was not set') 45 | self.assertEqual(os.getenv('TARGET'), 'vrops-vcenter-test.company.com', 'Target was not set') 46 | self.assertEqual(logger.level, 30) 47 | 48 | def test_with_cli_and_env_params(self): 49 | sys.argv = ['prog', '--port', '1234', '-i', 'inventory.some.url', '-m', 50 | 'tests/collector_config.yaml', '-t', 'vrops-vcenter-test.company.com', '-v'] 51 | os.environ['PORT'] = '1123' 52 | os.environ['INVENTORY'] = 'inventory.wedontwantthis.url' 53 | os.environ['COLLECTOR_CONFIG'] = 'tests/other_config.yaml' 54 | os.environ['TARGET'] = 'Othervrops-vcenter-test.company.com' 55 | parse_params(logger) 56 | # cli params preferred 57 | self.assertEqual(os.getenv('PORT'), '1234', 'The port was not set correctly!') 58 | self.assertEqual(os.getenv('INVENTORY'), 'inventory.some.url', 'Inventory was not set correctly') 59 | self.assertEqual(os.getenv('COLLECTOR_CONFIG'), 'tests/collector_config.yaml', 'Config was not set') 60 | self.assertEqual(os.getenv('TARGET'), 'vrops-vcenter-test.company.com', 'Target was not set') 61 | self.assertEqual(logger.level, 20) 62 | 63 | # test use default collectors when nothing is specified 64 | def test_with_no_collector(self): 65 | sys.argv = ['prog', '--port', '1234', '-i', 'inventory.some.url', '-m', 66 | 'tests/collector_config.yaml', '-t', 'vrops-vcenter-test.company.com'] 67 | options = parse_params(logger) 68 | self.assertEqual(options.collectors, default_collectors(), 'Default collector list does not match the default') 69 | 70 | # test with only one collector enabled 71 | def test_with_one_collector(self): 72 | sys.argv = ['prog', '--port', '1234', '-i', 'inventory.some.url', '-c', 'VMStatsCollector', '-m', 73 | 'tests/collector_config.yaml', '-t', 'vrops-vcenter-test.company.com'] 74 | options = parse_params(logger) 75 | self.assertEqual(options.collectors, ['VMStatsCollector'], 76 | 'Collector list does not match given single collector') 77 | 78 | # test multiple enabled collectors 79 | def test_with_multiple_collector(self): 80 | sys.argv = ['prog', '--port', '1234', '-i', 'inventory.some.url', '-c', 'VMStatsCollector', '-m', 81 | 'tests/collector_config.yaml', '-c', 'VMPropertiesCollector', '-t', 'vrops-vcenter-test.company.com'] 82 | options = parse_params(logger) 83 | self.assertEqual(options.collectors, ['VMStatsCollector', 'VMPropertiesCollector'], 84 | 'Collector list does not match given multiple collectors') 85 | 86 | def test_with_bogus_options(self): 87 | os.environ.clear() 88 | sys.argv = ['prog', '-z', 'foo', '-a', 'bar', '-w', 'bar'] 89 | with self.assertRaises(SystemExit) as se: 90 | parse_params(logger) 91 | self.assertEqual(se.exception.code, 2, 'PORT or INVENTORY are not set properly in ENV or command line!') 92 | 93 | def test_without_params(self): 94 | os.environ.clear() 95 | sys.argv = ['prog'] 96 | with self.assertRaises(KeyError) as e: 97 | parse_params(logger) 98 | self.assertEqual(str(e.exception.args), "('COLLECTOR_CONFIG',)", 'no collector config file provided!') 99 | 100 | 101 | if __name__ == '__main__': 102 | unittest.main() 103 | -------------------------------------------------------------------------------- /tests/TestLaunchInventory.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('.') 4 | from unittest import TestCase 5 | from inventory_launch import parse_params 6 | import os 7 | import unittest 8 | import logging 9 | 10 | logger = logging.getLogger('test-logger') 11 | # Level Numeric value 12 | # CRITICAL 50 13 | # ERROR 40 14 | # WARNING 30 15 | # INFO 20 16 | # DEBUG 10 17 | # NOTSET 0 18 | 19 | 20 | class TestLaunchInventory(TestCase): 21 | 22 | print(f"Running TestLaunchInventory") 23 | os.environ.setdefault('TARGET', "vrops-vcenter-test.company.com") 24 | 25 | # test with debug option on 26 | def test_with_cli_params_1(self): 27 | sys.argv = ['prog', '-u', 'testuser', '-p', 'testpw31!', '-o', '1234', 28 | '-t', 'vrops-vcenter-test.company.com', '-m', 'tests/inventory_config.yaml', '-l', '-s', '180', '-d'] 29 | parse_params(logger) 30 | self.assertEqual(os.getenv('USER'), 'testuser', 'The user was not set correctly!') 31 | self.assertEqual(os.getenv('PASSWORD'), 'testpw31!', 'The password was not set correctly!') 32 | self.assertEqual(os.getenv('PORT'), '1234', 'The port was not set correctly!') 33 | self.assertEqual(os.getenv('TARGET'), 'vrops-vcenter-test.company.com', 'Target was not set') 34 | self.assertEqual(os.getenv('INVENTORY_CONFIG'), 'tests/inventory_config.yaml', 'Config was not set') 35 | self.assertEqual(os.getenv('LOOPBACK'), '1', 'Loopback was not set correctly') 36 | self.assertEqual(os.getenv('SLEEP'), '180', 'Sleep time was not set correctly') 37 | self.assertEqual(logger.level, 10) 38 | 39 | # test with debug option off 40 | def test_with_cli_params_2(self): 41 | os.environ.clear() 42 | sys.argv = ['prog', '--user', 'testuser', '--password', 'testpw31!', '--port', '1234', 43 | '-t', 'vrops-vcenter-test.company.com', '-m', 'tests/inventory_config.yaml', '-l', '--sleep', '180', '--v'] 44 | parse_params(logger) 45 | self.assertEqual(os.getenv('USER'), 'testuser', 'The user was not set correctly!') 46 | self.assertEqual(os.getenv('PASSWORD'), 'testpw31!', 'The password was not set correctly!') 47 | self.assertEqual(os.getenv('PORT'), '1234', 'The port was not set correctly!') 48 | self.assertEqual(os.getenv('TARGET'), 'vrops-vcenter-test.company.com', 'Target was not set') 49 | self.assertEqual(os.getenv('INVENTORY_CONFIG'), 'tests/inventory_config.yaml', 'Config was not set') 50 | self.assertEqual(os.getenv('LOOPBACK'), '1', 'Loopback was not set correctly') 51 | self.assertEqual(os.getenv('SLEEP'), '180', 'Sleep time was not set correctly') 52 | self.assertEqual(logger.level, 20) 53 | 54 | def test_with_cli_and_env_params(self): 55 | sys.argv = ['prog', '--user', 'cli_testuser', '--password', 'testpw31!', 56 | '--port', '1234', '-t', 'vrops-vcenter-test.company.com', '-m', 'tests/inventory_config.yaml', '-l', 57 | '-s', '180'] 58 | os.environ['USER'] = 'env_testuser' 59 | os.environ['PASSWORD'] = 'testps31!_2' 60 | os.environ['PORT'] = '1123' 61 | os.environ['TARGET'] = '/wrong/path/to/atlas.yaml' 62 | os.environ['INVENTORY_CONFIG'] = 'wrong/path/to/config.yaml' 63 | os.environ['LOOPBACK'] = '0' 64 | os.environ['SLEEP'] = '199' 65 | 66 | parse_params(logger) 67 | # cli params preferred 68 | self.assertEqual(os.getenv('USER'), 'cli_testuser', 'The user was not set correctly!') 69 | self.assertEqual(os.getenv('PASSWORD'), 'testpw31!', 'The password was not set correctly!') 70 | self.assertEqual(os.getenv('PORT'), '1234', 'The port was not set correctly!') 71 | self.assertEqual(os.getenv('TARGET'), 'vrops-vcenter-test.company.com', 'Target was not set') 72 | self.assertEqual(os.getenv('INVENTORY_CONFIG'), 'tests/inventory_config.yaml', 'Config was not set') 73 | self.assertEqual(os.getenv('LOOPBACK'), '1', 'Loopback was not set correctly') 74 | self.assertEqual(os.getenv('SLEEP'), '180', 'Sleep time was not set correctly') 75 | self.assertEqual(logger.level, 30) 76 | 77 | def test_env_params(self): 78 | os.environ.clear() 79 | os.environ['USER'] = 'testuser' 80 | os.environ['PASSWORD'] = 'testpw31!' 81 | os.environ['PORT'] = '1234' 82 | os.environ['TARGET'] = 'vrops-vcenter-test.company.com' 83 | os.environ['INVENTORY_CONFIG'] = 'tests/inventory_config.yaml' 84 | os.environ['LOOPBACK'] = '0' 85 | os.environ['SLEEP'] = '180' 86 | 87 | parse_params(logger) 88 | self.assertEqual(os.getenv('USER'), 'testuser', 'The user was not set correctly!') 89 | self.assertEqual(os.getenv('PASSWORD'), 'testpw31!', 'The password was not set correctly!') 90 | self.assertEqual(os.getenv('PORT'), '1234', 'The port was not set correctly!') 91 | self.assertEqual(os.getenv('TARGET'), 'vrops-vcenter-test.company.com', 'Target was not set') 92 | self.assertEqual(os.getenv('INVENTORY_CONFIG'), 'tests/inventory_config.yaml', 'Config was not set') 93 | self.assertEqual(os.getenv('LOOPBACK'), '0', 'Loopback was not set correctly') 94 | self.assertEqual(os.getenv('SLEEP'), '180', 'Sleep time was not set correctly') 95 | 96 | def test_with_bogus_options(self): 97 | os.environ.clear() 98 | sys.argv = ['prog', '-z', 'foo', '-x', 'bar', '-w', 'bar'] 99 | with self.assertRaises(SystemExit) as se: 100 | parse_params(logger) 101 | self.assertEqual(se.exception.code, 2, 'PORT, USER, TARGET or PASSWORD are not set properly in ENV or command ' 102 | 'line!') 103 | 104 | def test_without_params(self): 105 | os.environ.clear() 106 | sys.argv = ['prog'] 107 | with self.assertRaises(SystemExit) as se: 108 | parse_params(logger) 109 | self.assertEqual(se.exception.code, 1, 'PORT, USER, TARGET or PASSWORD are not set in ENV or command line!') 110 | 111 | 112 | if __name__ == '__main__': 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/tests/__init__.py -------------------------------------------------------------------------------- /tests/collector_config.yaml: -------------------------------------------------------------------------------- 1 | default_collectors: 2 | - 'NSXTMgmtClusterStatsCollector' 3 | - 'NSXTMgmtClusterPropertiesCollector' 4 | - 'NSXTLogicalSwitchPropertiesCollector' 5 | - 'NSXTMgmtNodeStatsCollector' 6 | - 'NSXTMgmtNodePropertiesCollector' 7 | - 'NSXTTransportNodePropertiesCollector' 8 | - 'ClusterStatsCollector' 9 | - 'ClusterPropertiesCollector' 10 | - 'DistributedvSwitchPropertiesCollector' 11 | - 'DatastoreStatsCollector' 12 | - 'SDRSStatsCollector' 13 | - 'SDRSPropertiesCollector' 14 | - 'DatastorePropertiesCollector' 15 | - 'HostSystemStatsCollector' 16 | - 'HostSystemPropertiesCollector' 17 | - 'VCenterStatsCollector' 18 | - 'VCenterPropertiesCollector' 19 | - 'VMPropertiesCollector' 20 | - 'VMStatsCPUCollector' 21 | - 'VMStatsNetworkCollector' 22 | - 'VMStatsVirtualDiskCollector' 23 | - 'VMStatsMemoryCollector' 24 | - 'VMStatsDefaultCollector' 25 | - 'VcopsSelfMonitoringPropertiesCollector' 26 | - 'VcopsSelfMonitoringStatsCollector' 27 | 28 | # Alerts 29 | - 'NSXTAdapterAlertCollector' 30 | - 'NSXTMgmtClusterAlertCollector' 31 | - 'NSXTMgmtServiceAlertCollector' 32 | - 'NSXTMgmtNodeAlertCollector' 33 | - 'NSXTTransportNodeAlertCollector' 34 | - 'NSXTLogicalSwitchAlertCollector' 35 | - 'ClusterAlertCollector' 36 | - 'DatastoreAlertCollector' 37 | - 'HostSystemAlertCollector' 38 | - 'VCenterAlertCollector' 39 | - 'VMAlertCollector' 40 | - 'VcopsSelfMonitoringAlertCollector' 41 | - 'SDDCAlertCollector' 42 | 43 | alerts: 44 | alertCriticality: 45 | - 'CRITICAL' 46 | - 'WARNING' 47 | - 'IMMEDIATE' 48 | activeOnly: True 49 | 50 | CustomInfoMetricsGenerator: 51 | - metric: 'vrops_virtualmachine_guest_tools_target_version' 52 | values_dict: 53 | # A dict with label: label_values 54 | guest_tools_target_version: '10.2.0' 55 | 56 | ClusterPropertiesCollector: 57 | - metric_suffix: "configuration_dasConfig_admissionControlEnabled" 58 | expected: "true" 59 | key: "configuration|dasConfig|admissionControlEnabled" 60 | - metric_suffix: "configuration_dasConfig_enabled" 61 | expected: "true" 62 | key: "configuration|dasConfig|enabled" 63 | - metric_suffix: "configuration_drsconfig_enabled" 64 | expected: "true" 65 | key: "configuration|drsConfig|enabled" 66 | - metric_suffix: "configuration_drsconfig_defaultVmBehavior" 67 | expected: "fullyAutomated" 68 | key: "configuration|drsConfig|defaultVmBehavior" 69 | - metric_suffix: "configuration_dasConfig_admissionControlPolicyId" 70 | key: "configuration|dasConfig|admissionControlPolicyId" 71 | - metric_suffix: "custom_attributes_info" 72 | key: "summary|customTag:INFO|customTagValue" 73 | 74 | ClusterStatsCollector: 75 | - metric_suffix: "cluster_running_hosts" 76 | key: "summary|number_running_hosts" 77 | - metric_suffix: "cpu_capacity_usage_percentage" 78 | key: "cpu|capacity_usagepct_average" 79 | - metric_suffix: "cpu_usage_mhz" 80 | key: "cpu|usagemhz_average" 81 | - metric_suffix: "cpu_capacity_mhz" 82 | key: "cpu|haTotalCapacity_average" 83 | - metric_suffix: "memory_usage_percentage" 84 | key: "mem|host_usagePct" 85 | - metric_suffix: "memory_usage_kilobytes" 86 | key: "mem|host_usage" 87 | - metric_suffix: "memory_capacity_kilobytes" 88 | key: "mem|totalCapacity_average" 89 | - metric_suffix: "summary_total_number_vms" 90 | key: "summary|total_number_vms" 91 | 92 | DatastoreStatsCollector: 93 | - metric_suffix: "diskspace_total_usage_gigabytes" 94 | key: "diskspace|disktotal" 95 | - metric_suffix: "diskspace_freespace_gigabytes" 96 | key: "diskspace|freespace" 97 | - metric_suffix: "diskspace_capacity_gigabytes" 98 | key: "diskspace|capacity" 99 | - metric_suffix: "summary_total_number_vms" 100 | key: "summary|total_number_vms" 101 | 102 | SDRSStatsCollector: 103 | - metric_suffix: "capacity_remaining_percentage" 104 | key: "OnlineCapacityAnalytics|capacityRemainingPercentage" 105 | 106 | SDRSPropertiesCollector: 107 | - metric_suffix: "config_sdrsconfig_vmStorageAntiAffinityRules" 108 | key: "config|sdrsconfig|vmStorageAntiAffinityRules" 109 | 110 | DatastorePropertiesCollector: 111 | - metric_suffix: "summary_datastore_accessible" 112 | expected: "PoweredOn" 113 | key: "summary|accessible" 114 | 115 | HostSystemStatsCollector: 116 | - metric_suffix: "configuration_dasConfig_admissionControlPolicy_failoverHost" 117 | key: "configuration|dasConfig|admissionControlPolicy|failoverHost" 118 | - metric_suffix: "cpu_sockets_number" 119 | key: "cpu|numpackages" 120 | - metric_suffix: "hardware_number_of_cpu_cores_info" 121 | key: "hardware|cpuInfo|num_CpuCores" 122 | - metric_suffix: "cpu_usage_megahertz" 123 | key: "cpu|usagemhz_average" 124 | - metric_suffix: "cpu_demand_percentage" 125 | key: "cpu|demandPct" 126 | - metric_suffix: "cpu_demand_megahertz" 127 | key: "cpu|demandmhz" 128 | - metric_suffix: "cpu_usage_average_percentage" 129 | key: "cpu|usage_average" 130 | - metric_suffix: "cpu_co_stop_miliseconds" 131 | key: "cpu|costop_summation" 132 | - metric_suffix: "cpu_contention_percentage" 133 | key: "cpu|capacity_contentionPct" 134 | - metric_suffix: "cpu_io_wait_miliseconds" 135 | key: "cpu|iowait" 136 | - metric_suffix: "memory_host_usage_kilobytes" 137 | key: "mem|host_usage" 138 | - metric_suffix: "memory_useable_kilobytes" 139 | key: "mem|host_usable" 140 | - metric_suffix: "memory_usage_percentage" 141 | key: "mem|usage_average" 142 | - metric_suffix: "memory_utilization" 143 | key: "mem|total_need" 144 | - metric_suffix: "memory_contention_percentage" 145 | key: "mem|host_contentionPct" 146 | - metric_suffix: "memory_ballooning_kilobytes" 147 | key: "mem|vmmemctl_average" 148 | - metric_suffix: "memory_compressed_kilobytes" 149 | key: "mem|compressed_average" 150 | - metric_suffix: "memory_activly_used_by_vms_kilobytes" 151 | key: "mem|active_average" 152 | - metric_suffix: "memory_consumed_by_vms_kilobytes" 153 | key: "mem|consumed_average" 154 | - metric_suffix: "memory_capacity_available_to_vms_kilobytes" 155 | key: "mem|host_provisioned" 156 | - metric_suffix: "summary_number_VMs_total" 157 | key: "summary|total_number_vms" 158 | - metric_suffix: "summary_running_VMs_number" 159 | key: "summary|number_running_vms" 160 | - metric_suffix: "network_packets_dropped_rx_number" 161 | key: "net|droppedRx_summation" 162 | - metric_suffix: "network_packets_dropped_tx_number" 163 | key: "net|droppedTx_summation" 164 | - metric_suffix: "network_packets_dropped_percentage" 165 | key: "net|droppedPct" 166 | - metric_suffix: "system_uptime_seconds" 167 | key: "sys|uptime_latest" 168 | - metric_suffix: "memory_swap_in_rate_kbps" 169 | key: "mem|swapinRate_average" 170 | - metric_suffix: "memory_swap_out_rate_kbps" 171 | key: "mem|swapoutRate_average" 172 | - metric_suffix: "memory_swap_used_rate_kbps" 173 | key: "mem|swapused_average" 174 | - metric_suffix: "cpu_ready_miliseconds" 175 | key: "cpu|ready_summation" 176 | - metric_suffix: "cpu_swap_wait_miliseconds" 177 | key: "cpu|swapwait_summation" 178 | - metric_suffix: "summary_number_running_vcpus_total" 179 | key: "summary|number_running_vcpus" 180 | - metric_suffix: "summary_number_vmotion_total" 181 | key: "summary|number_vmotion" 182 | 183 | 184 | HostSystemPropertiesCollector: 185 | - metric_suffix: "runtime_powerState" 186 | key: "runtime|powerState" 187 | expected: "Powered On" 188 | - metric_suffix: "runtime_connectionState" 189 | key: "runtime|connectionState" 190 | expected: "connected" 191 | - metric_suffix: "runtime_maintenanceState" 192 | key: "runtime|maintenanceState" 193 | expected: "notInMaintenance" 194 | - metric_suffix: "summary_version" 195 | key: "summary|version" 196 | - metric_suffix: "sys_build" 197 | key: "sys|build" 198 | - metric_suffix: "custom_attributes_hw" 199 | key: "summary|customTag:HW|customTagValue" 200 | - metric_suffix: "config_diskSpace_bytes" 201 | key: "config|diskSpace" 202 | 203 | VCenterStatsCollector: 204 | - metric_suffix: "cpu_used_percent" 205 | key: "cpu|capacity_usagepct_average" 206 | - metric_suffix: "memory_used_percent" 207 | key: "mem|host_usagePct" 208 | - metric_suffix: "diskspace_total_gigabytes" 209 | key: "diskspace|total_capacity" 210 | - metric_suffix: "diskspace_usage_gigabytes" 211 | key: "diskspace|total_usage" 212 | - metric_suffix: "vcsa_certificate_remaining_days" 213 | key: "summary|CPO vCSA Certificate Remaining Days" 214 | 215 | VCenterPropertiesCollector: 216 | - metric_suffix: "summary_version" 217 | key: "summary|version" 218 | - metric_suffix: "vc_fullname" 219 | key: "summary|vcfullname" 220 | 221 | VMPropertiesCollector: 222 | - metric_suffix: "runtime_powerState" 223 | expected: "Powered On" 224 | key: "summary|runtime|powerState" 225 | - metric_suffix: "runtime_connectionState" 226 | expected: "connected" 227 | key: "summary|runtime|connectionState" 228 | - metric_suffix: "virtualdisk_scsi0_0_datastore" 229 | key: "virtualDisk:scsi0:0|datastore" 230 | - metric_suffix: "virtualdisk_scsi0_1_datastore" 231 | key: "virtualDisk:scsi0:1|datastore" 232 | - metric_suffix: "guest_os_full_name" 233 | key: "config|guestFullName" 234 | - metric_suffix: "guest_tools_version" 235 | key: "summary|guest|toolsVersion" 236 | - metric_suffix: "summary_ethernetCards" 237 | key: "summary|config|numEthernetCards" 238 | - metric_suffix: "config_hardware_memory_kilobytes" 239 | key: "config|hardware|memoryKB" 240 | 241 | VMStatsMemoryCollector: 242 | - metric_suffix: "memory_usage_average" 243 | key: "mem|usage_average" 244 | - metric_suffix: "memory_kilobytes" 245 | key: "mem|guest_provisioned" 246 | - metric_suffix: "memory_consumed_kilobytes" 247 | key: "mem|consumed_average" 248 | - metric_suffix: "memory_activewrite_kilobytes" 249 | key: "mem|activewrite_average" 250 | - metric_suffix: "memory_active_ratio" 251 | key: "mem|guest_activePct" 252 | - metric_suffix: "memory_ballooning_ratio" 253 | key: "mem|balloonPct" 254 | - metric_suffix: "memory_contention_ratio" 255 | key: "mem|host_contentionPct" 256 | - metric_suffix: "swapped_memory_kilobytes" 257 | key: "mem|swapped_average" 258 | - metric_suffix: "swapin_memory_kilobytes" 259 | key: "mem|swapinRate_average" 260 | 261 | VMStatsCPUCollector: 262 | - metric_suffix: "number_vcpus_total" 263 | key: "config|hardware|num_Cpu" 264 | - metric_suffix: "cpu_demand_ratio" 265 | key: "cpu|demandPct" 266 | - metric_suffix: "cpu_usage_ratio" 267 | key: "cpu|usage_average" 268 | - metric_suffix: "cpu_usage_average_mhz" 269 | key: "cpu|usagemhz_average" 270 | - metric_suffix: "cpu_contention_ratio" 271 | key: "cpu|capacity_contentionPct" 272 | - metric_suffix: "cpu_ready_ratio" 273 | key: "cpu|readyPct" 274 | - metric_suffix: "cpu_latency_average" 275 | key: "cpu|latency_average" 276 | - metric_suffix: "cpu_wait_summation_miliseconds" 277 | key: "cpu|wait_summation" 278 | - metric_suffix: "cpu_io_wait_percentage" 279 | key: "cpu|iowaitPct" 280 | 281 | VMStatsNetworkCollector: 282 | - metric_suffix: "network_packets_dropped_rx_number" 283 | key: "net|droppedRx_summation" 284 | - metric_suffix: "network_packets_dropped_tx_number" 285 | key: "net|droppedTx_summation" 286 | - metric_suffix: "network_packets_rx_number" 287 | key: "net|packetsRx_summation" 288 | - metric_suffix: "network_packets_tx_number" 289 | key: "net|packetsTx_summation" 290 | - metric_suffix: "network_usage_average_kilobytes_per_second" 291 | key: "net|usage_average" 292 | - metric_suffix: "network_data_received_kilobytes_per_second" 293 | key: "net|bytesRx_average" 294 | - metric_suffix: "network_data_transmitted_kilobytes_per_second" 295 | key: "net|bytesTx_average" 296 | 297 | VMStatsVirtualDiskCollector: 298 | - metric_suffix: "virtual_disk_outstanding_io" 299 | key: "virtualDisk|vDiskOIO" 300 | - metric_suffix: "virtual_disk_read_kilobytes_per_second" 301 | key: "virtualDisk|read_average" 302 | - metric_suffix: "virtual_disk_write_kilobytes_per_second" 303 | key: "virtualDisk|write_average" 304 | - metric_suffix: "virtual_disk_outstanding_read_number" 305 | key: "virtualDisk|readOIO_latest" 306 | - metric_suffix: "virtual_disk_outstanding_write_number" 307 | key: "virtualDisk|writeOIO_latest" 308 | - metric_suffix: "virtual_disk_average_read_miliseconds" 309 | key: "virtualDisk|totalReadLatency_average" 310 | - metric_suffix: "virtual_disk_average_write_miliseconds" 311 | key: "virtualDisk|totalWriteLatency_average" 312 | 313 | VMStatsDefaultCollector: 314 | - metric_suffix: "disk_usage_average_kilobytes_per_second" 315 | key: "disk|usage_average" 316 | - metric_suffix: "diskspace_virtual_machine_used_gigabytes" 317 | key: "diskspace|perDsUsed" 318 | - metric_suffix: "diskspace_gigabytes" 319 | key: "config|hardware|disk_Space" 320 | - metric_suffix: "datastore_total" 321 | key: "summary|number_datastore" 322 | - metric_suffix: "datastore_outstanding_io_requests" 323 | key: "datastore|demand_oio" 324 | - metric_suffix: "guestfilesystem_storage_db_usage" 325 | key: "guestfilesystem:/storage/db|usage" 326 | - metric_suffix: "guestfilesystem_storage_db_capacity" 327 | key: "guestfilesystem:/storage/db|capacity" 328 | - metric_suffix: "guestfilesystem_storage_db_percentage" 329 | key: "guestfilesystem:/storage/db|percentage" 330 | - metric_suffix: "guestfilesystem_storage_autodeploy_usage" 331 | key: "guestfilesystem:/storage/autodeploy|usage" 332 | - metric_suffix: "guestfilesystem_storage_autodeploy_capacity" 333 | key: "guestfilesystem:/storage/autodeploy|capacity" 334 | - metric_suffix: "guestfilesystem_storage_autodeploy_percentage" 335 | key: "guestfilesystem:/storage/autodeploy|percentage" 336 | - metric_suffix: "guestfilesystem_storage_core_usage" 337 | key: "guestfilesystem:/storage/core|usage" 338 | - metric_suffix: "guestfilesystem_storage_core_capacity" 339 | key: "guestfilesystem:/storage/core|capacity" 340 | - metric_suffix: "guestfilesystem_storage_core_percentage" 341 | key: "guestfilesystem:/storage/core|percentage" 342 | - metric_suffix: "guestfilesystem_storage_dblog_usage" 343 | key: "guestfilesystem:/storage/dblog|usage" 344 | - metric_suffix: "guestfilesystem_storage_dblog_capacity" 345 | key: "guestfilesystem:/storage/dblog|capacity" 346 | - metric_suffix: "guestfilesystem_storage_dblog_percentage" 347 | key: "guestfilesystem:/storage/dblog|percentage" 348 | - metric_suffix: "guestfilesystem_storage_imagebuilder_usage" 349 | key: "guestfilesystem:/storage/imagebuilder|usage" 350 | - metric_suffix: "guestfilesystem_storage_imagebuilder_capacity" 351 | key: "guestfilesystem:/storage/imagebuilder|capacity" 352 | - metric_suffix: "guestfilesystem_storage_imagebuilder_percentage" 353 | key: "guestfilesystem:/storage/imagebuilder|percentage" 354 | - metric_suffix: "guestfilesystem_storage_netdump_usage" 355 | key: "guestfilesystem:/storage/netdump|usage" 356 | - metric_suffix: "guestfilesystem_storage_netdump_capacity" 357 | key: "guestfilesystem:/storage/netdump|capacity" 358 | - metric_suffix: "guestfilesystem_storage_netdump_percentage" 359 | key: "guestfilesystem:/storage/netdump|percentage" 360 | - metric_suffix: "guestfilesystem_storage_seat_usage" 361 | key: "guestfilesystem:/storage/seat|usage" 362 | - metric_suffix: "guestfilesystem_storage_seat_capacity" 363 | key: "guestfilesystem:/storage/seat|capacity" 364 | - metric_suffix: "guestfilesystem_storage_seat_percentage" 365 | key: "guestfilesystem:/storage/seat|percentage" 366 | - metric_suffix: "guestfilesystem_storage_updatemgr_usage" 367 | key: "guestfilesystem:/storage/updatemgr|usage" 368 | - metric_suffix: "guestfilesystem_storage_updatemgr_capacity" 369 | key: "guestfilesystem:/storage/updatemgr|capacity" 370 | - metric_suffix: "guestfilesystem_storage_updatemgr_percentage" 371 | key: "guestfilesystem:/storage/updatemgr|percentage" 372 | - metric_suffix: "guestfilesystem_boot_usage" 373 | key: "guestfilesystem:/boot|usage" 374 | - metric_suffix: "guestfilesystem_boot_capacity" 375 | key: "guestfilesystem:/boot|capacity" 376 | - metric_suffix: "guestfilesystem_boot_percentage" 377 | key: "guestfilesystem:/boot|percentage" 378 | - metric_suffix: "guestfilesystem_usage" 379 | key: "guestfilesystem:/|usage" 380 | - metric_suffix: "guestfilesystem_capacity" 381 | key: "guestfilesystem:/|capacity" 382 | - metric_suffix: "guestfilesystem_percentage" 383 | key: "guestfilesystem:/|percentage" 384 | 385 | DistributedvSwitchPropertiesCollector: 386 | - metric_suffix: "summary_version" 387 | key: "summary|version" 388 | 389 | NSXTMgmtClusterStatsCollector: 390 | - metric_suffix: "sys_capacity_distributed_firewall_rules_usage_count" 391 | key: "System Capacity|Distributed Firewall Rules|UsageCount" 392 | - metric_suffix: "sys_capacity_distributed_firewall_rules_usage_count_percentage" 393 | key: "System Capacity|Distributed Firewall Rules|UsageCountPercentage" 394 | - metric_suffix: "sys_capacity_distributed_firewall_rules_max_supported_count" 395 | key: "System Capacity|Distributed Firewall Rules|MaxSupportedCount" 396 | - metric_suffix: "sys_capacity_distributed_firewall_section_max_supported_count" 397 | key: "System Capacity|Distributed Firewall Sections|MaxSupportedCount" 398 | - metric_suffix: "sys_capacity_distributed_firewall_section_usage_count" 399 | key: "System Capacity|Distributed Firewall Sections|UsageCount" 400 | - metric_suffix: "sys_capacity_logical_switches_max_supported_count" 401 | key: "System Capacity|Logical Switches|MaxSupportedCount" 402 | - metric_suffix: "sys_capacity_logical_switches_usage_count" 403 | key: "System Capacity|Logical Switches|UsageCount" 404 | - metric_suffix: "sys_capacity_system_wide_logical_switch_max_supported_count" 405 | key: "System Capacity|System-wide Logical Switch Ports|MaxSupportedCount" 406 | - metric_suffix: "sys_capacity_system_wide_logical_switch_usage_count" 407 | key: "System Capacity|System-wide Logical Switch Ports|UsageCount" 408 | - metric_suffix: "sys_capacity_groups_max_supported_count" 409 | key: "System Capacity|Groups|MaxSupportedCount" 410 | - metric_suffix: "sys_capacity_groups_max_usage_count" 411 | key: "System Capacity|Groups|UsageCount" 412 | - metric_suffix: "sys_capacity_ip_sets_max_supported_count" 413 | key: "System Capacity|IP Sets|MaxSupportedCount" 414 | - metric_suffix: "sys_capacity_ip_sets_usage_count" 415 | key: "System Capacity|IP Sets|UsageCount" 416 | - metric_suffix: "sys_capacity_groups_based_in_ip_max_supported_count" 417 | key: "System Capacity|Groups Based on IP Sets|MaxSupportedCount" 418 | - metric_suffix: "sys_capacity_groups_based_in_ip_usage_count" 419 | key: "System Capacity|Groups Based on IP Sets|UsageCount" 420 | 421 | NSXTMgmtClusterPropertiesCollector: 422 | - metric_suffix: "product_version" 423 | key: "NSXTProductVersion" 424 | - metric_suffix: "management_cluster_connectivity_status" 425 | expected: "STABLE" 426 | key: "ConnectivityStatus|ClusterStatus|ManagementClusterStatusProperty" 427 | - metric_suffix: "controller_cluster_connectivity_status" 428 | expected: "STABLE" 429 | key: "ConnectivityStatus|ClusterStatus|ControllerClusterStatusProperty" 430 | 431 | NSXTMgmtNodeStatsCollector: 432 | - metric_suffix: "memory_used" 433 | key: "Memory|Used" 434 | - metric_suffix: "memory_total" 435 | key: "Memory|Total" 436 | 437 | NSXTMgmtNodePropertiesCollector: 438 | - metric_suffix: "version" 439 | key: "NSXTManagerNodeVersion" 440 | - metric_suffix: "connectivity_status" 441 | key: "ConnectivityStatus|ManagerConnectivityProperty" 442 | 443 | NSXTTransportNodePropertiesCollector: 444 | - metric_suffix: "connectivity_status" 445 | key: "ConnectivityStatus|TransportNodeState" 446 | 447 | NSXTLogicalSwitchPropertiesCollector: 448 | - metric_suffix: "state" 449 | expected: "SUCCESS" 450 | key: "summary|LogicalSwitchStateProperty" 451 | 452 | VcopsSelfMonitoringStatsCollector: 453 | - metric_suffix: "primary_objects_count" 454 | key: "PrimaryResourcesCount" 455 | - metric_suffix: "primary_metrics_count" 456 | key: "PrimaryMetricsCount" 457 | 458 | VcopsSelfMonitoringPropertiesCollector: 459 | - metric_suffix: "build_number" 460 | key: "build_number" 461 | - metric_suffix: "cluster_state" 462 | expected: "ONLINE" 463 | key: "ClusterState" 464 | -------------------------------------------------------------------------------- /tests/inventory_config.yaml: -------------------------------------------------------------------------------- 1 | query_specs: 2 | # to create specific resource query specs for a resourcekind, stick to one of the following keys: 3 | # vCenter Adapter: [Datacenter, ClusterComputeResource, Datastore, HostSystem, VmwareDistributedVirtualSwitch] 4 | # NSX-T: [ManagementCluster, ManagementNode, ManagementService, TransportZone, TransportNode, LogicalSwitch] 5 | VirtualMachine: 6 | resourceHealth: 7 | - "GREEN" 8 | - "YELLOW" 9 | - "ORANGE" 10 | - "RED" 11 | - "GREY" 12 | resourceStatus: 13 | # resource data collection stats 14 | # - "ERROR" 15 | - "UNKNOWN" 16 | # - "DOWN" 17 | - "DATA_RECEIVING" 18 | # - "OLD_DATA_RECEIVING" 19 | # - "NO_DATA_RECEIVING" 20 | # - "NO_PARENT_MONITORING" 21 | # - "COLLECTOR_DOWN" 22 | resourceStates: 23 | # resource states 24 | # - "STOPPED" 25 | # - "STARTING" 26 | - "STARTED" 27 | # - "STOPPING" 28 | # - "UPDATING" 29 | # - "FAILED" 30 | # - "MAINTAINED" 31 | # - "MAINTAINED_MANUAL" 32 | # - "REMOVING" 33 | # - "NOT_EXISTING" 34 | # - "UNKNOWN" 35 | default: 36 | # resourceHealth: 37 | # - "GREEN" 38 | # - "YELLOW" 39 | # - "ORANGE" 40 | # - "RED" 41 | # - "GREY" 42 | resourceStatus: 43 | # resource data collection stats 44 | # - "ERROR" 45 | # - "UNKNOWN" 46 | # - "DOWN" 47 | - "DATA_RECEIVING" 48 | # - "OLD_DATA_RECEIVING" 49 | # - "NO_DATA_RECEIVING" 50 | # - "NO_PARENT_MONITORING" 51 | # - "COLLECTOR_DOWN" 52 | resourceStates: 53 | # resource states 54 | # - "STOPPED" 55 | # - "STARTING" 56 | - "STARTED" 57 | # - "STOPPING" 58 | # - "UPDATING" 59 | # - "FAILED" 60 | # - "MAINTAINED" 61 | # - "MAINTAINED_MANUAL" 62 | # - "REMOVING" 63 | # - "NOT_EXISTING" 64 | # - "UNKNOWN" 65 | 66 | resourcekinds: 67 | vcops_resourcekinds: 68 | - "vC-Ops-Analytics" 69 | - "vC-Ops-CaSA" 70 | - "vC-Ops-Cluster" 71 | - "vC-Ops-Collector" 72 | - "vC-Ops-Node" 73 | - "vC-Ops-Suite-API" 74 | - "vC-Ops-Watchdog" 75 | 76 | sddc_resourcekinds: 77 | - "NSXT Server" 78 | - "VCENTER" 79 | - "NSXVPostgresService" 80 | - "SSHService" 81 | - "StoragePod" 82 | - "NSXReplicatorService" 83 | - "NSXRabbitmqService" 84 | - "NSXManagerService" 85 | - "NSXControllerService" 86 | - "SDDCHealth Instance" 87 | - "vCenterBackupJob" 88 | -------------------------------------------------------------------------------- /tests/inventory_launch.py: -------------------------------------------------------------------------------- 1 | ../inventory.py -------------------------------------------------------------------------------- /tools/Vrops.py: -------------------------------------------------------------------------------- 1 | from urllib3 import disable_warnings 2 | from urllib3 import exceptions 3 | from tools.helper import chunk_list, remove_html_tags 4 | from threading import Thread 5 | import requests 6 | import json 7 | import os 8 | import queue 9 | import logging 10 | import re 11 | 12 | logger = logging.getLogger('vrops-exporter') 13 | 14 | 15 | class Vrops: 16 | def get_token(target): 17 | url = "https://" + target + "/suite-api/api/auth/token/acquire" 18 | timeout = 40 19 | headers = { 20 | 'Content-Type': "application/json", 21 | 'Accept': "application/json" 22 | } 23 | payload = { 24 | "username": os.environ['USER'], 25 | "authSource": "Local", 26 | "password": os.environ['PASSWORD'] 27 | } 28 | disable_warnings(exceptions.InsecureRequestWarning) 29 | try: 30 | response = requests.post(url, 31 | data=json.dumps(payload), 32 | verify=False, 33 | headers=headers, 34 | timeout=timeout) 35 | except requests.exceptions.ReadTimeout as e: 36 | logger.error(f'Request to {url} timed out. Error: {e}') 37 | return False, 504, timeout 38 | except Exception as e: 39 | logger.error(f'Problem connecting to {target}. Error: {e}') 40 | return False, 503, 0 41 | 42 | if response.status_code == 200: 43 | return response.json()["token"], response.status_code, response.elapsed.total_seconds() 44 | else: 45 | logger.error(f'Problem getting token from {target} : {response.text}') 46 | return False, response.status_code, response.elapsed.total_seconds() 47 | 48 | def get_adapter(self, target: str, token: str, adapterkind: str) -> (list, int): 49 | url = f'https://{target}/suite-api/api/adapters' 50 | timeout = 40 51 | querystring = { 52 | "adapterKindKey": adapterkind 53 | } 54 | headers = { 55 | 'Content-Type': "application/json", 56 | 'Accept': "application/json", 57 | 'Authorization': f"vRealizeOpsToken {token}" 58 | } 59 | adapter = list() 60 | disable_warnings(exceptions.InsecureRequestWarning) 61 | try: 62 | response = requests.get(url, 63 | params=querystring, 64 | verify=False, 65 | headers=headers, 66 | timeout=timeout) 67 | except requests.exceptions.ReadTimeout as e: 68 | logger.error(f'Request to {url} timed out. Error: {e}') 69 | return adapter, 504, timeout 70 | except Exception as e: 71 | logger.error(f'Problem connecting to {target} - Error: {e}') 72 | return adapter, 503, 0 73 | 74 | if response.status_code == 200: 75 | for resource in response.json()["adapterInstancesInfoDto"]: 76 | resourcekindkey = resource["resourceKey"]["resourceKindKey"] 77 | resourcekindkey = re.sub("[^a-zA-Z]+", "", resourcekindkey) 78 | 79 | adapter_object = type(resourcekindkey, (object,), { 80 | "name": resource["resourceKey"]["name"], 81 | "uuid": resource["id"], 82 | "adapterkind": adapterkind, 83 | "resourcekindkey": resourcekindkey, 84 | "target": target, 85 | "token": token 86 | }) 87 | adapter.append(adapter_object) 88 | return adapter, response.status_code, response.elapsed.total_seconds() 89 | else: 90 | logger.error(f'Problem getting adapter {target} : {response.text}') 91 | return adapter, response.status_code, response.elapsed.total_seconds() 92 | 93 | def get_vcenter_adapter(self, target, token): 94 | return self.get_adapter(target, token, adapterkind="VMWARE") 95 | 96 | def get_nsxt_adapter(self, target, token): 97 | return self.get_adapter(target, token, adapterkind="NSXTAdapter") 98 | 99 | def get_vcenter_operations_adapter_intance(self, target, token): 100 | return self.get_adapter(target, token, adapterkind="vCenter Operations Adapter") 101 | 102 | def get_sddc_health_adapter_intance(self, target, token): 103 | return self.get_adapter(target, token, adapterkind="SDDCHealthAdapter") 104 | 105 | def get_resources(self, target: str, 106 | token: str, 107 | adapterkind: str, 108 | resourcekinds: list, # Array of resource kind keys 109 | uuids: list, # Array of parent uuids 110 | query_specs: dict, # Dict of resource query specifications 111 | h_depth: int = 1) -> (list, int): 112 | if not uuids: 113 | logger.debug(f'No parent resources for {resourcekinds} from {target}') 114 | return [], 400, 0 115 | logger.debug(f'Getting {resourcekinds} from {target}') 116 | url = "https://" + target + "/suite-api/api/resources/bulk/relationships" 117 | timeout = 40 118 | 119 | logger.debug(f'Using resource query specs: {query_specs}') 120 | r_status_list = [rs for rs in query_specs.get('resourceStatus', [])] 121 | r_health_list = [rh for rh in query_specs.get('resourceHealth', [])] 122 | r_states_list = [rst for rst in query_specs.get('resourceStates', [])] 123 | 124 | querystring = { 125 | 'pageSize': 10000 126 | } 127 | payload = { 128 | "relationshipType": "DESCENDANT", 129 | "resourceIds": uuids, 130 | "resourceQuery": { 131 | "adapterKind": [adapterkind], 132 | "resourceKind": resourcekinds, 133 | "resourceStatus": r_status_list, 134 | "resourceHealth": r_health_list, 135 | "resourceState": r_states_list 136 | }, 137 | "hierarchyDepth": h_depth 138 | } 139 | headers = { 140 | 'Content-Type': "application/json", 141 | 'Accept': "application/json", 142 | 'Authorization': f"vRealizeOpsToken {token}" 143 | } 144 | resources = list() 145 | disable_warnings(exceptions.InsecureRequestWarning) 146 | try: 147 | response = requests.post(url, 148 | data=json.dumps(payload), 149 | params=querystring, 150 | verify=False, 151 | headers=headers, 152 | timeout=timeout) 153 | except requests.exceptions.ReadTimeout as e: 154 | logger.error(f'Request to {url} timed out. Error: {e}') 155 | return resources, 504, timeout 156 | except Exception as e: 157 | logger.error(f'Problem connecting to {target} - Error: {e}') 158 | return resources, 503, 0 159 | 160 | if response.status_code == 200: 161 | try: 162 | relations = response.json()["resourcesRelations"] 163 | if not relations: 164 | resourcekinds_beautyfied = ', '.join(resourcekinds) 165 | logger.warning(f'No child relation returned for {resourcekinds_beautyfied} from adapter {adapterkind} for {target}.') 166 | return resources, 204, response.elapsed.total_seconds() 167 | for resource in relations: 168 | resourcekind = resource["resource"]["resourceKey"]["resourceKindKey"] 169 | resourcekind = re.sub("[^a-zA-Z]+", "", resourcekind) 170 | resource_identifiers = resource["resource"]["resourceKey"]["resourceIdentifiers"] 171 | internal_name = list(filter(lambda identifier_type: 172 | identifier_type['identifierType']['name'] 173 | == 'VMEntityObjectID', resource_identifiers)) 174 | internal_name = internal_name[0].get('value') if internal_name else None 175 | instance_uuid = list(filter(lambda identifier_type: 176 | identifier_type['identifierType']['name'] 177 | == 'VMEntityInstanceUUID', resource_identifiers)) 178 | instance_uuid = instance_uuid[0].get('value') if instance_uuid else None 179 | 180 | resource_object = type(resourcekind, (object,), { 181 | "name": resource["resource"]["resourceKey"]["name"], 182 | "uuid": resource["resource"]["identifier"], 183 | "resourcekind": resourcekind, 184 | "parent": resource.get("relatedResources", ["None"])[0], 185 | "internal_name": internal_name, 186 | "instance_uuid": instance_uuid 187 | }) 188 | if not resource.get("relatedResources"): 189 | logger.warning(f'No parent relation returned for {resource["resource"]["resourceKey"]["name"]}; resourcekind: {resourcekind}; target: {target}.') 190 | resources.append(resource_object) 191 | return resources, response.status_code, response.elapsed.total_seconds() 192 | except json.decoder.JSONDecodeError as e: 193 | logger.error(f'Catching JSONDecodeError for target {target}' 194 | f'- Error: {e}') 195 | return resources, response.status_code, response.elapsed.total_seconds() 196 | else: 197 | logger.error(f'Problem getting resources from {target} : {response.text}') 198 | return resources, response.status_code, response.elapsed.total_seconds() 199 | 200 | def get_datacenter(self, target, token, parent_uuids, query_specs): 201 | resourcekind = 'Datacenter' 202 | return self.get_resources(target, token, adapterkind="VMWARE", resourcekinds=[resourcekind], 203 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind)) 204 | 205 | def get_cluster(self, target, token, parent_uuids, query_specs): 206 | resourcekind = 'ClusterComputeResource' 207 | return self.get_resources(target, token, adapterkind="VMWARE", resourcekinds=[resourcekind], 208 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind)) 209 | 210 | def get_SDRS_cluster(self, target, token, parent_uuids, query_specs): 211 | resourcekind = 'StoragePod' 212 | return self.get_resources(target, token, adapterkind="VMWARE", resourcekinds=["StoragePod"], 213 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind)) 214 | 215 | def get_datastores(self, target, token, parent_uuids, query_specs): 216 | resourcekind = 'Datastore' 217 | datastores, http_code, response_time = self.get_resources(target, token, adapterkind="VMWARE", 218 | resourcekinds=["Datastore"], uuids=parent_uuids, 219 | query_specs=self._set_query_specs(query_specs, resourcekind)) 220 | for datastore in datastores: 221 | if "p_ssd" in datastore.name: 222 | datastore.type = "vmfs_p_ssd" 223 | elif "s_hdd" in datastore.name: 224 | datastore.type = "vmfs_s_hdd" 225 | elif datastore.name.startswith("eph") or datastore.name.startswith("donotdeploy_eph"): 226 | datastore.type = "ephemeral" 227 | elif "Management" in datastore.name: 228 | datastore.type = "Management" 229 | elif datastore.name.endswith("local"): 230 | datastore.type = "local" 231 | elif datastore.name.startswith('nfs'): 232 | datastore.type = "nfs" 233 | elif datastore.name.startswith('nsxt'): 234 | datastore.type = "nsxt" 235 | elif datastore.name.endswith("swap"): 236 | datastore.type = "NVMe" 237 | else: 238 | datastore.type = "other" 239 | return datastores, http_code, response_time 240 | 241 | def get_hosts(self, target, token, parent_uuids, query_specs): 242 | resourcekind = 'HostSystem' 243 | return self.get_resources(target, token, adapterkind="VMWARE", resourcekinds=[resourcekind], 244 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind)) 245 | 246 | def get_vms(self, target, token, parent_uuids, vcenter_uuid, query_specs): 247 | if not parent_uuids: 248 | logger.debug(f'No parent resources for virtual machines from {target}') 249 | return [], 400, 0 250 | resourcekind = 'VirtualMachine' 251 | q_specs = self._set_query_specs(query_specs, resourcekind) 252 | amount_vms, http_code, _ = self.get_latest_stats_multiple(target, token, [vcenter_uuid], 253 | ['summary|total_number_vms'], 254 | 'Inventory') 255 | 256 | number_of_vms = amount_vms[0].get('stat-list', {}).get('stat', [])[0].get('data', [0])[0] if \ 257 | http_code == 200 and amount_vms else 0 258 | 259 | # vrops cannot handle more than 10000 uuids in a single request 260 | split_factor = int(number_of_vms / 10000) 261 | if split_factor >= 1: 262 | uuids_chunked = list(chunk_list(parent_uuids, int(len(parent_uuids) / (split_factor + 1)))) 263 | logger.debug(f'Chunking VM requests into {len(uuids_chunked)} chunks') 264 | vms = list() 265 | http_codes = list() 266 | response_times = list() 267 | 268 | for uuid_list in uuids_chunked: 269 | 270 | vm_chunks, http_code, response_time = self.get_resources(target, token, adapterkind="VMWARE", 271 | resourcekinds=[resourcekind], 272 | uuids=uuid_list, query_specs=q_specs) 273 | vms.extend(vm_chunks) 274 | http_codes.append(http_code) 275 | response_times.append(response_time) 276 | logger.debug(f'Number of VMs collected: {len(vms)}') 277 | return vms, max(http_codes), sum(response_times) 278 | return self.get_resources(target, token, adapterkind="VMWARE", resourcekinds=[resourcekind], 279 | uuids=parent_uuids, query_specs=q_specs) 280 | 281 | def get_dis_virtual_switch(self, target, token, parent_uuids, query_specs): 282 | resourcekind = 'VmwareDistributedVirtualSwitch' 283 | return self.get_resources(target, token, adapterkind="VMWARE", resourcekinds=[resourcekind], 284 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind)) 285 | 286 | def get_nsxt_mgmt_cluster(self, target, token, parent_uuids, query_specs): 287 | resourcekind = 'ManagementCluster' 288 | return self.get_resources(target, token, adapterkind="NSXTAdapter", resourcekinds=[resourcekind], 289 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind)) 290 | 291 | def get_nsxt_mgmt_nodes(self, target, token, parent_uuids, query_specs): 292 | resourcekind = 'ManagementNode' 293 | return self.get_resources(target, token, adapterkind="NSXTAdapter", resourcekinds=[resourcekind], 294 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind), h_depth=5) 295 | 296 | def get_nsxt_mgmt_service(self, target, token, parent_uuids, query_specs): 297 | resourcekind = 'ManagementService' 298 | return self.get_resources(target, token, adapterkind="NSXTAdapter", resourcekinds=[resourcekind], 299 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind), h_depth=5) 300 | 301 | def get_nsxt_transport_zone(self, target, token, parent_uuids, query_specs): 302 | resourcekind = 'TransportZone' 303 | return self.get_resources(target, token, adapterkind="NSXTAdapter", resourcekinds=[resourcekind], 304 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind), h_depth=5) 305 | 306 | def get_nsxt_transport_node(self, target, token, parent_uuids, query_specs): 307 | resourcekind = 'TransportNode' 308 | return self.get_resources(target, token, adapterkind="NSXTAdapter", resourcekinds=[resourcekind], 309 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind)) 310 | 311 | def get_nsxt_logical_switch(self, target, token, parent_uuids, query_specs): 312 | resourcekind = 'LogicalSwitch' 313 | return self.get_resources(target, token, adapterkind="NSXTAdapter", resourcekinds=[resourcekind], 314 | uuids=parent_uuids, query_specs=self._set_query_specs(query_specs, resourcekind), h_depth=5) 315 | 316 | def get_vcops_instances(self, target, token, parent_uuids, resourcekinds, query_specs): 317 | return self.get_resources(target, token, adapterkind="vCenter Operations Adapter", 318 | resourcekinds=resourcekinds, uuids=parent_uuids, 319 | query_specs=query_specs.get('default', {}), h_depth=5) 320 | 321 | def get_sddc_instances(self, target, token, parent_uuids, resourcekinds, query_specs): 322 | return self.get_resources(target, token, adapterkind="SDDCHealthAdapter", 323 | resourcekinds=resourcekinds, uuids=parent_uuids, 324 | query_specs=query_specs.get('default', {}), h_depth=5) 325 | 326 | def _set_query_specs(self, query_specs, resourcekind): 327 | return query_specs.get(resourcekind) if resourcekind in query_specs else query_specs.get('default', {}) 328 | 329 | def get_latest_values_multiple(self, target: str, token: str, 330 | uuids: list, 331 | keys: list, 332 | collector: str, 333 | kind: str = None) -> (list, int, float): 334 | 335 | # vrops can not handle more than 1000 uuids for stats 336 | uuids_chunked = list(chunk_list(uuids, 1000)) if kind == 'stats' else [uuids] 337 | 338 | url = f"https://{target}/suite-api/api/resources/stats/latest/query" if kind == 'stats' else \ 339 | f"https://{target}/suite-api/api/resources/properties/latest/query" 340 | 341 | headers = { 342 | 'Content-Type': "application/json", 343 | 'Accept': "application/json", 344 | 'Authorization': f"vRealizeOpsToken {token}" 345 | } 346 | 347 | q = queue.Queue() 348 | thread_list = list() 349 | chunk_iteration = 0 350 | 351 | logger.debug('>----------------------- get_latest_values_multiple') 352 | logger.debug(f'target : {target}') 353 | logger.debug(f'collector: {collector}') 354 | logger.debug(f'Amount keys : {len(keys)}') 355 | for k in keys: 356 | logger.debug(f'key : {k}') 357 | 358 | for uuid_list in uuids_chunked: 359 | chunk_iteration += 1 360 | t = Thread(target=Vrops._get_chunk, 361 | args=(q, uuid_list, url, headers, keys, target, kind, collector, chunk_iteration)) 362 | thread_list.append(t) 363 | t.start() 364 | for t in thread_list: 365 | t.join() 366 | 367 | return_list = list() 368 | response_status_codes = list() 369 | response_time_elapsed = list() 370 | 371 | while not q.empty(): 372 | returned_chunks = q.get() 373 | response_time_elapsed.append(returned_chunks[2]) 374 | response_status_codes.append(returned_chunks[1]) 375 | return_list.extend(returned_chunks[0]) 376 | 377 | logger.debug(f'Amount uuids: {len(uuids)}') 378 | logger.debug(f'Fetched : {len({r.get("resourceId") for r in return_list})}') 379 | logger.debug('<--------------------------------------------------') 380 | 381 | return return_list, max(response_status_codes), sum(response_time_elapsed) / len(response_time_elapsed) 382 | 383 | def get_latest_properties_multiple(self, target: str, token: str, uuids: list, keys: list, collector: str): 384 | return self.get_latest_values_multiple(target, token, uuids, keys, collector, kind='properties') 385 | 386 | def get_latest_stats_multiple(self, target: str, token: str, uuids: list, keys: list, collector: str): 387 | return self.get_latest_values_multiple(target, token, uuids, keys, collector, kind='stats') 388 | 389 | def _get_chunk(q, uuid_list, url, headers, keys, target, kind, collector, chunk_iteration): 390 | logger.debug(f'chunk: {chunk_iteration}') 391 | 392 | querystring = { 393 | "pageSize": 10000 394 | } 395 | # Indicates whether to report only "current" stat values, i.e. skip the stat-s that haven't published any value 396 | # during recent collection cycles. 397 | current_only = True 398 | 399 | payload = { 400 | "resourceId": uuid_list, 401 | "statKey": keys, 402 | "currentOnly": current_only 403 | } if kind == 'stats' else { 404 | "resourceIds": uuid_list, 405 | "propertyKeys": keys, 406 | "currentOnly": current_only 407 | } 408 | 409 | disable_warnings(exceptions.InsecureRequestWarning) 410 | try: 411 | response = requests.post(url, 412 | params=querystring, 413 | data=json.dumps(payload), 414 | verify=False, 415 | headers=headers, 416 | timeout=60) 417 | except requests.exceptions.ReadTimeout as e: 418 | logger.error(f'{collector} has timed out getting latest data from: {target} - Error: {e}') 419 | q.put([[], 504, 999]) 420 | return 421 | except Exception as e: 422 | logger.error(f'{collector} has problems getting latest data from: {target} - Error: {e}') 423 | q.put([[], 503, 999]) 424 | return 425 | 426 | if response.status_code == 200: 427 | try: 428 | q.put([response.json().get('values', []), response.status_code, response.elapsed.total_seconds()]) 429 | except json.decoder.JSONDecodeError as e: 430 | logger.error(f'Catching JSONDecodeError for {collector}, target: {collector}, chunk_iteration: ' 431 | f'{chunk_iteration} - Error: {e}') 432 | q.put([[], response.status_code, response.elapsed.total_seconds()]) 433 | return 434 | else: 435 | logger.error(f'Return code: {response.status_code} != 200 for {collector} : {response.text}') 436 | q.put([[], response.status_code, response.elapsed.total_seconds()]) 437 | return 438 | 439 | def get_project_ids(target: str, token: str, uuids: list, collector: str) -> (list, int): 440 | logger.debug('>---------------------------------- get_project_ids') 441 | logger.debug(f'target : {target}') 442 | logger.debug(f'collector: {collector}') 443 | 444 | project_ids = list() 445 | url = f'https://{target}/suite-api/api/resources/bulk/relationships' 446 | querystring = { 447 | 'pageSize': 10000 448 | } 449 | headers = { 450 | 'Content-Type': "application/json", 451 | 'Accept': "application/json", 452 | 'Authorization': "vRealizeOpsToken " + token 453 | } 454 | payload = { 455 | "relationshipType": "ANCESTOR", 456 | "resourceIds": uuids, 457 | "resourceQuery": { 458 | "name": ["Project"], 459 | "adapterKind": ["VMWARE"], 460 | "resourceKind": ["VMFolder"] 461 | }, 462 | "hierarchyDepth": 5 463 | } 464 | disable_warnings(exceptions.InsecureRequestWarning) 465 | try: 466 | response = requests.post(url, 467 | data=json.dumps(payload), 468 | params=querystring, 469 | verify=False, 470 | headers=headers, 471 | timeout=30) 472 | except requests.exceptions.ReadTimeout as e: 473 | logger.error(f'Request for getting project folder timed out - Error: {e}') 474 | return [], 504 475 | except Exception as e: 476 | logger.error(f'Problem getting project folder - Error: {e}') 477 | return [], 503 478 | 479 | if response.status_code == 200: 480 | try: 481 | for project in response.json()['resourcesRelations']: 482 | p_ids = dict() 483 | for vm_uuid in project["relatedResources"]: 484 | project_name = project["resource"]["resourceKey"]["name"] 485 | p_ids[vm_uuid] = project_name[project_name.find("(") + 1:project_name.find(")")] 486 | project_ids.append(p_ids) 487 | except json.decoder.JSONDecodeError as e: 488 | logger.error(f'Catching JSONDecodeError for target: {target}, {collector}' 489 | f' - Error: {e}') 490 | return [], response.status_code 491 | else: 492 | logger.error(f'Return code: {response.status_code} != 200 for {target} : {response.text}') 493 | return [], response.status_code 494 | 495 | logger.debug(f'Fetched project ids: {len(project_ids)}') 496 | logger.debug('<--------------------------------------------------') 497 | 498 | return project_ids 499 | 500 | def get_alerts(self, target: str, token: str, 501 | alert_criticality: list, # [ "CRITICAL", "IMMEDIATE", "WARNING", "INFORMATION" ] 502 | resourcekinds: list, # [ "HostSystem" ] 503 | active_only=True, 504 | resourceIds: list = None, 505 | adapterkinds: list = None, # [ "VMWARE" ] 506 | resource_names: list = None, # [ "Windows2017VM", "Windows2018VM" ] 507 | regex: list = None # [ "\\\\S+-BNA-\\\\S+", null ] 508 | ): 509 | logger.debug('>---------------------------------- get_alerts') 510 | logger.debug(f'target : {target}') 511 | 512 | alerts = list() 513 | url = f'https://{target}/suite-api/api/alerts/query' 514 | querystring = { 515 | "pageSize": 10000 516 | } 517 | 518 | headers = { 519 | 'Content-Type': "application/json", 520 | 'Accept': "application/json", 521 | 'Authorization': "vRealizeOpsToken " + token 522 | } 523 | payload = { 524 | "compositeOperator": "AND", 525 | "resource-query": { 526 | "name": resource_names, 527 | "regex": regex, 528 | "adapterKind": adapterkinds, 529 | "resourceKind": resourcekinds, 530 | "resourceId": resourceIds, 531 | "statKeyInclusive": True 532 | }, 533 | "activeOnly": active_only, 534 | "alertCriticality": alert_criticality 535 | } 536 | disable_warnings(exceptions.InsecureRequestWarning) 537 | try: 538 | response = requests.post(url, 539 | data=json.dumps(payload), 540 | params=querystring, 541 | verify=False, 542 | headers=headers, 543 | timeout=30) 544 | except requests.exceptions.ReadTimeout as e: 545 | logger.error(f'Request for getting project folder timed out - Error: {e}') 546 | return [], 504 547 | except Exception as e: 548 | logger.error(f'Problem getting project folder - Error: {e}') 549 | return [], 503 550 | if response.status_code == 200: 551 | try: 552 | for alert in response.json()['alerts']: 553 | alert_dict = dict() 554 | alert_dict["resourceId"] = alert["resourceId"] 555 | alert_dict["alertLevel"] = alert["alertLevel"] 556 | alert_dict["status"] = alert['status'] 557 | alert_dict["alertDefinitionName"] = alert.get("alertDefinitionName", alert["alertDefinitionId"]) 558 | alert_dict["alertImpact"] = alert["alertImpact"] 559 | alert_dict["alertDefinitionId"] = alert["alertDefinitionId"] 560 | alerts.append(alert_dict) 561 | except json.decoder.JSONDecodeError as e: 562 | logger.error(f'Catching JSONDecodeError for target: {target}' 563 | f' - Error: {e}') 564 | return [], response.status_code 565 | else: 566 | logger.error(f'Return code get_alerts: {response.status_code} != 200 for {target} : {response.text}') 567 | return [], response.status_code 568 | 569 | logger.debug(f'Fetched alerts: {len(alerts)}') 570 | logger.debug('<--------------------------------------------------') 571 | 572 | return alerts, response.status_code, response.elapsed.total_seconds() 573 | 574 | def get_definitions(self, target, token, name: str): 575 | url = f'https://{target}/suite-api/api/{name}' 576 | 577 | querystring = { 578 | 'pageSize': 10000 579 | } 580 | headers = { 581 | 'Content-Type': "application/json", 582 | 'Accept': "application/json", 583 | 'Authorization': "vRealizeOpsToken " + token 584 | } 585 | disable_warnings(exceptions.InsecureRequestWarning) 586 | try: 587 | response = requests.get(url, 588 | params=querystring, 589 | verify=False, 590 | headers=headers, 591 | timeout=30) 592 | except requests.exceptions.ReadTimeout as e: 593 | logger.error(f'Request to {url} timed out. Error: {e}') 594 | return {} 595 | except Exception as e: 596 | logger.error(f'Problem connecting to {target} - Error: {e}') 597 | return {} 598 | 599 | if response.status_code == 200: 600 | return response.json() 601 | 602 | else: 603 | logger.error(f'Problem getting {name} {target} : {response.text}') 604 | return {} 605 | 606 | def get_alert_recommendations(self, target, token): 607 | return self.get_definitions(target, token, name='recommendations') 608 | 609 | def get_alert_symptomdefinitions(self, target, token): 610 | return self.get_definitions(target, token, name='symptomdefinitions') 611 | 612 | def get_alertdefinitions(self, target, token): 613 | alertdefinitions = self.get_definitions(target, token, name='alertdefinitions') 614 | symptomdefinitions = self.get_alert_symptomdefinitions(target, token) 615 | recommendations = self.get_alert_recommendations(target, token) 616 | 617 | # mapping symptoms and recommendations to alerts 618 | alerts = dict() 619 | for alert in alertdefinitions['alertDefinitions']: 620 | alert_entry = dict() 621 | alert_entry['id'] = alert.get('id') 622 | alert_entry['name'] = alert.get('name') 623 | alert_entry['description'] = alert.get('description', 'n/a') 624 | alert_entry['adapterKindKey'] = alert.get('adapterKindKey', 'n/a') 625 | alert_entry['resourceKindKey'] = alert.get('resourceKindKey', 'n/a') 626 | alert_entry['symptoms'] = list() 627 | symptomdefinition_ids = alert.get("states", [])[0].get("base-symptom-set", {}).get( 628 | "symptomDefinitionIds", []) 629 | for symptom_id in symptomdefinition_ids: 630 | for symptomdefinition_id in symptomdefinitions["symptomDefinitions"]: 631 | if symptom_id == symptomdefinition_id['id']: 632 | symptom_entry = dict() 633 | symptom_entry['name'] = symptomdefinition_id['name'] 634 | symptom_entry['state'] = symptomdefinition_id['state'] 635 | alert_entry['symptoms'].append(symptom_entry) 636 | 637 | alert_entry['recommendations'] = list() 638 | recommendation_ids = alert.get("states", [])[0].get("recommendationPriorityMap", {}) 639 | for recommendation in recommendation_ids: 640 | for rd in recommendations["recommendations"]: 641 | if recommendation == rd["id"]: 642 | recommendation_entry = dict() 643 | recommendation_entry['id'] = rd.get("id") 644 | recommendation_entry['description'] = remove_html_tags(rd.get("description")) 645 | alert_entry['recommendations'].append(recommendation_entry) 646 | alerts[alert.get('id')] = alert_entry 647 | return alerts 648 | 649 | def get_service_states(self, target: str, token: str): 650 | url = f'https://{target}/suite-api/api/deployment/node/services/info' 651 | timeout = 40 652 | headers = { 653 | 'Content-Type': "application/json", 654 | 'Accept': "application/json", 655 | 'Authorization': f"vRealizeOpsToken {token}" 656 | } 657 | disable_warnings(exceptions.InsecureRequestWarning) 658 | try: 659 | response = requests.get(url, 660 | verify=False, 661 | headers=headers, 662 | timeout=timeout) 663 | except requests.exceptions.ReadTimeout as e: 664 | logger.error(f'Request to {url} timed out. Error: {e}') 665 | return {}, 504, timeout 666 | except Exception as e: 667 | logger.error(f'Problem connecting to {target} - Error: {e}') 668 | return {}, 503, 0 669 | 670 | if response.status_code == 200: 671 | return response.json(), response.status_code, response.elapsed.total_seconds() 672 | else: 673 | logger.error(f'Problem getting service stats from {target} : {response.text}') 674 | return response.json(), response.status_code, response.elapsed.total_seconds() 675 | -------------------------------------------------------------------------------- /tools/YamlRead.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | class YamlRead: 5 | def __init__(self, path): 6 | self._path = path 7 | 8 | def run(self): 9 | yml = dict() 10 | with open(self._path, 'r') as stream: 11 | try: 12 | yml = yaml.safe_load(stream) 13 | except yaml.YAMLError as exc: 14 | print(exc) 15 | return yml 16 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapcc/vrops-exporter/7969a5859b53da618d605d5294d6cbb280a0a660/tools/__init__.py -------------------------------------------------------------------------------- /tools/helper.py: -------------------------------------------------------------------------------- 1 | def chunk_list(lst, n): 2 | for i in range(0, len(lst), n): 3 | yield lst[i:i + n] 4 | 5 | 6 | def yaml_read(path): 7 | import yaml 8 | yml = dict() 9 | with open(path, 'r') as stream: 10 | try: 11 | yml = yaml.safe_load(stream) 12 | except yaml.YAMLError as exc: 13 | print(exc) 14 | return yml 15 | 16 | 17 | def remove_html_tags(text): 18 | from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning 19 | import warnings 20 | import re 21 | warnings.filterwarnings('ignore', category=MarkupResemblesLocatorWarning) 22 | 23 | soup = BeautifulSoup(text, features="lxml") 24 | text = soup.text 25 | text = re.sub(r"\s+", " ", text) 26 | text = re.sub("\n", "", text) 27 | for link in soup.find_all('a'): 28 | text = text + " " + link.get('href') if link.get('href') else text 29 | return text 30 | --------------------------------------------------------------------------------