1 # Copyright 2017 Intel Research and Development Ireland Limited
2 # *************************************************************
4 # This file is part of OSM Monitoring module
5 # All Rights Reserved to Intel Corporation
7 # Licensed under the Apache License, Version 2.0 (the "License"); you may
8 # not use this file except in compliance with the License. You may obtain
9 # a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 # License for the specific language governing permissions and limitations
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact: helena.mcgough@intel.com or adrian.hoban@intel.com
22 """Carry out alarming requests via Aodh API."""
26 from json
import JSONDecodeError
31 from osm_mon
.core
.auth
import AuthManager
32 from osm_mon
.core
.database
import DatabaseManager
33 from osm_mon
.core
.message_bus
.producer
import KafkaProducer
34 from osm_mon
.core
.settings
import Config
35 from osm_mon
.plugins
.OpenStack
.Gnocchi
.metrics
import METRIC_MAPPINGS
36 from osm_mon
.plugins
.OpenStack
.common
import Common
37 from osm_mon
.plugins
.OpenStack
.response
import OpenStack_Response
39 log
= logging
.getLogger(__name__
)
45 "critical": "critical",
46 "indeterminate": "critical"}
56 class Alarming(object):
57 """Carries out alarming requests and responses via Aodh API."""
60 """Create the OpenStack alarming instance."""
61 self
._database
_manager
= DatabaseManager()
62 self
._auth
_manager
= AuthManager()
64 # Use the Response class to generate valid json response messages
65 self
._response
= OpenStack_Response()
67 # Initializer a producer to send responses back to SO
68 self
._producer
= KafkaProducer("alarm_response")
70 def alarming(self
, message
, vim_uuid
):
72 Processes alarm request message depending on it's key
73 :param message: Message containing key and value attributes. This last one can be in JSON or YAML format.
74 :param vim_uuid: UUID of the VIM to handle the alarm request.
78 values
= json
.loads(message
.value
)
79 except JSONDecodeError
:
80 values
= yaml
.safe_load(message
.value
)
82 log
.info("OpenStack alarm action required.")
84 verify_ssl
= self
._auth
_manager
.is_verify_ssl(vim_uuid
)
86 auth_token
= Common
.get_auth_token(vim_uuid
, verify_ssl
=verify_ssl
)
88 alarm_endpoint
= Common
.get_endpoint("alarming", vim_uuid
, verify_ssl
=verify_ssl
)
89 metric_endpoint
= Common
.get_endpoint("metric", vim_uuid
, verify_ssl
=verify_ssl
)
91 vim_account
= self
._auth
_manager
.get_credentials(vim_uuid
)
92 vim_config
= json
.loads(vim_account
.config
)
94 if message
.key
== "create_alarm_request":
95 alarm_details
= values
['alarm_create_request']
99 metric_name
= alarm_details
['metric_name'].lower()
100 resource_id
= alarm_details
['resource_uuid']
102 self
.check_for_metric(auth_token
, metric_endpoint
, metric_name
, resource_id
, verify_ssl
)
104 alarm_id
= self
.configure_alarm(
105 alarm_endpoint
, auth_token
, alarm_details
, vim_config
, verify_ssl
)
107 log
.info("Alarm successfully created")
108 self
._database
_manager
.save_alarm(alarm_id
,
110 alarm_details
['threshold_value'],
111 alarm_details
['operation'].lower(),
112 alarm_details
['metric_name'].lower(),
113 alarm_details
['vdu_name'].lower(),
114 alarm_details
['vnf_member_index'],
115 alarm_details
['ns_id'].lower()
118 except Exception as e
:
119 log
.exception("Error creating alarm")
122 self
._generate
_and
_send
_response
('create_alarm_response',
123 alarm_details
['correlation_id'],
127 elif message
.key
== "list_alarm_request":
128 list_details
= values
['alarm_list_request']
131 alarm_list
= self
.list_alarms(
132 alarm_endpoint
, auth_token
, list_details
, verify_ssl
)
133 except Exception as e
:
134 log
.exception("Error listing alarms")
137 self
._generate
_and
_send
_response
('list_alarm_response',
138 list_details
['correlation_id'],
139 alarm_list
=alarm_list
)
141 elif message
.key
== "delete_alarm_request":
142 request_details
= values
['alarm_delete_request']
143 alarm_id
= request_details
['alarm_uuid']
147 alarm_endpoint
, auth_token
, alarm_id
, verify_ssl
)
149 except Exception as e
:
150 log
.exception("Error deleting alarm")
153 self
._generate
_and
_send
_response
('delete_alarm_response',
154 request_details
['correlation_id'],
158 elif message
.key
== "acknowledge_alarm":
160 alarm_id
= values
['ack_details']['alarm_uuid']
162 self
.update_alarm_state(
163 alarm_endpoint
, auth_token
, alarm_id
, verify_ssl
)
165 log
.info("Acknowledged the alarm and cleared it.")
166 except Exception as e
:
167 log
.exception("Error acknowledging alarm")
170 elif message
.key
== "update_alarm_request":
171 # Update alarm configurations
172 alarm_details
= values
['alarm_update_request']
176 alarm_id
= self
.update_alarm(
177 alarm_endpoint
, auth_token
, alarm_details
, vim_config
, verify_ssl
)
179 except Exception as e
:
180 log
.exception("Error updating alarm")
183 self
._generate
_and
_send
_response
('update_alarm_response',
184 alarm_details
['correlation_id'],
189 log
.debug("Unknown key, no action will be performed")
191 def configure_alarm(self
, alarm_endpoint
, auth_token
, values
, vim_config
, verify_ssl
):
192 """Create requested alarm in Aodh."""
193 url
= "{}/v2/alarms/".format(alarm_endpoint
)
195 # Check if the desired alarm is supported
196 alarm_name
= values
['alarm_name'].lower()
197 metric_name
= values
['metric_name'].lower()
198 resource_id
= values
['resource_uuid']
200 if metric_name
not in METRIC_MAPPINGS
.keys():
201 raise KeyError("Metric {} is not supported.".format(metric_name
))
204 if 'granularity' in vim_config
and 'granularity' not in values
:
205 values
['granularity'] = vim_config
['granularity']
206 payload
= self
.check_payload(values
, metric_name
, resource_id
,
208 new_alarm
= Common
.perform_request(
209 url
, auth_token
, req_type
="post", payload
=payload
, verify_ssl
=verify_ssl
)
210 return json
.loads(new_alarm
.text
)['alarm_id']
212 def delete_alarm(self
, endpoint
, auth_token
, alarm_id
, verify_ssl
):
213 """Delete alarm function."""
214 url
= "{}/v2/alarms/%s".format(endpoint
) % alarm_id
216 result
= Common
.perform_request(
217 url
, auth_token
, req_type
="delete", verify_ssl
=verify_ssl
)
218 if str(result
.status_code
) == "404":
219 raise ValueError("Alarm {} doesn't exist".format(alarm_id
))
221 def list_alarms(self
, endpoint
, auth_token
, list_details
, verify_ssl
):
222 """Generate the requested list of alarms."""
223 url
= "{}/v2/alarms/".format(endpoint
)
224 a_list
, name_list
, sev_list
, res_list
= [], [], [], []
226 # TODO(mcgoughh): for now resource_id is a mandatory field
227 # Check for a resource id
229 resource
= list_details
['resource_uuid']
230 name
= list_details
['alarm_name'].lower()
231 severity
= list_details
['severity'].lower()
232 sev
= SEVERITIES
[severity
]
233 except KeyError as e
:
234 log
.warning("Missing parameter for alarm list request: %s", e
)
237 # Perform the request to get the desired list
239 result
= Common
.perform_request(
240 url
, auth_token
, req_type
="get", verify_ssl
=verify_ssl
)
242 if result
is not None:
243 # Get list based on resource id
244 for alarm
in json
.loads(result
.text
):
245 rule
= alarm
['gnocchi_resources_threshold_rule']
246 if resource
== rule
['resource_id']:
247 res_list
.append(alarm
['alarm_id'])
249 # Generate specified listed if requested
250 if name
is not None and sev
is not None:
251 log
.info("Return a list of %s alarms with %s severity.",
253 for alarm
in json
.loads(result
.text
):
254 if name
== alarm
['name']:
255 name_list
.append(alarm
['alarm_id'])
256 for alarm
in json
.loads(result
.text
):
257 if sev
== alarm
['severity']:
258 sev_list
.append(alarm
['alarm_id'])
259 name_sev_list
= list(set(name_list
).intersection(sev_list
))
260 a_list
= list(set(name_sev_list
).intersection(res_list
))
261 elif name
is not None:
262 log
.info("Returning a %s list of alarms.", name
)
263 for alarm
in json
.loads(result
.text
):
264 if name
== alarm
['name']:
265 name_list
.append(alarm
['alarm_id'])
266 a_list
= list(set(name_list
).intersection(res_list
))
267 elif sev
is not None:
268 log
.info("Returning %s severity alarm list.", sev
)
269 for alarm
in json
.loads(result
.text
):
270 if sev
== alarm
['severity']:
271 sev_list
.append(alarm
['alarm_id'])
272 a_list
= list(set(sev_list
).intersection(res_list
))
274 log
.info("Returning an entire list of alarms.")
277 log
.info("There are no alarms!")
279 for alarm
in json
.loads(result
.text
):
280 if alarm
['alarm_id'] in a_list
:
281 response_list
.append(alarm
)
284 except Exception as e
:
285 log
.exception("Failed to generate alarm list: ")
288 def update_alarm_state(self
, endpoint
, auth_token
, alarm_id
, verify_ssl
):
289 """Set the state of an alarm to ok when ack message is received."""
290 url
= "{}/v2/alarms/%s/state".format(endpoint
) % alarm_id
291 payload
= json
.dumps("ok")
293 Common
.perform_request(
294 url
, auth_token
, req_type
="put", payload
=payload
, verify_ssl
=verify_ssl
)
296 def update_alarm(self
, endpoint
, auth_token
, values
, vim_config
, verify_ssl
):
297 """Get alarm name for an alarm configuration update."""
298 # Get already existing alarm details
299 url
= "{}/v2/alarms/%s".format(endpoint
) % values
['alarm_uuid']
301 # Gets current configurations about the alarm
302 result
= Common
.perform_request(
303 url
, auth_token
, req_type
="get")
304 alarm_name
= json
.loads(result
.text
)['name']
305 rule
= json
.loads(result
.text
)['gnocchi_resources_threshold_rule']
306 alarm_state
= json
.loads(result
.text
)['state']
307 resource_id
= rule
['resource_id']
308 metric_name
= [key
for key
, value
in six
.iteritems(METRIC_MAPPINGS
) if value
== rule
['metric']][0]
310 # Generates and check payload configuration for alarm update
311 if 'granularity' in vim_config
and 'granularity' not in values
:
312 values
['granularity'] = vim_config
['granularity']
313 payload
= self
.check_payload(values
, metric_name
, resource_id
,
314 alarm_name
, alarm_state
=alarm_state
)
316 # Updates the alarm configurations with the valid payload
317 update_alarm
= Common
.perform_request(
318 url
, auth_token
, req_type
="put", payload
=payload
, verify_ssl
=verify_ssl
)
320 return json
.loads(update_alarm
.text
)['alarm_id']
322 def check_payload(self
, values
, metric_name
, resource_id
,
323 alarm_name
, alarm_state
=None):
324 """Check that the payload is configuration for update/create alarm."""
325 cfg
= Config
.instance()
326 # Check state and severity
328 severity
= 'critical'
329 if 'severity' in values
:
330 severity
= values
['severity'].lower()
332 if severity
== "indeterminate":
333 alarm_state
= "insufficient data"
334 if alarm_state
is None:
337 statistic
= values
['statistic'].lower()
339 granularity
= cfg
.OS_DEFAULT_GRANULARITY
340 if 'granularity' in values
:
341 granularity
= values
['granularity']
343 resource_type
= 'generic'
344 if 'resource_type' in values
:
345 resource_type
= values
['resource_type'].lower()
347 # Try to configure the payload for the update/create request
348 # Can only update: threshold, operation, statistic and
349 # the severity of the alarm
350 rule
= {'threshold': values
['threshold_value'],
351 'comparison_operator': values
['operation'].lower(),
352 'metric': METRIC_MAPPINGS
[metric_name
],
353 'resource_id': resource_id
,
354 'resource_type': resource_type
,
355 'aggregation_method': STATISTICS
[statistic
],
356 'granularity': granularity
, }
357 payload
= json
.dumps({'state': alarm_state
,
359 'severity': SEVERITIES
[severity
],
360 'type': 'gnocchi_resources_threshold',
361 'gnocchi_resources_threshold_rule': rule
,
362 'alarm_actions': [cfg
.OS_NOTIFIER_URI
],
363 'repeat_actions': True}, sort_keys
=True)
366 def get_alarm_state(self
, endpoint
, auth_token
, alarm_id
):
367 """Get the state of the alarm."""
368 url
= "{}/v2/alarms/%s/state".format(endpoint
) % alarm_id
370 alarm_state
= Common
.perform_request(
371 url
, auth_token
, req_type
="get")
372 return json
.loads(alarm_state
.text
)
374 def check_for_metric(self
, auth_token
, metric_endpoint
, metric_name
, resource_id
, verify_ssl
):
376 Checks if resource has a specific metric. If not, throws exception.
377 :param auth_token: OpenStack auth token
378 :param metric_endpoint: OpenStack metric endpoint
379 :param metric_name: Metric name
380 :param resource_id: Resource UUID
381 :return: Metric details from resource
382 :raise Exception: Could not retrieve metric from resource
385 url
= "{}/v1/resource/generic/{}".format(metric_endpoint
, resource_id
)
386 result
= Common
.perform_request(
387 url
, auth_token
, req_type
="get", verify_ssl
=verify_ssl
)
388 resource
= json
.loads(result
.text
)
389 metrics_dict
= resource
['metrics']
390 return metrics_dict
[METRIC_MAPPINGS
[metric_name
]]
391 except Exception as e
:
392 log
.exception("Desired Gnocchi metric not found:", e
)
395 def _generate_and_send_response(self
, key
, correlation_id
, **kwargs
):
397 resp_message
= self
._response
.generate_response(
398 key
, cor_id
=correlation_id
, **kwargs
)
399 log
.info("Response Message: %s", resp_message
)
400 self
._producer
.publish_alarm_response(
402 except Exception as e
:
403 log
.exception("Response creation failed:")