Merge "Build on jenkins nodes with label docker"
[osm/SO.git] / rwmon / plugins / vala / rwmon_ceilometer / rwmon_ceilometer.py
1
2 #
3 # Copyright 2016 RIFT.IO Inc
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 import collections
19 import dateutil.parser
20 import json
21 import logging
22 import urllib.parse
23
24 import requests
25
26 import gi
27 gi.require_version('RwTypes', '1.0')
28 gi.require_version('RwcalYang', '1.0')
29 gi.require_version('RwmonYang', '1.0')
30
31 from gi.repository import (
32 GObject,
33 RwMon,
34 RwTypes,
35 RwmonYang,
36 )
37
38 import rift.rwcal.openstack as openstack_drv
39 import rw_status
40 import rwlogger
41
42 logger = logging.getLogger('rwmon.ceilometer')
43
44 rwstatus = rw_status.rwstatus_from_exc_map({
45 IndexError: RwTypes.RwStatus.NOTFOUND,
46 KeyError: RwTypes.RwStatus.NOTFOUND,
47 })
48
49
50 class UnknownService(Exception):
51 pass
52
53
54 class CeilometerMonitoringPlugin(GObject.Object, RwMon.Monitoring):
55 def __init__(self):
56 GObject.Object.__init__(self)
57 self._driver_class = openstack_drv.OpenstackDriver
58
59 def _get_driver(self, account):
60 return self._driver_class(username = account.openstack.key,
61 password = account.openstack.secret,
62 auth_url = account.openstack.auth_url,
63 tenant_name = account.openstack.tenant,
64 mgmt_network = account.openstack.mgmt_network)
65
66 @rwstatus
67 def do_init(self, rwlog_ctx):
68 if not any(isinstance(h, rwlogger.RwLogger) for h in logger.handlers):
69 logger.addHandler(
70 rwlogger.RwLogger(
71 category="rw-monitor-log",
72 subcategory="ceilometer",
73 log_hdl=rwlog_ctx,
74 )
75 )
76
77 @rwstatus(ret_on_failure=[None])
78 def do_nfvi_metrics(self, account, vm_id):
79 try:
80 samples = self._get_driver(account).ceilo_nfvi_metrics(vm_id)
81
82 metrics = RwmonYang.NfviMetrics()
83
84 vcpu = samples.get("cpu_util", {})
85 memory = samples.get("memory_usage", {})
86 storage = samples.get("disk_usage", {})
87
88 metrics.vcpu.utilization = vcpu.get("volume", 0)
89 metrics.memory.used = memory.get("volume", 0)
90 metrics.storage.used = storage.get("volume", 0)
91
92 def convert_timestamp(t):
93 return dateutil.parser.parse(t).timestamp()
94
95 timestamps = []
96 if 'timestamp' in vcpu:
97 timestamps.append(convert_timestamp(vcpu['timestamp']))
98 if 'timestamp' in memory:
99 timestamps.append(convert_timestamp(memory['timestamp']))
100 if 'timestamp' in storage:
101 timestamps.append(convert_timestamp(storage['timestamp']))
102
103 metrics.timestamp = max(timestamps) if timestamps else 0.0
104
105 return metrics
106
107 except Exception as e:
108 logger.exception(e)
109
110 @rwstatus(ret_on_failure=[None])
111 def do_nfvi_vcpu_metrics(self, account, vm_id):
112 try:
113 samples = self._get_driver(account).ceilo_nfvi_metrics(vm_id)
114
115 metrics = RwmonYang.NfviMetrics_Vcpu()
116 metrics.utilization = samples.get("cpu_util", 0)
117
118 return metrics
119
120 except Exception as e:
121 logger.exception(e)
122
123 @rwstatus(ret_on_failure=[None])
124 def do_nfvi_memory_metrics(self, account, vm_id):
125 try:
126 samples = self._get_driver(account).ceilo_nfvi_metrics(vm_id)
127
128 metrics = RwmonYang.NfviMetrics_Memory()
129 metrics.used = samples.get("memory_usage", 0)
130
131 return metrics
132
133 except Exception as e:
134 logger.exception(e)
135
136 @rwstatus(ret_on_failure=[None])
137 def do_nfvi_storage_metrics(self, account, vm_id):
138 try:
139 samples = self._get_driver(account).ceilo_nfvi_metrics(vm_id)
140
141 metrics = RwmonYang.NfviMetrics_Storage()
142 metrics.used = samples.get("disk_usage", 0)
143
144 return metrics
145
146 except Exception as e:
147 logger.exception(e)
148
149 @rwstatus(ret_on_failure=[False])
150 def do_nfvi_metrics_available(self, account):
151 try:
152 endpoint = self._get_driver(account).ceilo_meter_endpoint()
153 except Exception:
154 return False
155
156 return endpoint is not None
157
158 @rwstatus(ret_on_failure=[None])
159 def do_alarm_create(self, account, vim_id, alarm):
160 # Retrieve a token using account information
161 token = openstack_auth_token(account)
162 service = token.service("ceilometer")
163 headers = {"content-type": "application/json", "x-auth-token": token.id}
164
165 # Convert the alarm from its YANG representation into something that
166 # can be passed to the openstack interface
167 ceilometer_alarm = CeilometerAlarm.from_gi_obj(alarm, vim_id).to_dict()
168
169 # POST the data to ceilometer
170 response = requests.post(
171 service.url.public + "/v2/alarms",
172 headers=headers,
173 data=json.dumps(ceilometer_alarm),
174 timeout=5,
175 )
176
177 # Returns the response object and update the alarm ID
178 obj = response.json()
179 alarm.alarm_id = obj['alarm_id']
180 return obj
181
182 @rwstatus(ret_on_failure=[None])
183 def do_alarm_update(self, account, alarm):
184 # Retrieve a token using account information
185 token = openstack_auth_token(account)
186 service = token.service("ceilometer")
187 headers = {"content-type": "application/json", "x-auth-token": token.id}
188
189 # Convert the alarm from its YANG representation into something that
190 # can be passed to the openstack interface
191 ceilometer_alarm = CeilometerAlarm.from_gi_obj(alarm).to_dict()
192
193 # PUT the data to ceilometer
194 response = requests.put(
195 service.url.public + "/v2/alarms/{}".format(alarm.alarm_id),
196 headers=headers,
197 data=json.dumps(ceilometer_alarm),
198 timeout=5,
199 )
200
201 return response.json()
202
203 @rwstatus(ret_on_failure=[None])
204 def do_alarm_delete(self, account, alarm_id):
205 # Retrieve a token using account information
206 token = openstack_auth_token(account)
207 service = token.service("ceilometer")
208 headers = {"content-type": "application/json", "x-auth-token": token.id}
209
210 # DELETE the alarm
211 _ = requests.delete(
212 service.url.public + "/v2/alarms/{}".format(alarm_id),
213 headers=headers,
214 timeout=5,
215 )
216
217 @rwstatus(ret_on_failure=[None])
218 def do_alarm_list(self, account):
219 # Retrieve a token using account information
220 token = openstack_auth_token(account)
221 service = token.service("ceilometer")
222 headers = {"x-auth-token": token.id}
223
224 # GET a list of alarms
225 response = requests.get(
226 service.url.public + "/v2/alarms",
227 headers=headers,
228 timeout=5,
229 )
230
231 return response.json()
232
233
234 class OpenstackAuthTokenV2(object):
235 def __init__(self, data):
236 self._data = data
237
238 @classmethod
239 def request(cls, account):
240 """Create an OpenstackAuthTokenV2 using account information
241
242 Arguments:
243 account - an RwcalYang.CloudAccount object
244
245 Returns:
246 an openstack token
247
248 """
249 headers = {"content-type": "application/json"}
250 data = json.dumps({
251 "auth": {
252 "tenantName": account.openstack.tenant,
253 "passwordCredentials": {
254 "username": account.openstack.key,
255 "password": account.openstack.secret,
256 }
257 }
258 })
259
260 url = "{}/tokens".format(account.openstack.auth_url)
261 response = requests.post(url, headers=headers, data=data)
262 response.raise_for_status()
263
264 return cls(response.json())
265
266 @property
267 def id(self):
268 """The token identifier"""
269 return self._data["access"]["token"]["id"]
270
271 def service(self, name):
272 """Returns information about the specified service
273
274 Arguments:
275 name - the name of the service to return
276
277 Raises:
278 If the requested service cannot be found, an UnknownService
279 exception is raised.
280
281 Returns:
282 an OpenstackService object
283
284 """
285 for s in self._data["access"]["serviceCatalog"]:
286 if s["name"] == name:
287 return OpenstackService(
288 name=name,
289 url=OpenstackServiceURLs(
290 public=s["endpoints"][0]["publicURL"],
291 internal=s["endpoints"][0]["internalURL"],
292 admin=s["endpoints"][0]["adminURL"],
293 )
294 )
295
296 raise UnknownService(name)
297
298
299 class OpenstackAuthTokenV3(object):
300 def __init__(self, token, data):
301 self._data = data
302 self._token = token
303
304 @classmethod
305 def request(cls, account):
306 """Create an OpenstackAuthTokenV3 using account information
307
308 Arguments:
309 account - an RwcalYang.CloudAccount object
310
311 Returns:
312 an openstack token
313
314 """
315 headers = {"content-type": "application/json"}
316 data = json.dumps({
317 "auth": {
318 "identity": {
319 "methods": ["password"],
320 "password": {
321 "user": {
322 "name": account.openstack.key,
323 "password": account.openstack.secret,
324 "domain": {"id": "default"},
325 }
326 }
327 },
328 "scope": {
329 "project": {
330 "name": account.openstack.tenant,
331 "domain": {"id": "default"},
332 }
333 }
334 }
335 })
336
337 url = account.openstack.auth_url + "/auth/tokens"
338
339 response = requests.post(url, headers=headers, data=data)
340 response.raise_for_status()
341
342 return cls(response.headers['x-subject-token'], response.json())
343
344 @property
345 def id(self):
346 """The token identifier"""
347 return self._token
348
349 def service(self, name):
350 """Returns information about the specified service
351
352 Arguments:
353 name - the name of the service to return
354
355 Raises:
356 If the requested service cannot be found, an UnknownService
357 exception is raised.
358
359 Returns:
360 an OpenstackService object
361
362 """
363 for s in self._data["token"]["catalog"]:
364 if s["name"] == name:
365 endpoints = {e["interface"]:e["url"] for e in s["endpoints"]}
366 return OpenstackService(
367 name=name,
368 url=OpenstackServiceURLs(
369 public=endpoints["public"],
370 internal=endpoints["internal"],
371 admin=endpoints["admin"],
372 )
373 )
374
375 raise UnknownService(name)
376
377
378 def openstack_auth_token(account):
379 url = urllib.parse.urlparse(account.openstack.auth_url)
380
381 if url.path in ('/v3',):
382 return OpenstackAuthTokenV3.request(account)
383
384 if url.path in ('/v2.0', 'v2.1'):
385 return OpenstackAuthTokenV2.request(account)
386
387 raise ValueError("Unrecognized keystone version")
388
389
390 class OpenstackService(collections.namedtuple(
391 "OpenstackServer",
392 "name url")):
393 pass
394
395
396 class OpenstackServiceURLs(collections.namedtuple(
397 "OpenstackServiceURLs",
398 "public internal admin")):
399 pass
400
401
402 class CeilometerAlarm(collections.namedtuple(
403 "CeilometerAlarm",
404 "name type description severity repeat_actions enabled alarm_actions ok_actions insufficient_data_actions threshold_rule")):
405 @classmethod
406 def from_gi_obj(cls, alarm, vim_id=None):
407 severity = CeilometerAlarmSeverity.from_gi_obj(alarm.severity).severity
408 actions = CeilometerAlarmActions.from_gi_obj(alarm.actions)
409
410 alarm_id = alarm.alarm_id if vim_id is None else vim_id
411 threshold_rule = CeilometerThresholdRule.from_gi_obj(alarm_id, alarm)
412
413 return cls(
414 type="threshold",
415 name=alarm.name,
416 description=alarm.description,
417 severity=severity,
418 repeat_actions=alarm.repeat,
419 enabled=alarm.enabled,
420 threshold_rule=threshold_rule,
421 ok_actions=actions.ok,
422 alarm_actions=actions.alarm,
423 insufficient_data_actions=actions.insufficient_data,
424 )
425
426 def to_dict(self):
427 """Returns a dictionary containing the tuple data"""
428 def recursive_to_dict(obj):
429 if not hasattr(obj, '_fields'):
430 return obj
431
432 return {k: recursive_to_dict(getattr(obj, k)) for k in obj._fields}
433
434 return recursive_to_dict(self)
435
436
437 class CeilometerThresholdRule(collections.namedtuple(
438 "CeilometerThresholdRule",
439 "evaluation_periods threshold statistic meter_name comparison_operator period query")):
440 @classmethod
441 def from_gi_obj(cls, vim_id, alarm):
442 meter = CeilometerAlarmMeter.from_gi_obj(alarm.metric).meter
443 statistic = CeilometerAlarmStatistic.from_gi_obj(alarm.statistic).statistic
444 operation = CeilometerAlarmOperation.from_gi_obj(alarm.operation).operation
445
446 return cls(
447 evaluation_periods=alarm.evaluations,
448 threshold=alarm.value,
449 statistic=statistic,
450 meter_name=meter,
451 comparison_operator=operation,
452 period=alarm.period,
453 query=[{
454 "op": "eq",
455 "field": "resource_id",
456 "value": vim_id,
457 }]
458 )
459
460 def to_dict(self):
461 """Returns a dictionary containing the tuple data"""
462 def recursive_to_dict(obj):
463 if not hasattr(obj, '_fields'):
464 return obj
465
466 return {k: recursive_to_dict(getattr(obj, k)) for k in obj._fields}
467
468 return recursive_to_dict(self)
469
470
471 class CeilometerAlarmMeter(collections.namedtuple(
472 "CeiloemterAlarmMeter",
473 "meter")):
474 __mapping__ = {
475 "CPU_UTILIZATION" : "cpu_util",
476 "MEMORY_UTILIZATION" : "memory_usage",
477 "STORAGE_UTILIZATION" : "disk_usage",
478 }
479 @classmethod
480 def from_gi_obj(cls, meter):
481 return cls(meter=cls.__mapping__[meter])
482
483
484 class CeilometerAlarmStatistic(collections.namedtuple(
485 "CeilometerAlarmStatistic",
486 "statistic")):
487 __mapping__ = {
488 "AVERAGE": "avg",
489 "MINIMUM": "min",
490 "MAXIMUM": "max",
491 "COUNT": "count",
492 "SUM": "sum",
493 }
494 @classmethod
495 def from_gi_obj(cls, statistic):
496 return cls(statistic=cls.__mapping__[statistic])
497
498
499 class CeilometerAlarmOperation(collections.namedtuple(
500 "CeilometerAlarmOperation",
501 "operation")):
502 __mapping__ = {
503 "LT": "lt",
504 "LE": "le",
505 "EQ": "eq",
506 "GE": "ge",
507 "GT": "gt",
508 }
509 @classmethod
510 def from_gi_obj(cls, operation):
511 return cls(operation=cls.__mapping__[operation])
512
513
514 class CeilometerAlarmSeverity(collections.namedtuple(
515 "CeilometerAlarmSeverity",
516 "severity")):
517 __mapping__ = {
518 "LOW": "low",
519 "MODERATE": "moderate",
520 "CRITICAL": "critical",
521 }
522 @classmethod
523 def from_gi_obj(cls, severity):
524 return cls(severity=cls.__mapping__[severity])
525
526
527 class CeilometerAlarmActions(collections.namedtuple(
528 "CeilometerAlarmActions",
529 "ok alarm insufficient_data")):
530 @classmethod
531 def from_gi_obj(cls, actions):
532 return cls(
533 ok=[obj.url for obj in actions.ok],
534 alarm=[obj.url for obj in actions.alarm],
535 insufficient_data=[obj.url for obj in actions.insufficient_data],
536 )