RIFT OSM R1 Initial Submission
[osm/SO.git] / rwmon / plugins / vala / rwmon_ceilometer / rwmon_ceilometer.py
diff --git a/rwmon/plugins/vala/rwmon_ceilometer/rwmon_ceilometer.py b/rwmon/plugins/vala/rwmon_ceilometer/rwmon_ceilometer.py
new file mode 100644 (file)
index 0000000..bc6506c
--- /dev/null
@@ -0,0 +1,536 @@
+
+# 
+#   Copyright 2016 RIFT.IO Inc
+#
+#   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 collections
+import dateutil.parser
+import json
+import logging
+import urllib.parse
+
+import requests
+
+import gi
+gi.require_version('RwTypes', '1.0')
+gi.require_version('RwcalYang', '1.0')
+gi.require_version('RwmonYang', '1.0')
+
+from gi.repository import (
+    GObject,
+    RwMon,
+    RwTypes,
+    RwmonYang,
+    )
+
+import rift.rwcal.openstack as openstack_drv
+import rw_status
+import rwlogger
+
+logger = logging.getLogger('rwmon.ceilometer')
+
+rwstatus = rw_status.rwstatus_from_exc_map({
+    IndexError: RwTypes.RwStatus.NOTFOUND,
+    KeyError: RwTypes.RwStatus.NOTFOUND,
+    })
+
+
+class UnknownService(Exception):
+    pass
+
+
+class CeilometerMonitoringPlugin(GObject.Object, RwMon.Monitoring):
+    def __init__(self):
+        GObject.Object.__init__(self)
+        self._driver_class = openstack_drv.OpenstackDriver
+
+    def _get_driver(self, account):
+        return self._driver_class(username = account.openstack.key,
+                                  password = account.openstack.secret,
+                                  auth_url = account.openstack.auth_url,
+                                  tenant_name = account.openstack.tenant,
+                                  mgmt_network = account.openstack.mgmt_network)
+
+    @rwstatus
+    def do_init(self, rwlog_ctx):
+        if not any(isinstance(h, rwlogger.RwLogger) for h in logger.handlers):
+            logger.addHandler(
+                rwlogger.RwLogger(
+                    category="rw-monitor-log",
+                    subcategory="ceilometer",
+                    log_hdl=rwlog_ctx,
+                )
+            )
+
+    @rwstatus(ret_on_failure=[None])
+    def do_nfvi_metrics(self, account, vm_id):
+        try:
+            samples = self._get_driver(account).ceilo_nfvi_metrics(vm_id)
+
+            metrics = RwmonYang.NfviMetrics()
+
+            vcpu = samples.get("cpu_util", {})
+            memory = samples.get("memory_usage", {})
+            storage = samples.get("disk_usage", {})
+
+            metrics.vcpu.utilization = vcpu.get("volume", 0)
+            metrics.memory.used = memory.get("volume", 0)
+            metrics.storage.used = storage.get("volume", 0)
+
+            def convert_timestamp(t):
+                return dateutil.parser.parse(t).timestamp()
+
+            timestamps = []
+            if 'timestamp' in vcpu:
+                timestamps.append(convert_timestamp(vcpu['timestamp']))
+            if 'timestamp' in memory:
+                timestamps.append(convert_timestamp(memory['timestamp']))
+            if 'timestamp' in storage:
+                timestamps.append(convert_timestamp(storage['timestamp']))
+
+            metrics.timestamp = max(timestamps) if timestamps else 0.0
+
+            return metrics
+
+        except Exception as e:
+            logger.exception(e)
+
+    @rwstatus(ret_on_failure=[None])
+    def do_nfvi_vcpu_metrics(self, account, vm_id):
+        try:
+            samples = self._get_driver(account).ceilo_nfvi_metrics(vm_id)
+
+            metrics = RwmonYang.NfviMetrics_Vcpu()
+            metrics.utilization = samples.get("cpu_util", 0)
+
+            return metrics
+
+        except Exception as e:
+            logger.exception(e)
+
+    @rwstatus(ret_on_failure=[None])
+    def do_nfvi_memory_metrics(self, account, vm_id):
+        try:
+            samples = self._get_driver(account).ceilo_nfvi_metrics(vm_id)
+
+            metrics = RwmonYang.NfviMetrics_Memory()
+            metrics.used = samples.get("memory_usage", 0)
+
+            return metrics
+
+        except Exception as e:
+            logger.exception(e)
+
+    @rwstatus(ret_on_failure=[None])
+    def do_nfvi_storage_metrics(self, account, vm_id):
+        try:
+            samples = self._get_driver(account).ceilo_nfvi_metrics(vm_id)
+
+            metrics = RwmonYang.NfviMetrics_Storage()
+            metrics.used = samples.get("disk_usage", 0)
+
+            return metrics
+
+        except Exception as e:
+            logger.exception(e)
+
+    @rwstatus(ret_on_failure=[False])
+    def do_nfvi_metrics_available(self, account):
+        try:
+            endpoint = self._get_driver(account).ceilo_meter_endpoint()
+        except Exception:
+            return False
+
+        return endpoint is not None
+
+    @rwstatus(ret_on_failure=[None])
+    def do_alarm_create(self, account, vim_id, alarm):
+        # Retrieve a token using account information
+        token = openstack_auth_token(account)
+        service = token.service("ceilometer")
+        headers = {"content-type": "application/json", "x-auth-token": token.id}
+
+        # Convert the alarm from its YANG representation into something that
+        # can be passed to the openstack interface
+        ceilometer_alarm = CeilometerAlarm.from_gi_obj(alarm, vim_id).to_dict()
+
+        # POST the data to ceilometer
+        response = requests.post(
+                service.url.public + "/v2/alarms",
+                headers=headers,
+                data=json.dumps(ceilometer_alarm),
+                timeout=5,
+                )
+
+        # Returns the response object and update the alarm ID
+        obj = response.json()
+        alarm.alarm_id = obj['alarm_id']
+        return obj
+
+    @rwstatus(ret_on_failure=[None])
+    def do_alarm_update(self, account, alarm):
+        # Retrieve a token using account information
+        token = openstack_auth_token(account)
+        service = token.service("ceilometer")
+        headers = {"content-type": "application/json", "x-auth-token": token.id}
+
+        # Convert the alarm from its YANG representation into something that
+        # can be passed to the openstack interface
+        ceilometer_alarm = CeilometerAlarm.from_gi_obj(alarm).to_dict()
+
+        # PUT the data to ceilometer
+        response = requests.put(
+                service.url.public + "/v2/alarms/{}".format(alarm.alarm_id),
+                headers=headers,
+                data=json.dumps(ceilometer_alarm),
+                timeout=5,
+                )
+
+        return response.json()
+
+    @rwstatus(ret_on_failure=[None])
+    def do_alarm_delete(self, account, alarm_id):
+        # Retrieve a token using account information
+        token = openstack_auth_token(account)
+        service = token.service("ceilometer")
+        headers = {"content-type": "application/json", "x-auth-token": token.id}
+
+        # DELETE the alarm
+        _ = requests.delete(
+                service.url.public + "/v2/alarms/{}".format(alarm_id),
+                headers=headers,
+                timeout=5,
+                )
+
+    @rwstatus(ret_on_failure=[None])
+    def do_alarm_list(self, account):
+        # Retrieve a token using account information
+        token = openstack_auth_token(account)
+        service = token.service("ceilometer")
+        headers = {"x-auth-token": token.id}
+
+        # GET a list of alarms
+        response = requests.get(
+                service.url.public + "/v2/alarms",
+                headers=headers,
+                timeout=5,
+                )
+
+        return response.json()
+
+
+class OpenstackAuthTokenV2(object):
+    def __init__(self, data):
+        self._data = data
+
+    @classmethod
+    def request(cls, account):
+        """Create an OpenstackAuthTokenV2 using account information
+
+        Arguments:
+            account - an RwcalYang.CloudAccount object
+
+        Returns:
+            an openstack token
+
+        """
+        headers = {"content-type": "application/json"}
+        data = json.dumps({
+            "auth": {
+                "tenantName": account.openstack.tenant,
+                "passwordCredentials": {
+                    "username": account.openstack.key,
+                    "password": account.openstack.secret,
+                    }
+                }
+            })
+
+        url = "{}/tokens".format(account.openstack.auth_url)
+        response = requests.post(url, headers=headers, data=data)
+        response.raise_for_status()
+
+        return cls(response.json())
+
+    @property
+    def id(self):
+        """The token identifier"""
+        return self._data["access"]["token"]["id"]
+
+    def service(self, name):
+        """Returns information about the specified service
+
+        Arguments:
+            name - the name of the service to return
+
+        Raises:
+            If the requested service cannot be found, an UnknownService
+            exception is raised.
+
+        Returns:
+            an OpenstackService object
+
+        """
+        for s in self._data["access"]["serviceCatalog"]:
+            if s["name"] == name:
+                return OpenstackService(
+                        name=name,
+                        url=OpenstackServiceURLs(
+                            public=s["endpoints"][0]["publicURL"],
+                            internal=s["endpoints"][0]["internalURL"],
+                            admin=s["endpoints"][0]["adminURL"],
+                            )
+                        )
+
+        raise UnknownService(name)
+
+
+class OpenstackAuthTokenV3(object):
+    def __init__(self, token, data):
+        self._data = data
+        self._token = token
+
+    @classmethod
+    def request(cls, account):
+        """Create an OpenstackAuthTokenV3 using account information
+
+        Arguments:
+            account - an RwcalYang.CloudAccount object
+
+        Returns:
+            an openstack token
+
+        """
+        headers = {"content-type": "application/json"}
+        data = json.dumps({
+            "auth": {
+                "identity": {
+                    "methods": ["password"],
+                    "password": {
+                        "user": {
+                            "name": account.openstack.key,
+                            "password": account.openstack.secret,
+                            "domain": {"id": "default"},
+                            }
+                        }
+                    },
+                "scope": {
+                    "project": {
+                        "name": account.openstack.tenant,
+                        "domain": {"id": "default"},
+                        }
+                    }
+                }
+            })
+
+        url = account.openstack.auth_url + "/auth/tokens"
+
+        response = requests.post(url, headers=headers, data=data)
+        response.raise_for_status()
+
+        return cls(response.headers['x-subject-token'], response.json())
+
+    @property
+    def id(self):
+        """The token identifier"""
+        return self._token
+
+    def service(self, name):
+        """Returns information about the specified service
+
+        Arguments:
+            name - the name of the service to return
+
+        Raises:
+            If the requested service cannot be found, an UnknownService
+            exception is raised.
+
+        Returns:
+            an OpenstackService object
+
+        """
+        for s in self._data["token"]["catalog"]:
+            if s["name"] == name:
+                endpoints = {e["interface"]:e["url"] for e in s["endpoints"]}
+                return OpenstackService(
+                        name=name,
+                        url=OpenstackServiceURLs(
+                            public=endpoints["public"],
+                            internal=endpoints["internal"],
+                            admin=endpoints["admin"],
+                            )
+                        )
+
+        raise UnknownService(name)
+
+
+def openstack_auth_token(account):
+    url = urllib.parse.urlparse(account.openstack.auth_url)
+
+    if url.path in ('/v3',):
+        return OpenstackAuthTokenV3.request(account)
+
+    if url.path in ('/v2.0', 'v2.1'):
+        return OpenstackAuthTokenV2.request(account)
+
+    raise ValueError("Unrecognized keystone version")
+
+
+class OpenstackService(collections.namedtuple(
+    "OpenstackServer",
+    "name url")):
+    pass
+
+
+class OpenstackServiceURLs(collections.namedtuple(
+    "OpenstackServiceURLs",
+    "public internal admin")):
+    pass
+
+
+class CeilometerAlarm(collections.namedtuple(
+    "CeilometerAlarm",
+    "name type description severity repeat_actions enabled alarm_actions ok_actions insufficient_data_actions threshold_rule")):
+    @classmethod
+    def from_gi_obj(cls, alarm, vim_id=None):
+        severity = CeilometerAlarmSeverity.from_gi_obj(alarm.severity).severity
+        actions = CeilometerAlarmActions.from_gi_obj(alarm.actions)
+
+        alarm_id = alarm.alarm_id if vim_id is None else vim_id
+        threshold_rule = CeilometerThresholdRule.from_gi_obj(alarm_id, alarm)
+
+        return cls(
+                type="threshold",
+                name=alarm.name,
+                description=alarm.description,
+                severity=severity,
+                repeat_actions=alarm.repeat,
+                enabled=alarm.enabled,
+                threshold_rule=threshold_rule,
+                ok_actions=actions.ok,
+                alarm_actions=actions.alarm,
+                insufficient_data_actions=actions.insufficient_data,
+                )
+
+    def to_dict(self):
+        """Returns a dictionary containing the tuple data"""
+        def recursive_to_dict(obj):
+            if not hasattr(obj, '_fields'):
+                return obj
+
+            return {k: recursive_to_dict(getattr(obj, k)) for k in obj._fields}
+
+        return recursive_to_dict(self)
+
+
+class CeilometerThresholdRule(collections.namedtuple(
+    "CeilometerThresholdRule",
+    "evaluation_periods threshold statistic meter_name comparison_operator period query")):
+    @classmethod
+    def from_gi_obj(cls, vim_id, alarm):
+        meter = CeilometerAlarmMeter.from_gi_obj(alarm.metric).meter
+        statistic = CeilometerAlarmStatistic.from_gi_obj(alarm.statistic).statistic
+        operation = CeilometerAlarmOperation.from_gi_obj(alarm.operation).operation
+
+        return cls(
+                evaluation_periods=alarm.evaluations,
+                threshold=alarm.value,
+                statistic=statistic,
+                meter_name=meter,
+                comparison_operator=operation,
+                period=alarm.period,
+                query=[{
+                    "op": "eq",
+                    "field": "resource_id",
+                    "value": vim_id,
+                    }]
+                )
+
+    def to_dict(self):
+        """Returns a dictionary containing the tuple data"""
+        def recursive_to_dict(obj):
+            if not hasattr(obj, '_fields'):
+                return obj
+
+            return {k: recursive_to_dict(getattr(obj, k)) for k in obj._fields}
+
+        return recursive_to_dict(self)
+
+
+class CeilometerAlarmMeter(collections.namedtuple(
+    "CeiloemterAlarmMeter",
+    "meter")):
+    __mapping__ = {
+            "CPU_UTILIZATION" : "cpu_util",
+            "MEMORY_UTILIZATION" : "memory_usage",
+            "STORAGE_UTILIZATION" : "disk_usage",
+            }
+    @classmethod
+    def from_gi_obj(cls, meter):
+        return cls(meter=cls.__mapping__[meter])
+
+
+class CeilometerAlarmStatistic(collections.namedtuple(
+    "CeilometerAlarmStatistic",
+    "statistic")):
+    __mapping__ = {
+            "AVERAGE": "avg",
+            "MINIMUM": "min",
+            "MAXIMUM": "max",
+            "COUNT": "count",
+            "SUM": "sum",
+            }
+    @classmethod
+    def from_gi_obj(cls, statistic):
+        return cls(statistic=cls.__mapping__[statistic])
+
+
+class CeilometerAlarmOperation(collections.namedtuple(
+    "CeilometerAlarmOperation",
+    "operation")):
+    __mapping__ = {
+            "LT": "lt",
+            "LE": "le",
+            "EQ": "eq",
+            "GE": "ge",
+            "GT": "gt",
+            }
+    @classmethod
+    def from_gi_obj(cls, operation):
+        return cls(operation=cls.__mapping__[operation])
+
+
+class CeilometerAlarmSeverity(collections.namedtuple(
+    "CeilometerAlarmSeverity",
+    "severity")):
+    __mapping__ = {
+            "LOW": "low",
+            "MODERATE": "moderate",
+            "CRITICAL": "critical",
+            }
+    @classmethod
+    def from_gi_obj(cls, severity):
+        return cls(severity=cls.__mapping__[severity])
+
+
+class CeilometerAlarmActions(collections.namedtuple(
+    "CeilometerAlarmActions",
+    "ok alarm insufficient_data")):
+    @classmethod
+    def from_gi_obj(cls, actions):
+        return cls(
+            ok=[obj.url for obj in actions.ok],
+            alarm=[obj.url for obj in actions.alarm],
+            insufficient_data=[obj.url for obj in actions.insufficient_data],
+            )