From 54c033b73391a95c16f8ec737a9970072763acb2 Mon Sep 17 00:00:00 2001 From: aguilard Date: Tue, 31 Oct 2023 13:03:12 +0000 Subject: [PATCH] Add VIO support in DAGs Change-Id: I055587d4df315be45371cc84043cdc1f2e1b7888 Signed-off-by: aguilard --- src/osm_ngsa/dags/multivim_vim_status.py | 2 +- src/osm_ngsa/dags/multivim_vm_metrics.py | 2 +- src/osm_ngsa/dags/multivim_vm_status.py | 2 +- .../osm_mon/vim_connectors/openstack.py | 61 ++++- .../osm_mon/vim_connectors/vrops_helper.py | 250 ++++++++++++++++++ tox.ini | 2 +- 6 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 src/osm_ngsa/osm_mon/vim_connectors/vrops_helper.py diff --git a/src/osm_ngsa/dags/multivim_vim_status.py b/src/osm_ngsa/dags/multivim_vim_status.py index e830ff1..d9f54bb 100644 --- a/src/osm_ngsa/dags/multivim_vim_status.py +++ b/src/osm_ngsa/dags/multivim_vim_status.py @@ -85,7 +85,7 @@ def create_dag(dag_id, dag_number, dag_description, vim_id): vim_type = vim_account["config"]["vim_type"].lower() if vim_type == "vio" and "vrops_site" not in vim_account["config"]: vim_type = "openstack" - if vim_type == "openstack": + if vim_type == "openstack" or vim_type == "vio": return OpenStackCollector(vim_account) if vim_type == "gcp": return GcpCollector(vim_account) diff --git a/src/osm_ngsa/dags/multivim_vm_metrics.py b/src/osm_ngsa/dags/multivim_vm_metrics.py index 2e67ce1..acec04d 100644 --- a/src/osm_ngsa/dags/multivim_vm_metrics.py +++ b/src/osm_ngsa/dags/multivim_vm_metrics.py @@ -226,7 +226,7 @@ def create_dag(dag_id, dag_number, dag_description, vim_id): vim_type = vim_account["config"]["vim_type"].lower() if vim_type == "vio" and "vrops_site" not in vim_account["config"]: vim_type = "openstack" - if vim_type == "openstack": + if vim_type == "openstack" or vim_type == "vio": collector = OpenStackCollector(vim_account) elif vim_type == "azure": collector = AzureCollector(vim_account) diff --git a/src/osm_ngsa/dags/multivim_vm_status.py b/src/osm_ngsa/dags/multivim_vm_status.py index 18a02e1..80a38eb 100644 --- a/src/osm_ngsa/dags/multivim_vm_status.py +++ b/src/osm_ngsa/dags/multivim_vm_status.py @@ -86,7 +86,7 @@ def create_dag(dag_id, dag_number, dag_description, vim_id): vim_type = vim_account["config"]["vim_type"].lower() if vim_type == "vio" and "vrops_site" not in vim_account["config"]: vim_type = "openstack" - if vim_type == "openstack": + if vim_type == "openstack" or vim_type == "vio": return OpenStackCollector(vim_account) if vim_type == "gcp": return GcpCollector(vim_account) diff --git a/src/osm_ngsa/osm_mon/vim_connectors/openstack.py b/src/osm_ngsa/osm_mon/vim_connectors/openstack.py index 1eb33af..1918a22 100644 --- a/src/osm_ngsa/osm_mon/vim_connectors/openstack.py +++ b/src/osm_ngsa/osm_mon/vim_connectors/openstack.py @@ -28,6 +28,7 @@ from keystoneauth1.exceptions.catalog import EndpointNotFound from keystoneauth1.identity import v3 from novaclient import client as nova_client from osm_mon.vim_connectors.base_vim import VIMConnector +from osm_mon.vim_connectors.vrops_helper import vROPS_Helper from prometheus_api_client import PrometheusConnect as prometheus_client log = logging.getLogger(__name__) @@ -95,7 +96,6 @@ class OpenStackCollector(VIMConnector): self.vim_session = None self.vim_session = self._get_session(vim_account) self.nova = self._build_nova_client() - # self.gnocchi = self._build_gnocchi_client() self.backend = self._get_backend(vim_account, self.vim_session) def _get_session(self, creds: Dict): @@ -139,6 +139,20 @@ class OpenStackCollector(VIMConnector): # log.error(f"Can't create prometheus client, {e}") # return None return None + + if "config" in vim_account and "vim_type" in vim_account["config"]: + vim_type = vim_account["config"]["vim_type"].lower() + log.debug(f"vim_type: {vim_type}") + log.debug(f"vim_account[config]: {vim_account['config']}") + if vim_type == "vio" and "vrops_site" in vim_account["config"]: + try: + log.debug("Using vROPS backend to collect metric") + vrops = VropsBackend(vim_account) + return vrops + except Exception as e: + log.error(f"Can't create vROPS client, {e}") + return None + try: gnocchi = GnocchiBackend(vim_account, vim_session) gnocchi.client.metric.list(limit=1) @@ -196,6 +210,10 @@ class OpenStackCollector(VIMConnector): log.info("Using Prometheus as backend (NOT SUPPORTED)") return [] + if type(self.backend) is VropsBackend: + log.info("Using vROPS as backend") + return self.backend.collect_metrics(metric_list) + metric_results = [] for metric in metric_list: server = metric["vm_id"] @@ -224,6 +242,9 @@ class OpenstackBackend: ): pass + def collect_metrics(self, metrics_list: List[Dict]): + pass + class PrometheusTSBDBackend(OpenstackBackend): def __init__(self, vim_account: dict): @@ -418,3 +439,41 @@ class CeilometerBackend(OpenstackBackend): q=[{"field": "resource_id", "op": "eq", "value": resource_id}], ) return measures[0].counter_volume if measures else None + + +class VropsBackend(OpenstackBackend): + def __init__(self, vim_account: dict): + self.vrops = vROPS_Helper( + vrops_site=vim_account["config"]["vrops_site"], + vrops_user=vim_account["config"]["vrops_user"], + vrops_password=vim_account["config"]["vrops_password"], + ) + + def collect_metrics(self, metrics_list: List[Dict]): + # Fetch the list of all known resources from vROPS. + resource_list = self.vrops.get_vm_resource_list_from_vrops() + + vdu_mappings = {} + extended_metrics = [] + for metric in metrics_list: + vim_id = metric["vm_id"] + # Map the vROPS instance id to the vim-id so we can look it up. + for resource in resource_list: + for resourceIdentifier in resource["resourceKey"][ + "resourceIdentifiers" + ]: + if ( + resourceIdentifier["identifierType"]["name"] + == "VMEntityInstanceUUID" + ): + if resourceIdentifier["value"] != vim_id: + continue + vdu_mappings[vim_id] = resource["identifier"] + if vim_id in vdu_mappings: + metric["vrops_id"] = vdu_mappings[vim_id] + extended_metrics.append(metric) + + if len(extended_metrics) != 0: + return self.vrops.get_metrics(extended_metrics) + else: + return [] diff --git a/src/osm_ngsa/osm_mon/vim_connectors/vrops_helper.py b/src/osm_ngsa/osm_mon/vim_connectors/vrops_helper.py new file mode 100644 index 0000000..7bd1ec9 --- /dev/null +++ b/src/osm_ngsa/osm_mon/vim_connectors/vrops_helper.py @@ -0,0 +1,250 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +import json +import logging +import traceback + +import requests + +log = logging.getLogger(__name__) + +# Ref: https://docs.vmware.com/en/vRealize-Operations-Manager/7.0/vrealize-operations-manager-70-reference-guide.pdf +# Potential metrics of interest +# "cpu|capacity_contentionPct" +# "cpu|corecount_provisioned" +# "cpu|costopPct" +# "cpu|demandmhz" +# "cpu|demandPct" +# "cpu|effective_limit" +# "cpu|iowaitPct" +# "cpu|readyPct" +# "cpu|swapwaitPct" +# "cpu|usage_average" +# "cpu|usagemhz_average" +# "cpu|usagemhz_average_mtd" +# "cpu|vm_capacity_provisioned" +# "cpu|workload" +# "guestfilesystem|percentage_total" +# "guestfilesystem|usage_total" +# "mem|consumedPct" +# "mem|guest_usage" +# "mem|host_contentionPct" +# "mem|reservation_used" +# "mem|swapinRate_average" +# "mem|swapoutRate_average" +# "mem|swapped_average" +# "mem|usage_average" +# "net:Aggregate of all instances|droppedPct" +# "net|broadcastTx_summation" +# "net|droppedTx_summation" +# "net|multicastTx_summation" +# "net|pnicBytesRx_average" +# "net|pnicBytesTx_average" +# "net|received_average" +# "net|transmitted_average" +# "net|usage_average" +# "virtualDisk:Aggregate of all instances|commandsAveraged_average" +# "virtualDisk:Aggregate of all instances|numberReadAveraged_average" +# "virtualDisk:Aggregate of all instances|numberWriteAveraged_average" +# "virtualDisk:Aggregate of all instances|totalLatency" +# "virtualDisk:Aggregate of all instances|totalReadLatency_average" +# "virtualDisk:Aggregate of all instances|totalWriteLatency_average" +# "virtualDisk:Aggregate of all instances|usage" +# "virtualDisk:Aggregate of all instances|vDiskOIO" +# "virtualDisk|read_average" +# "virtualDisk|write_average" +METRIC_MAPPINGS = { + # Percent guest operating system active memory. + "average_memory_utilization": "mem|usage_average", + # Percentage of CPU that was used out of all the CPU that was allocated. + "cpu_utilization": "cpu|usage_average", + # KB/s of data read in the performance interval + "disk_read_bytes": "virtualDisk|read_average", + # Average of read commands per second during the collection interval. + "disk_read_ops": "virtualDisk:aggregate of all instances|numberReadAveraged_average", + # KB/s of data written in the performance interval. + "disk_write_bytes": "virtualDisk|write_average", + # Average of write commands per second during the collection interval. + "disk_write_ops": "virtualDisk:aggregate of all instances|numberWriteAveraged_average", + # Not supported by vROPS, will always return 0. + "packets_in_dropped": "net|droppedRx_summation", + # Transmitted packets dropped in the collection interval. + "packets_out_dropped": "net|droppedTx_summation", + # Bytes received in the performance interval. + "packets_received": "net|received_average", + # Packets transmitted in the performance interval. + "packets_sent": "net|transmitted_average", +} + +# If the unit from vROPS does not align with the expected value. multiply by the specified amount to ensure +# the correct unit is returned. +METRIC_MULTIPLIERS = { + "disk_read_bytes": 1024, + "disk_write_bytes": 1024, + "packets_received": 1024, + "packets_sent": 1024, +} + + +class vROPS_Helper: + def __init__(self, vrops_site="https://vrops", vrops_user="", vrops_password=""): + self.vrops_site = vrops_site + self.vrops_user = vrops_user + self.vrops_password = vrops_password + + def get_vrops_token(self): + """Fetches token from vrops""" + auth_url = "/suite-api/api/auth/token/acquire" + headers = {"Content-Type": "application/json", "Accept": "application/json"} + req_body = {"username": self.vrops_user, "password": self.vrops_password} + resp = requests.post( + self.vrops_site + auth_url, json=req_body, verify=False, headers=headers + ) + if resp.status_code != 200: + log.error( + "Failed to get token from vROPS: {} {}".format( + resp.status_code, resp.content + ) + ) + return None + + resp_data = json.loads(resp.content.decode("utf-8")) + return resp_data["token"] + + def get_vm_resource_list_from_vrops(self): + """Find all known resource IDs in vROPs""" + auth_token = self.get_vrops_token() + api_url = "/suite-api/api/resources?resourceKind=VirtualMachine" + headers = { + "Accept": "application/json", + "Authorization": "vRealizeOpsToken {}".format(auth_token), + } + resource_list = [] + + resp = requests.get(self.vrops_site + api_url, verify=False, headers=headers) + + if resp.status_code != 200: + log.error( + "Failed to get resource list from vROPS: {} {}".format( + resp.status_code, resp.content + ) + ) + return resource_list + + try: + resp_data = json.loads(resp.content.decode("utf-8")) + if resp_data.get("resourceList") is not None: + resource_list = resp_data.get("resourceList") + + except Exception as exp: + log.error( + "get_vm_resource_id: Error in parsing {}\n{}".format( + exp, traceback.format_exc() + ) + ) + + return resource_list + + def get_metrics(self, metrics_list=[]): + monitoring_keys = {} + vdus = {} + # Collect the names of all the metrics we need to query + for metric in metrics_list: + metric_name = metric["metric"] + if metric_name not in METRIC_MAPPINGS: + log.debug(f"Metric {metric_name} not supported, ignoring") + continue + monitoring_keys[metric_name] = METRIC_MAPPINGS[metric_name] + vrops_id = metric["vrops_id"] + vdus[vrops_id] = 1 + + metrics = [] + # Make a query for only the stats we have been asked for + stats_key = "" + for stat in monitoring_keys.values(): + stats_key += "&statKey={}".format(stat) + + # And only ask for the resource ids that we are interested in + resource_ids = "" + for key in vdus.keys(): + resource_ids += "&resourceId={}".format(key) + + try: + # Now we can make a single call to vROPS to collect all relevant metrics for resources we need to monitor + api_url = ( + "/suite-api/api/resources/stats?IntervalType=MINUTES&IntervalCount=1" + "&rollUpType=MAX¤tOnly=true{}{}".format(stats_key, resource_ids) + ) + + auth_token = self.get_vrops_token() + headers = { + "Accept": "application/json", + "Authorization": "vRealizeOpsToken {}".format(auth_token), + } + + resp = requests.get( + self.vrops_site + api_url, verify=False, headers=headers + ) + + if resp.status_code != 200: + log.error( + f"Failed to get Metrics data from vROPS for {resp.status_code} {resp.content}" + ) + return [] + m_data = json.loads(resp.content.decode("utf-8")) + if "values" not in m_data: + return metrics + + statistics = m_data["values"] + for vdu_stat in statistics: + vrops_id = vdu_stat["resourceId"] + log.info(f"vrops_id: {vrops_id}") + for item in vdu_stat["stat-list"]["stat"]: + reported_metric = item["statKey"]["key"] + if reported_metric not in METRIC_MAPPINGS.values(): + continue + + # Convert the vROPS metric name back to OSM key + metric_name = list(METRIC_MAPPINGS.keys())[ + list(METRIC_MAPPINGS.values()).index(reported_metric) + ] + if metric_name in monitoring_keys.keys(): + metric_value = item["data"][-1] + if metric_name in METRIC_MULTIPLIERS: + metric_value *= METRIC_MULTIPLIERS[metric_name] + log.info(f" {metric_name} ({reported_metric}): {metric_value}") + + # Find the associated metric in requested list + for item in metrics_list: + if ( + item["vrops_id"] == vrops_id + and item["metric"] == metric_name + ): + metric = item + metric["value"] = metric_value + metrics.append(metric) + break + + except Exception as exp: + log.error( + "Exception while parsing metrics data from vROPS {}\n{}".format( + exp, traceback.format_exc() + ) + ) + + return metrics diff --git a/tox.ini b/tox.ini index fb89d5c..f215d0f 100644 --- a/tox.ini +++ b/tox.ini @@ -75,7 +75,7 @@ deps = {[testenv]deps} pylint skip_install = true commands = - pylint -E src setup.py --disable=E0401 + pylint -E src setup.py --disable=E0401 --disable=E1111 [testenv:pylint-webhook] -- 2.25.1