From: hoban Date: Fri, 1 Sep 2017 15:14:40 +0000 (+0200) Subject: Merge "Initial CloudWatch-boto plugin for MON" X-Git-Tag: v4.0.0~94 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=f6064437ba352d7fee6b4a7a4e7cb2582ef5cd32;hp=a14a2ab77f2e67a92a4d3c05cca374d8643e5b98;p=osm%2FMON.git Merge "Initial CloudWatch-boto plugin for MON" --- diff --git a/core/message-bus/northbound_producer.py b/core/message-bus/northbound_producer.py index 4c304a0..d0e96bc 100644 --- a/core/message-bus/northbound_producer.py +++ b/core/message-bus/northbound_producer.py @@ -2,32 +2,38 @@ from kafka import KafkaProducer from kafka.errors import KafkaError import logging import json +import os class KafkaProducer(object): - def __init__(self): - if not cfg.CONF.kafka.uri: - raise Exception("Kafka URI Not Found. Check the config file for Kafka URI") - else: - broker = cfg.CONF.kafka.uri - producer = KafkaProducer(key_serializer=str.encode, value_serializer=lambda v: json.dumps(v).encode('ascii'), bootstrap_servers='localhost:9092', api_version=(0,10)) + def __init__(self, topic, message): - def publish(self, topic, messages): + self._topic= topic + self._message = message - """Takes messages and puts them on the supplied kafka topic - Memory Usage is used as a test value for alarm creation for - topic 'alarms'. 'Configure_alarm' is the key, resource UUID - and type of alarm are the list of values. + if "ZOOKEEPER_URI" in os.environ: + broker = os.getenv("ZOOKEEPER_URI") + else: + broker = "SO-ub.lxd:2181" - """ + ''' + If the zookeeper broker URI is not set in the env, by default, + SO-ub.lxd container is taken as the host because an instance of + is already running. + ''' - payload = {'configure_alarm': ['memory_usage']} + producer = KafkaProducer(key_serializer=str.encode, + value_serializer=lambda v: json.dumps(v).encode('ascii'), + bootstrap_servers=broker, api_version=(0,10)) + + + def publish(self, key, message, topic=None): try: - future = producer.send('alarms', payload) + future = producer.send('alarms', key, payload) producer.flush() except Exception: - log.exception('Error publishing to {} topic.'.format(topic)) + log.exception("Error publishing to {} topic." .format(topic)) raise try: record_metadata = future.get(timeout=10) @@ -36,5 +42,253 @@ class KafkaProducer(object): self._log.debug("OFFSET:", record_metadata.offset) except KafkaError: pass - #producer = KafkaProducer(retries=5) + def configure_alarm(self, key, message, topic): + + payload_configure = { + "alarm_configuration": + { + "schema_version": 1.0, + "schema_type": "configure_alarm", + "alarm_configuration": + { + "metric_name": { "type": "string" }, + "tenant_uuid": { "type": "string" }, + "resource_uuid": { "type": "string" }, + "description": { "type": "string" }, + "severity": { "type": "string" }, + "operation": { "type": "string" }, + "threshold_value": { "type": "integer" }, + "unit": { "type": "string" }, + "statistic": { "type": "string" } + }, + "required": [ "schema_version", + "schema_type", + "metric_name", + "resource_uuid", + "severity", + "operation", + "threshold_value", + "unit", + "statistic" ] + } + } + + publish(key, value=json.dumps(payload_configure), topic='alarms') + + def notify_alarm(self, key, message, topic): + + payload_notify = { + "notify_alarm": + { + "schema_version": 1.0, + "schema_type": "notify_alarm", + "notify_details": + { + "alarm_uuid": { "type": "string" }, + "resource_uuid": { "type": "string" }, + "description": { "type": "string" }, + "tenant_uuid": { "type": "string" }, + "severity": { "type" : ["integer", "string"] }, + "status": { "type": "string" }, + "start_date": { "type": "date-time" }, + "update_date": { "type": "date-time" }, + "cancel_date": { "type": "date-time" } + }, + "required": [ "schema_version", + "schema_type", + "alarm_uuid", + "resource_uuid", + "tenant_uuid", + "severity", + "status", + "start_date" ] + } + } + + publish(key, value=json.dumps(payload_notify), topic='alarms') + + def alarm_ack(self, key, message, topic): + + payload_ack = { + "alarm_ack": + { + "schema_version": 1.0, + "schema_type": "alarm_ack", + "ack_details": + { + "alarm_uuid": { "type": "string" }, + "tenant_uuid": { "type": "string" }, + "resource_uuid": { "type": "string" } + }, + "required": [ "schema_version", + "schema_type", + "alarm_uuid", + "tenant_uuid", + "resource_uuid" ] + } + } + + publish(key, value.json.dumps(payload_ack), topic='alarms') + + def configure_metrics(self, key, message, topic): + + payload_configure_metrics = { + "configure_metrics": + { + "schema_version": 1.0, + "schema_type": "configure_metrics", + "tenant_uuid": { "type": "string" }, + "metrics_configuration": + { + "metric_name": { "type": "string" }, + "metric_unit": { "type": "string" }, + "resource_uuid": { "type": "string" } + }, + "required": [ "schema_version", + "schema_type", + "metric_name", + "metric_unit", + "resource_uuid" ] + } + } + + publish(key, value.json.dumps(payload_configure_metrics), topic='metrics') + + def metric_data_request(self, key, message, topic): + + payload_metric_data_request = { + "metric_data_request": + { + "schema_version": 1.0, + "schema_type": "metric_data_request", + "metric_name": { "type": "string" }, + "resource_uuid": { "type": "string" }, + "tenant_uuid": { "type": "string" }, + "collection_period": { "type": "string" } + }, + "required": ["schema_version", + "schema_type", + "tenant_uuid", + "metric_name", + "collection_period", + "resource_uuid"] + } + + publish(key, value.json.dumps(payload_metric_data_request), topic='metrics') + + def metric_data_response(self, key, message, topic): + + payload_metric_data_response = { + "metric_data_response": + { + "schema_version": 1.0, + "schema_type": "metric_data_response", + "metrics_name": { "type": "string" }, + "resource_uuid": { "type": "string" }, + "tenant_uuid": { "type": "string" }, + "metrics_data": + { + "time_series": { "type": "array" }, + "metrics_series": { "type": "array" } + } + }, + "required": [ "schema_version", + "schema_type", + "metrics_name", + "resource_uuid", + "tenant_uuid", + "time_series", + "metrics_series" ] + } + + publish(key, value.json.dumps(payload_metric_data_response), topic='metrics') + + def access_credentials(self, key, message, topic): + + payload_access_credentials = { + "access_credentials": + { + "schema_version": 1.0, + "schema_type": "vim_access_credentials", + "vim_type": { "type": "string" }, + "required": [ "schema_version", + "schema_type", + "vim_type" ], + "access_config": + { + "if": + { + "vim_type": "openstack" + }, + "then": + { + "openstack-site": { "type": "string" }, + "user": { "type": "string" }, + "password": { "type": "string", + "options": { "hidden": true }}, + "vim_tenant_name": { "type": "string" } + }, + "required": [ "openstack_site", + "user", + "password", + "vim_tenant_name" ], + "else": + { + "vim_type": "aws" + }, + "then": + { + "aws_site": { "type": "string" }, + "user": { "type": "string" }, + "password": { "type": "string", + "options": { "hidden": true }}, + "vim_tenant_name": { "type": "string" } + }, + "required": [ "aws_site", + "user", + "password", + "vim_tenant_name" ], + "else": + { + "vim_type": "VMWare" + }, + "then": + { + "vrops_site": { "type": "string" }, + "vrops_user": { "type": "string" }, + "vrops_password": { "type": "string", + "options": { "hidden": true }}, + "vcloud_site": { "type": "string" }, + "admin_username": { "type": "string" }, + "admin_password": { "type": "string", + "options": { "hidden": true }}, + "nsx_manager": { "type": "string" }, + "nsx_user": { "type": "string" }, + "nsx_password": { "type": "string", + "options": { "hidden": true }}, + "vcenter_ip": { "type": "string" }, + "vcenter_port": { "type": "string" }, + "vcenter_user": { "type": "string" }, + "vcenter_password": { "type": "string", + "options": { "hidden": true }}, + "vim_tenant_name": { "type": "string" }, + "org_name": { "type": "string" } + }, + "required": [ "vrops_site", + "vrops_user", + "vrops_password", + "vcloud_site", + "admin_username", + "admin_password", + "vcenter_ip", + "vcenter_port", + "vcenter_user", + "vcenter_password", + "vim_tenant_name", + "orgname" ] + } + } + } + + publish(key, value.json.dumps(payload_access_credentials), topic='access_credentials') diff --git a/plugins/OpenStack/Ceilometer/.gitkeep b/plugins/OpenStack/Ceilometer/.gitkeep deleted file mode 100644 index 2272ebb..0000000 --- a/plugins/OpenStack/Ceilometer/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -#gitkeep file to keep the initial empty directory structure. diff --git a/plugins/OpenStack/Gnocchi/.gitkeep b/plugins/OpenStack/Gnocchi/.gitkeep new file mode 100644 index 0000000..2272ebb --- /dev/null +++ b/plugins/OpenStack/Gnocchi/.gitkeep @@ -0,0 +1 @@ +#gitkeep file to keep the initial empty directory structure. diff --git a/plugins/OpenStack/Gnocchi/__init__.py b/plugins/OpenStack/Gnocchi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/OpenStack/Gnocchi/metrics.py b/plugins/OpenStack/Gnocchi/metrics.py new file mode 100644 index 0000000..bc1a729 --- /dev/null +++ b/plugins/OpenStack/Gnocchi/metrics.py @@ -0,0 +1,116 @@ +"""Gnocchi acts on a metric message received from the SO via MON.""" + +import json +import logging as log + +from kafka import KafkaConsumer + +from plugins.OpenStack.common import Common + + +class Metrics(object): + """Gnocchi based metric actions performed on info from MON.""" + + def __init__(self): + """Initialize the metric actions.""" + self._common = Common() + + # TODO(mcgoughh): Initialize a generic consumer object to consume + # message from the SO. This is hardcoded for now + server = {'server': 'localhost:9092', 'topic': 'metrics'} + self._consumer = KafkaConsumer(server['topic'], + group_id='my-group', + bootstrap_servers=server['server']) + + # TODO(mcgoughh): Initialize a producer to send messages bask to the SO + + def metric_calls(self): + """Consume info from the message bus to manage metrics.""" + # Concumer check for metric messages + for message in self._consumer: + + if message.topic == "metrics": + log.info("Metric action required on this topic: %s", + (message.topic)) + + if message.key == "configure_metric": + # Configure/Update a resource and its metric + values = json.loads(message.value) + schema = values['configure_metrics'] + metric_details = schema['metrics_configuration'] + + # Generate authentication credentials via keystone: + # auth_token, endpoint + auth_token = self._common._authenticate( + schema['tenant_uuid']) + endpoint = self._common.get_endpoint("metric") + + metric_id = self.configure_metric( + endpoint, auth_token, metric_details) + log.info("New metric created with metricID: %s", metric_id) + + # TODO(mcgoughh): will send an acknowledge message back on + # the bus via the producer + + # TODO(mcoughh): Key alternatives are "metric_data_request" and + # "metric_data_response" will be accomodated later + # Will also need a producer for this functionality + elif message.key == "metric_data_request": + log.debug("Key used to request a metrics data") + + elif message.key == "metric_data_response": + log.debug("Key used for a metrics data response") + + else: + log.debug("Unknown key, no action will be performed") + + else: + log.info("Message topic not relevant to this plugin: %s", + message.topic) + + return + + def configure_metric(self, endpoint, auth_token, values): + """Create the new SO desired metric in Gnocchi.""" + metric_id = None + + # TODO(mcgoughh): error check the values sent in the message + # will query the database for the request resource and then + # check that resource for the desired metric + metric_name = values['metric_name'] + + if metric_id is None: + + # Need to create a new version of the resource for gnocchi to + # the new metric + resource_url = "{}/v1/resource/generic".format(endpoint) + + metric = {'name': metric_name, + 'unit': values['metric_unit'], } + + resource_payload = json.dumps({'id': values['resource_uuid'], + 'metrics': {metric_name: metric}}) + + new_resource = self._common._perform_request( + resource_url, auth_token, + req_type="post", payload=resource_payload) + new_metric = json.loads(new_resource.text)['metrics'] + + return new_metric[metric_name] + else: + return metric_id + + def delete_metric(self, endpoint, auth_token, metric_id): + """Delete metric.""" + url = "{}/v1/metric/%s".format(endpoint) % (metric_id) + + self._common._perform_request(url, auth_token, req_type="delete") + return None + + def list_metrics(self, endpoint, auth_token): + """List all metrics.""" + url = "{}/v1/metric/".format(endpoint) + + metric_list = self._common._perform_request( + url, auth_token, req_type="get") + return json.loads(metric_list.text) diff --git a/plugins/OpenStack/Gnocchi/plugin_instance.py b/plugins/OpenStack/Gnocchi/plugin_instance.py new file mode 100644 index 0000000..6f9e306 --- /dev/null +++ b/plugins/OpenStack/Gnocchi/plugin_instance.py @@ -0,0 +1,36 @@ +"""Gnocchi plugin for the OSM monitoring module.""" + +import logging as log + +from plugins.OpenStack.Gnocchi.metrics import Metrics +from plugins.OpenStack.settings import Config + + +def register_plugin(): + """Register the plugin.""" + config = Config.instance() + instance = Plugin(config=config) + instance.config() + instance.metrics() + + +class Plugin(object): + """Gnocchi plugin for OSM MON.""" + + def __init__(self, config): + """Plugin instance.""" + log.info("Initialze the plugin instance.") + self._config = config + self._metrics = Metrics() + + def config(self): + """Configure plugin.""" + log.info("Configure the plugin instance.") + self._config.read_environ("gnocchi") + + def metrics(self): + """Initialize metric functionality.""" + log.info("Initialize metric functionality.") + self._metrics.metric_calls() + +register_plugin() diff --git a/plugins/OpenStack/__init__.py b/plugins/OpenStack/__init__.py index 805dce3..e69de29 100644 Binary files a/plugins/OpenStack/__init__.py and b/plugins/OpenStack/__init__.py differ diff --git a/plugins/OpenStack/common.py b/plugins/OpenStack/common.py new file mode 100644 index 0000000..d706456 --- /dev/null +++ b/plugins/OpenStack/common.py @@ -0,0 +1,86 @@ +"""Common methods for the Aodh Sender/Receiver.""" + +import logging as log + +from keystoneclient.v3 import client + +from plugins.OpenStack.settings import Config + +import requests + +# from keystoneauth1.identity.v3 import AuthMethod +# from keystoneclient.service_catalog import ServiceCatalog + + +class Common(object): + """Common calls for Gnocchi/Aodh plugins.""" + + def __init__(self): + """Create the common instance.""" + self._auth_token = None + self._endpoint = None + self._ks = None + + def _authenticate(self, tenant_id): + """Authenticate and/or renew the authentication token.""" + if self._auth_token is not None: + return self._auth_token + + try: + cfg = Config.instance() + self._ks = client.Client(auth_url=cfg.OS_AUTH_URL, + username=cfg.OS_USERNAME, + password=cfg.OS_PASSWORD, + tenant_name=cfg.OS_TENANT_NAME) + self._auth_token = self._ks.auth_token + except Exception as exc: + + log.warn("Authentication failed with the following exception: %s", + exc) + self._auth_token = None + + return self._auth_token + + def get_endpoint(self, service_type): + """Get the endpoint for Gnocchi/Aodh.""" + try: + return self._ks.service_catalog.url_for( + service_type=service_type, + endpoint_type='internalURL', + region_name='RegionOne') + except Exception as exc: + log.warning("Failed to retreive endpoint for Aodh due to: %s", + exc) + return None + + @classmethod + def _perform_request(cls, url, auth_token, + req_type=None, payload=None, params=None): + """Perform the POST/PUT/GET/DELETE request.""" + # request headers + headers = {'X-Auth-Token': auth_token, + 'Content-type': 'application/json'} + # perform request and return its result + response = None + try: + if req_type == "put": + response = requests.put( + url, data=payload, headers=headers, + timeout=1) + elif req_type == "post": + response = requests.post( + url, data=payload, headers=headers, + timeout=1) + elif req_type == "get": + response = requests.get( + url, params=params, headers=headers, timeout=1) + elif req_type == "delete": + response = requests.delete( + url, headers=headers, timeout=1) + else: + log.warn("Invalid request type") + + except Exception as e: + log.warn("Exception thrown on request", e) + + return response diff --git a/plugins/vRealiseOps/mon_plugin_vrops.py b/plugins/vRealiseOps/mon_plugin_vrops.py new file mode 100644 index 0000000..a65d01c --- /dev/null +++ b/plugins/vRealiseOps/mon_plugin_vrops.py @@ -0,0 +1,580 @@ +# -*- coding: utf-8 -*- +""" +Montoring metrics & creating Alarm definations in vROPs +""" + +import requests +import logging as log +from pyvcloud.vcloudair import VCA +from xml.etree import ElementTree as XmlElementTree +import traceback + +OPERATION_MAPPING = {'GE':'GT_EQ', 'LE':'LT_EQ', 'GT':'GT', 'LT':'LT', 'EQ':'EQ'} +SEVERITY_MAPPING = {'WARNING':'WARNING', 'MINOR':'WARNING', 'MAJOR':"IMMEDIATE", 'CRITICAL':'CRITICAL'} + + +class MonPlugin(): + """MON Plugin class for vROPs telemetry plugin + """ + def __init__(self, access_config={}): + """Constructor of MON plugin + Params: + 'access_config': dictionary with VIM access information based on VIM type. + This contains a consolidate version of VIM & monitoring tool config at creation and + particular VIM config at their attachment. + For VIM type: 'vmware', access_config - {'vrops_site':<>, 'vrops_user':<>, 'vrops_password':<>, + 'vcloud-site':<>,'admin_username':<>,'admin_password':<>, + 'nsx_manager':<>,'nsx_user':<>,'nsx_password':<>, + 'vcenter_ip':<>,'vcenter_port':<>,'vcenter_user':<>,'vcenter_password':<>, + 'vim_tenant_name':<>,'orgname':<>} + + #To Do + Returns: Raise an exception if some needed parameter is missing, but it must not do any connectivity + check against the VIM + """ + self.access_config = access_config + self.vrops_site = access_config['vrops_site'] + self.vrops_user = access_config['vrops_user'] + self.vrops_password = access_config['vrops_password'] + self.vcloud_site = access_config['vcloud_site'] + self.vcloud_username = access_config['vcloud_username'] + self.vcloud_password = access_config['vcloud_password'] + + def configure_alarm(self, config_dict = {}): + """Configures or creates a new alarm using the input parameters in config_dict + Params: + "alarm_name": Alarm name in string format + "description": Description of alarm in string format + "resource_uuid": Resource UUID for which alarm needs to be configured. in string format + "Resource type": String resource type: 'VDU' or 'host' + "Severity": 'WARNING', 'MINOR', 'MAJOR', 'CRITICAL' + "metric": Metric key in string format + "operation": One of ('GE', 'LE', 'GT', 'LT', 'EQ') + "threshold_value": Defines the threshold (up to 2 fraction digits) that, if crossed, will trigger the alarm. + "unit": Unit of measurement in string format + "statistic": AVERAGE, MINIMUM, MAXIMUM, COUNT, SUM + + Default parameters for each alarm are read from the plugin specific config file. + Dict of default parameters is as follows: + default_params keys = {'cancel_cycles','wait_cycles','resource_kind','adapter_kind','alarm_type','alarm_subType',impact} + + Returns the UUID of created alarm or None + """ + alarm_def_uuid = None + #1) get alarm & metrics parameters from plugin specific file + def_params = self.get_default_Params(config_dict['alarm_name']) + metric_key_params = self.get_default_Params(config_dict['metric']) + #2) create symptom definition + #TO DO - 'metric_key':config_dict['metric'] - mapping from file def_params + symptom_params ={'cancel_cycles': (def_params['cancel_period']/300)*def_params['cancel_cycles'], + 'wait_cycles': (def_params['period']/300)*def_params['evaluation'], + 'resource_kind_key': def_params['resource_kind'],'adapter_kind_key': def_params['adapter_kind'], + 'symptom_name':config_dict['alarm_name'],'severity': SEVERITY_MAPPING[config_dict['severity']], + 'metric_key':metric_key_params['metric_key'],'operation':OPERATION_MAPPING[config_dict['operation']], + 'threshold_value':config_dict['threshold_value']} + symptom_uuid = self.create_symptom(symptom_params) + if symptom_uuid is not None: + log.info("Symptom defined: {} with ID: {}".format(symptom_params['symptom_name'],symptom_uuid)) + else: + log.warn("Failed to create Symptom: {}".format(symptom_params['symptom_name'])) + return None + #3) create alert definition + #To Do - Get type & subtypes for all 5 alarms + alarm_params = {'name':config_dict['alarm_name'], + 'description':config_dict['description'] if config_dict['description'] is not None else config_dict['alarm_name'], + 'adapterKindKey':def_params['adapter_kind'], 'resourceKindKey':def_params['resource_kind'], + 'waitCycles':1, 'cancelCycles':1, + 'type':def_params['alarm_type'], 'subType':def_params['alarm_subType'], + 'severity':SEVERITY_MAPPING[config_dict['severity']], + 'symptomDefinitionId':symptom_uuid, + 'impact':def_params['impact']} + + alarm_def_uuid = self.create_alarm_definition(alarm_params) + if alarm_def_uuid is None: + log.warn("Failed to create Alert: {}".format(alarm_params['name'])) + return None + + log.info("Alarm defined: {} with ID: {}".format(alarm_params['name'],alarm_def_uuid)) + + #4) Find vm_moref_id from vApp uuid in vCD + vm_moref_id = self.get_vm_moref_id(config_dict['resource_uuid']) + if vm_moref_id is None: + log.warn("Failed to find vm morefid for vApp in vCD: {}".format(config_dict['resource_uuid'])) + return None + + #5) Based on vm_moref_id, find VM's corresponding resource_id in vROPs to set notification + resource_id = self.get_vm_resource_id(vm_moref_id) + if resource_id is None: + log.warn("Failed to find resource in vROPs: {}".format(config_dict['resource_uuid'])) + return None + + #6) Configure alarm notification for a particular VM using it's resource_id + notification_id = self.create_alarm_notification(config_dict['alarm_name'], alarm_def_uuid, resource_id) + if notification_id is None: + return None + else: + log.info("Alarm defination created with notification: {} with ID: {}".format(alarm_params['name'],alarm_def_uuid)) + return alarm_def_uuid + + def get_default_Params(self, metric_alarm_name): + """ + Read the default config parameters from plugin specific file stored with plugin file. + Params: + metric_alarm_name: Name of the alarm, whose congif params to be read from the config file. + """ + tree = XmlElementTree.parse('vROPs_default_config.xml') + alarms = tree.getroot() + a_params = {} + for alarm in alarms: + if alarm.tag == metric_alarm_name: + for param in alarm: + if param.tag in ("period", "evaluation", "cancel_period", "alarm_type", "cancel_cycles", "alarm_subType"): + a_params[param.tag] = int(param.text) + elif param.tag in ("enabled", "repeat"): + if(param.text == "True" or param.text == "true"): + a_params[param.tag] = True + else: + a_params[param.tag] = False + else: + a_params[param.tag] = param.text + + if not a_params: + log.warn("No such '{}' alarm found!.".format(alarm)) + + return a_params + + + def create_symptom(self, symptom_params): + """Create Symptom definition for an alarm + Params: + symptom_params: Dict of parameters required for defining a symptom as follows + cancel_cycles + wait_cycles + resource_kind_key = "VirtualMachine" + adapter_kind_key = "VMWARE" + symptom_name = Test_Memory_Usage_TooHigh + severity + metric_key + operation = GT_EQ + threshold_value = 85 + Returns the uuid of Symptom definition + """ + symptom_id = None + + try: + api_url = '/suite-api/api/symptomdefinitions' + headers = {'Content-Type': 'application/xml'} + data = """ + + {4:s} + + + {6:s} + {7:s} + {8:s} + NUMERIC + false + STATIC + + + """.format(str(symptom_params['cancel_cycles']),str(symptom_params['wait_cycles']),symptom_params['resource_kind_key'], + symptom_params['adapter_kind_key'],symptom_params['symptom_name'],symptom_params['severity'], + symptom_params['metric_key'],symptom_params['operation'],str(symptom_params['threshold_value'])) + + resp = requests.post(self.vrops_site + api_url, + auth=(self.vrops_user, self.vrops_password), + headers=headers, + verify = False, + data=data) + + if resp.status_code != 201: + log.warn("Failed to create Symptom definition: {}, response {}".format(symptom_params['symptom_name'], resp.content)) + return None + + symptom_xmlroot = XmlElementTree.fromstring(resp.content) + if symptom_xmlroot is not None and 'id' in symptom_xmlroot.attrib: + symptom_id = symptom_xmlroot.attrib['id'] + + return symptom_id + + except Exception as exp: + log.warn("Error creating symptom definition : {}\n{}".format(exp, traceback.format_exc())) + + + def create_alarm_definition(self, alarm_params): + """ + Create an alarm definition in vROPs + Params: + 'name': Alarm Name, + 'description':Alarm description, + 'adapterKindKey': Adapter type in vROPs "VMWARE", + 'resourceKindKey':Resource type in vROPs "VirtualMachine", + 'waitCycles': No of wait cycles, + 'cancelCycles': No of cancel cycles, + 'type': Alarm type: , + 'subType': Alarm subtype: , + 'severity': Severity in vROPs "CRITICAL", + 'symptomDefinitionId':symptom Definition uuid, + 'impact': impact 'risk' + Returns: + 'alarm_uuid': returns alarm uuid + """ + + alarm_uuid = None + + try: + api_url = '/suite-api/api/alertdefinitions' + headers = {'Content-Type': 'application/xml'} + data = """ + + {0:s} + {1:s} + {2:s} + {3:s} + 1 + 1 + {4:s} + {5:s} + + + + + {7:s} + + SELF + ALL + AND + + + BADGE + {8:s} + + + + """.format(alarm_params['name'],alarm_params['description'],alarm_params['adapterKindKey'], + alarm_params['resourceKindKey'],str(alarm_params['type']),str(alarm_params['subType']), + alarm_params['severity'],alarm_params['symptomDefinitionId'],alarm_params['impact']) + + resp = requests.post(self.vrops_site + api_url, + auth=(self.vrops_user, self.vrops_password), + headers=headers, + verify = False, + data=data) + + if resp.status_code != 201: + log.warn("Failed to create Alarm definition: {}, response {}".format(alarm_params['name'], resp.content)) + return None + + alarm_xmlroot = XmlElementTree.fromstring(resp.content) + for child in alarm_xmlroot: + if child.tag.split("}")[1] == 'id': + alarm_uuid = child.text + + return alarm_uuid + + except Exception as exp: + log.warn("Error creating alarm definition : {}\n{}".format(exp, traceback.format_exc())) + + + def configure_rest_plugin(self, plugin_name, webhook_url, certificate): + """ + Creates REST Plug-in for vROPs outbound alerts + + Params: + plugin_name: name of REST plugin instance + pluginTypeId - RestPlugin + Optional configValues + "Url">https://dev14136.service-now.com:8080 - reqd + "Username">admin - optional + "Password">VMware1! - optional + "Content-type">application/xml - reqd + "Certificate">abcdefgh123456 - get n set + "ConnectionCount" - 20 - default + """ + plugin_id = None + + api_url = '/suite-api/api/alertplugins' + headers = {'Content-Type': 'application/xml'} + data = """ + + RestPlugin + {0:s} + + {1:s} + application/xml + {2:s} + 20 + + """.format(plugin_name, webhook_url, certificate) + + resp = requests.post(self.vrops_site + api_url, + auth=(self.vrops_user, self.vrops_password), + headers=headers, + verify = False, + data=data) + + if resp.status_code is not 201: + log.warn("Failed to create REST Plugin: {} for url: {}, \nresponse code: {},\nresponse content: {}"\ + .format(plugin_name, webhook_url, resp.status_code, resp.content)) + return None + + plugin_xmlroot = XmlElementTree.fromstring(resp.content) + if plugin_xmlroot is not None: + for child in plugin_xmlroot: + if child.tag.split("}")[1] == 'pluginId': + plugin_id = plugin_xmlroot.find('{http://webservice.vmware.com/vRealizeOpsMgr/1.0/}pluginId').text + + if plugin_id is None: + log.warn("Failed to get REST Plugin ID for {}, url: {}".format(plugin_name, webhook_url)) + return None + else: + log.info("Created REST Plugin: {} with ID : {} for url: {}".format(plugin_name, plugin_id, webhook_url)) + status = self.enable_rest_plugin(plugin_id, plugin_name) + if status is False: + log.warn("Failed to enable created REST Plugin: {} for url: {}".format(plugin_name, webhook_url)) + return None + else: + log.info("Enabled REST Plugin: {} for url: {}".format(plugin_name, webhook_url)) + return plugin_id + + + def enable_rest_plugin(self, plugin_id, plugin_name): + """ + Enable the REST plugin using plugin_id + Params: plugin_id: plugin ID string that is to be enabled + Returns: status (Boolean) - True for success, False for failure + """ + + if plugin_id is None or plugin_name is None: + log.debug("enable_rest_plugin() : Plugin ID or plugin_name not provided for {} plugin".format(plugin_name)) + return False + + try: + api_url = "/suite-api/api/alertplugins/{}/enable/True".format(plugin_id) + + resp = requests.put(self.vrops_site + api_url, auth=(self.vrops_user, self.vrops_password), verify = False) + + if resp.status_code is not 204: + log.warn("Failed to enable REST plugin {}. \nResponse code {}\nResponse Content: {}"\ + .format(plugin_name, resp.status_code, resp.content)) + return False + + log.info("Enabled REST plugin {}.".format(plugin_name)) + return True + + except Exception as exp: + log.warn("Error enabling REST plugin for {} plugin: Exception: {}\n{}".format(plugin_name, exp, traceback.format_exc())) + + def create_alarm_notification(self, alarm_name, alarm_id, resource_id): + """ + Create notification for each alarm + Params: + alarm_name + alarm_id + resource_id + + Returns: + notification_id: notification_id or None + """ + notification_name = 'notify_' + alarm_name + notification_id = None + + #1) Find the REST Plugin id details for - MON_module_REST_Plugin + api_url = '/suite-api/api/alertplugins' + headers = {'Accept': 'application/xml'} + namespace = {'params':"http://webservice.vmware.com/vRealizeOpsMgr/1.0/"} + + resp = requests.get(self.vrops_site + api_url, + auth=(self.vrops_user, self.vrops_password), + verify = False, headers = headers) + + if resp.status_code is not 200: + log.warn("Failed to REST GET Alarm plugin details \nResponse code: {}\nResponse content: {}"\ + .format(resp.status_code, resp.content)) + return None + + # Look for specific plugin & parse pluginId for 'MON_module_REST_Plugin' + xmlroot_resp = XmlElementTree.fromstring(resp.content) + for notify_plugin in xmlroot_resp.findall('params:notification-plugin',namespace): + if notify_plugin.find('params:name',namespace) is not None and notify_plugin.find('params:pluginId',namespace) is not None: + if notify_plugin.find('params:name',namespace).text == 'MON_module_REST_Plugin': + plugin_id = notify_plugin.find('params:pluginId',namespace).text + + if plugin_id is None: + log.warn("Failed to get REST plugin_id for : {}".format('MON_module_REST_Plugin')) + return None + + #2) Create Alarm notification rule + api_url = '/suite-api/api/notifications/rules' + headers = {'Content-Type': 'application/xml'} + data = """ + + {0:s} + {1:s} + + true + + + {3:s} + + """.format(notification_name, plugin_id, resource_id, alarm_id) + + resp = requests.post(self.vrops_site + api_url, + auth=(self.vrops_user, self.vrops_password), + headers=headers, + verify = False, + data=data) + + if resp.status_code is not 201: + log.warn("Failed to create Alarm notification {} for {} alarm. \nResponse code: {}\nResponse content: {}"\ + .format(notification_name, alarm_name, resp.status_code, resp.content)) + return None + + #parse notification id from response + xmlroot_resp = XmlElementTree.fromstring(resp.content) + if xmlroot_resp is not None and 'id' in xmlroot_resp.attrib: + notification_id = xmlroot_resp.attrib.get('id') + + log.info("Created Alarm notification rule {} for {} alarm.".format(notification_name, alarm_name)) + return notification_id + + def get_vm_moref_id(self, vapp_uuid): + """ + Get the moref_id of given VM + """ + try: + if vapp_uuid: + vm_details = self.get_vapp_details_rest(vapp_uuid) + if vm_details and "vm_vcenter_info" in vm_details: + vm_moref_id = vm_details["vm_vcenter_info"].get("vm_moref_id", None) + + log.info("Found vm_moref_id: {} for vApp UUID: {}".format(vm_moref_id, vapp_uuid)) + return vm_moref_id + + except Exception as exp: + log.warn("Error occurred while getting VM moref ID for VM : {}\n{}".format(exp, traceback.format_exc())) + + + def get_vapp_details_rest(self, vapp_uuid=None): + """ + Method retrieve vapp detail from vCloud director + + Args: + vapp_uuid - is vapp identifier. + + Returns: + Returns VM MOref ID or return None + """ + + parsed_respond = {} + vca = None + + vca = self.connect_as_admin() + + if not vca: + log.warn("connect() to vCD is failed") + if vapp_uuid is None: + return None + + url_list = [vca.host, '/api/vApp/vapp-', vapp_uuid] + get_vapp_restcall = ''.join(url_list) + + if vca.vcloud_session and vca.vcloud_session.organization: + response = requests.get(get_vapp_restcall, + headers=vca.vcloud_session.get_vcloud_headers(), + verify=vca.verify) + + if response.status_code != 200: + log.warn("REST API call {} failed. Return status code {}".format(get_vapp_restcall, response.content)) + return parsed_respond + + try: + xmlroot_respond = XmlElementTree.fromstring(response.content) + + namespaces = {'vm': 'http://www.vmware.com/vcloud/v1.5', + "vmext":"http://www.vmware.com/vcloud/extension/v1.5", + "xmlns":"http://www.vmware.com/vcloud/v1.5" + } + + # parse children section for other attrib + children_section = xmlroot_respond.find('vm:Children/', namespaces) + if children_section is not None: + vCloud_extension_section = children_section.find('xmlns:VCloudExtension', namespaces) + if vCloud_extension_section is not None: + vm_vcenter_info = {} + vim_info = vCloud_extension_section.find('vmext:VmVimInfo', namespaces) + vmext = vim_info.find('vmext:VmVimObjectRef', namespaces) + if vmext is not None: + vm_vcenter_info["vm_moref_id"] = vmext.find('vmext:MoRef', namespaces).text + parsed_respond["vm_vcenter_info"]= vm_vcenter_info + + except Exception as exp : + log.warn("Error occurred calling rest api for getting vApp details: {}\n{}".format(exp, traceback.format_exc())) + + return parsed_respond + + + def connect_as_admin(self): + """ Method connect as pvdc admin user to vCloud director. + There are certain action that can be done only by provider vdc admin user. + Organization creation / provider network creation etc. + + Returns: + The return vca object that letter can be used to connect to vcloud direct as admin for provider vdc + """ + + log.info("Logging in to a VCD org as admin.") + + vca_admin = VCA(host=self.vcloud_site, + username=self.vcloud_username, + service_type='standalone', + version='5.9', + verify=False, + log=False) + result = vca_admin.login(password=self.vcloud_password, org='System') + if not result: + log.warn("Can't connect to a vCloud director as: {}".format(self.vcloud_username)) + result = vca_admin.login(token=vca_admin.token, org='System', org_url=vca_admin.vcloud_session.org_url) + if result is True: + log.info("Successfully logged to a vcloud direct org: {} as user: {}".format('System', self.vcloud_username)) + + return vca_admin + + + def get_vm_resource_id(self, vm_moref_id): + """ Find resource ID in vROPs using vm_moref_id + """ + if vm_moref_id is None: + return None + + api_url = '/suite-api/api/resources' + headers = {'Accept': 'application/xml'} + namespace = {'params':"http://webservice.vmware.com/vRealizeOpsMgr/1.0/"} + + resp = requests.get(self.vrops_site + api_url, + auth=(self.vrops_user, self.vrops_password), + verify = False, headers = headers) + + if resp.status_code is not 200: + log.warn("Failed to get resource details from vROPs for {}\nResponse code:{}\nResponse Content: {}"\ + .format(vm_moref_id, resp.status_code, resp.content)) + return None + + try: + xmlroot_respond = XmlElementTree.fromstring(resp.content) + for resource in xmlroot_respond.findall('params:resource',namespace): + if resource is not None: + resource_key = resource.find('params:resourceKey',namespace) + if resource_key is not None: + if resource_key.find('params:adapterKindKey',namespace).text == 'VMWARE' and \ + resource_key.find('params:resourceKindKey',namespace).text == 'VirtualMachine': + for child in resource_key: + if child.tag.split('}')[1]=='resourceIdentifiers': + resourceIdentifiers = child + for r_id in resourceIdentifiers: + if r_id.find('params:value',namespace).text == vm_moref_id: + log.info("Found Resource ID : {} in vROPs for {}".format(resource.attrib['identifier'], vm_moref_id)) + return resource.attrib['identifier'] + except Exception as exp: + log.warn("Error in parsing {}\n{}".format(exp, traceback.format_exc())) + diff --git a/plugins/vRealiseOps/vrops_config.xml b/plugins/vRealiseOps/vrops_config.xml new file mode 100644 index 0000000..aa628d7 --- /dev/null +++ b/plugins/vRealiseOps/vrops_config.xml @@ -0,0 +1,122 @@ + + + + 300 + 2 + 300 + 2 + true + false + acknowledge + VirtualMachine + VMWARE + 16 + 19 + risk + % + + + 300 + 3 + 300 + 3 + true + false + acknowledge + VirtualMachine + VMWARE + 16 + 19 + risk + msec + + + 300 + 3 + 300 + 3 + true + false + acknowledge + VirtualMachine + VMWARE + 16 + 19 + risk + msec + + + 300 + 1 + 300 + 1 + true + false + acknowledge + VirtualMachine + VMWARE + 16 + 19 + risk + nos + + + 300 + 1 + 300 + 1 + true + false + acknowledge + VirtualMachine + VMWARE + 16 + 19 + risk + msec + + + mem|usage_average + % + + + cpu|usage_average + % + + + virtualDisk:scsi0:0|totalReadLatency_average + msec + + + virtualDisk:scsi0:0|totalWriteLatency_average + msec + + + virtualDisk:scsi0:1|totalReadLatency_average + msec + + + virtualDisk:scsi0:1|totalWriteLatency_average + msec + + + net:4000|dropped + nos + + + net:4001|dropped + nos + + + net:4002|dropped + nos + + + net:Aggregate of all instances|packetsRxPerSec + nos + + + net:Aggregate of all instances|packetsTxPerSec + nos + +