Add VIO support in DAGs 82/13982/9 pre-v15.0 release-v15.0-start
authoraguilard <e.dah.tid@telefonica.com>
Tue, 31 Oct 2023 13:03:12 +0000 (13:03 +0000)
committeraguilard <e.dah.tid@telefonica.com>
Tue, 7 Nov 2023 16:41:15 +0000 (16:41 +0000)
Change-Id: I055587d4df315be45371cc84043cdc1f2e1b7888
Signed-off-by: aguilard <e.dah.tid@telefonica.com>
src/osm_ngsa/dags/multivim_vim_status.py
src/osm_ngsa/dags/multivim_vm_metrics.py
src/osm_ngsa/dags/multivim_vm_status.py
src/osm_ngsa/osm_mon/vim_connectors/openstack.py
src/osm_ngsa/osm_mon/vim_connectors/vrops_helper.py [new file with mode: 0644]
tox.ini

index e830ff1..d9f54bb 100644 (file)
@@ -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)
index 2e67ce1..acec04d 100644 (file)
@@ -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)
index 18a02e1..80a38eb 100644 (file)
@@ -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)
index 1eb33af..1918a22 100644 (file)
@@ -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 (file)
index 0000000..7bd1ec9
--- /dev/null
@@ -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&currentOnly=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 (file)
--- 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]