From e7046c44c01596abae9ef867cf0cd95a51919dac Mon Sep 17 00:00:00 2001 From: Lianhao Lu Date: Fri, 25 Jul 2014 13:19:32 +0800 Subject: [PATCH] Added new hardware inspector interface Added a new hardware inspector interface: inspect_generic and its snmp implementation. The old interface will be removed later. Partial implements: blueprint snmp-improvement. Partial-Bug: 1286132 Change-Id: I847cfdad5b6571c4d066250617050a8f0b1cfbc4 --- ceilometer/hardware/inspector/base.py | 13 + ceilometer/hardware/inspector/snmp.py | 282 +++++++++++++++++- .../tests/hardware/inspector/test_snmp.py | 96 ++++++ ceilometer/tests/hardware/pollsters/base.py | 3 + 4 files changed, 393 insertions(+), 1 deletion(-) diff --git a/ceilometer/hardware/inspector/base.py b/ceilometer/hardware/inspector/base.py index 0fae9efd3..1bcbf2814 100644 --- a/ceilometer/hardware/inspector/base.py +++ b/ceilometer/hardware/inspector/base.py @@ -107,3 +107,16 @@ class Inspector(object): :param : the target host :return: iterator of tuple (Interface, InterfaceStats) """ + + @abc.abstractmethod + def inspect_generic(self, host, identifier, cache): + """A generic inspect function. + + :param host: the target host + :param identifier: the identifier of the metric + :param cache: cache passed from the pollster + :return: an iterator of (value, metadata, extra) + :return value: the sample value + :return metadata: dict to construct sample's metadata + :return extra: dict of extra info to help constructing sample + """ \ No newline at end of file diff --git a/ceilometer/hardware/inspector/snmp.py b/ceilometer/hardware/inspector/snmp.py index 84d712173..6152f5aa4 100644 --- a/ceilometer/hardware/inspector/snmp.py +++ b/ceilometer/hardware/inspector/snmp.py @@ -1,8 +1,10 @@ # # Copyright 2014 ZHAW SoE +# Copyright 2014 Intel Corp # # Authors: Lucas Graf # Toni Zehnder +# Lianhao Lu # # 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 @@ -26,11 +28,12 @@ class SNMPException(Exception): pass -def parse_snmp_return(ret): +def parse_snmp_return(ret, is_bulk=False): """Check the return value of snmp operations :param ret: a tuple of (errorIndication, errorStatus, errorIndex, data) returned by pysnmp + :param is_bulk: True if the ret value is from GetBulkRequest :return: a tuple of (err, data) err: True if error found, or False if no error found data: a string of error description if error found, or the @@ -41,6 +44,8 @@ def parse_snmp_return(ret): if errIndication: data = errIndication elif errStatus: + if is_bulk: + varBinds = varBinds[-1] data = "%s at %s" % (errStatus.prettyPrint(), errIdx and varBinds[int(errIdx) - 1] or "?") else: @@ -49,6 +54,10 @@ def parse_snmp_return(ret): return (err, data) +EXACT = 'type_exact' +PREFIX = 'type_prefix' + + class SNMPInspector(base.Inspector): # CPU OIDs _cpu_1_min_load_oid = "1.3.6.1.4.1.2021.10.1.3.1" @@ -75,10 +84,281 @@ class SNMPInspector(base.Inspector): # Default port _port = 161 + _disk_metadata = { + 'path': (_disk_path_oid, str), + 'device': (_disk_device_oid, str), + } + + _net_metadata = { + 'name': (_interface_name_oid, str), + 'speed': (_interface_speed_oid, lambda x: int(x) / 8), + 'mac': (_interface_mac_oid, + lambda x: x.prettyPrint().replace('0x', '')), + } + + _CACHE_KEY_OID = "snmp_cached_oid" + + ''' + + The following mapping define how to construct + (value, metadata, extra) returned by inspect_generic + MAPPING = { + 'identifier: { + 'matching_type': EXACT or PREFIX, + 'metric_oid': (oid, value_converter) + 'metadata': { + metadata_name1: (oid1, value_converter), + metadata_name2: (oid2, value_converter), + }, + 'post_op': special func to modify the return data, + }, + } + + For matching_type of EXACT, each item in the above mapping will + return exact one (value, metadata, extra) tuple. The value would be + returned from SNMP request GetRequest for oid of 'metric_oid', the + metadata dict would be constructed based on the returning from SNMP + GetRequest for oids of 'metadata'. + + For matching_type of PREFIX, SNMP request GetBulkRequest + would be send to get values for oids of 'metric_oid' and + 'metadata' of each item in the above mapping. And each item might + return multiple (value, metadata, extra) tuple, e.g. + Suppose we have the following mapping: + MAPPING = { + 'disk.size.total': { + 'matching_type': PREFIX, + 'metric_oid': ("1.3.6.1.4.1.2021.9.1.6", int) + 'metadata': { + 'device': ("1.3.6.1.4.1.2021.9.1.3", str), + 'path': ("1.3.6.1.4.1.2021.9.1.2", str), + }, + 'post_op': None, + }, + and the SNMP have the following oid/value(s): + { + '1.3.6.1.4.1.2021.9.1.6.1': 19222656, + '1.3.6.1.4.1.2021.9.1.3.1': "/dev/sda2", + '1.3.6.1.4.1.2021.9.1.2.1': "/" + '1.3.6.1.4.1.2021.9.1.6.2': 808112, + '1.3.6.1.4.1.2021.9.1.3.2': "tmpfs", + '1.3.6.1.4.1.2021.9.1.2.2': "/run", + } + So here we'll return 2 instances of (value, metadata, extra): + (19222656, {'device': "/dev/sda2", 'path': "/"}, None) + (808112, {'device': "tmpfs", 'path': "/run"}, None) + + The post_op is assumed to be implemented by new metric developer. It + could be used to add additional special metadata(e.g. ip address), or + it could be used to add information into extra dict to be returned + to construct the pollster how to build final sample, e.g. + extra.update('project_id': xy, 'user_id': zw) + ''' + + MAPPING = { + 'cpu.load.1min': { + 'matching_type': EXACT, + 'metric_oid': (_cpu_1_min_load_oid, lambda x: float(str(x))), + 'metadata': {}, + 'post_op': None + }, + 'cpu.load.5min': { + 'matching_type': EXACT, + 'metric_oid': (_cpu_5_min_load_oid, lambda x: float(str(x))), + 'metadata': {}, + 'post_op': None, + }, + 'cpu.load.15min': { + 'matching_type': EXACT, + 'metric_oid': (_cpu_15_min_load_oid, lambda x: float(str(x))), + 'metadata': {}, + 'post_op': None, + }, + 'memory.total': { + 'matching_type': EXACT, + 'metric_oid': (_memory_total_oid, int), + 'metadata': {}, + 'post_op': None, + }, + 'memory.used': { + 'matching_type': EXACT, + 'metric_oid': (_memory_used_oid, int), + 'metadata': {}, + 'post_op': None, + }, + 'disk.size.total': { + 'matching_type': PREFIX, + 'metric_oid': (_disk_size_oid, int), + 'metadata': _disk_metadata, + 'post_op': None, + }, + 'disk.size.used': { + 'matching_type': PREFIX, + 'metric_oid': (_disk_used_oid, int), + 'metadata': _disk_metadata, + 'post_op': None, + }, + 'network.incoming.bytes': { + 'matching_type': PREFIX, + 'metric_oid': (_interface_received_oid, int), + 'metadata': _net_metadata, + 'post_op': "_post_op_net", + }, + 'network.outgoing.bytes': { + 'matching_type': PREFIX, + 'metric_oid': (_interface_transmitted_oid, int), + 'metadata': _net_metadata, + 'post_op': "_post_op_net", + }, + 'network.outgoing.errors': { + 'matching_type': PREFIX, + 'metric_oid': (_interface_error_oid, int), + 'metadata': _net_metadata, + 'post_op': "_post_op_net", + }, + } + def __init__(self): super(SNMPInspector, self).__init__() self._cmdGen = cmdgen.CommandGenerator() + def _query_oids(self, host, oids, cache, is_bulk): + # send GetRequest or GetBulkRequest to get oid values and + # populate the values into cache + authData = self._get_auth_strategy(host) + transport = cmdgen.UdpTransportTarget((host.hostname, + host.port or self._port)) + oid_cache = cache.setdefault(self._CACHE_KEY_OID, {}) + + if is_bulk: + ret = self._cmdGen.bulkCmd(authData, + transport, + 0, 100, + *oids, + lookupValues=True) + else: + ret = self._cmdGen.getCmd(authData, + transport, + *oids, + lookupValues=True) + (error, data) = parse_snmp_return(ret, is_bulk) + if error: + raise SNMPException("An error occurred, oids %(oid)s, " + "host %(host)s, %(err)s" % + dict(oid=oids, + host=host.hostname, + err=data)) + # save result into cache + if is_bulk: + for var_bind_table_row in data: + for name, val in var_bind_table_row: + oid_cache[name.prettyPrint()] = val + else: + for name, val in data: + oid_cache[name.prettyPrint()] = val + cache[self._CACHE_KEY_OID] = oid_cache + + @staticmethod + def find_matching_oids(oid_cache, oid, match_type, find_one=True): + matched = [] + if match_type == PREFIX: + for key in oid_cache.keys(): + if key.startswith(oid): + matched.append(key) + if find_one: + break + else: + if oid in oid_cache: + matched.append(oid) + return matched + + @staticmethod + def get_oid_value(oid_cache, oid_def, suffix=''): + oid, converter = oid_def + value = oid_cache[oid + suffix] + if converter: + value = converter(value) + return value + + @classmethod + def construct_metadata(cls, oid_cache, meta_defs, suffix=''): + metadata = {} + for key, oid_def in meta_defs.iteritems(): + metadata[key] = cls.get_oid_value(oid_cache, oid_def, suffix) + return metadata + + @classmethod + def _find_missing_oids(cls, meter_def, cache): + # find oids have not been queried and cached + new_oids = [] + oid_cache = cache.setdefault(cls._CACHE_KEY_OID, {}) + # check metric_oid + if not cls.find_matching_oids(oid_cache, + meter_def['metric_oid'][0], + meter_def['matching_type']): + new_oids.append(meter_def['metric_oid'][0]) + for metadata in meter_def['metadata'].values(): + if not cls.find_matching_oids(oid_cache, + metadata[0], + meter_def['matching_type']): + new_oids.append(metadata[0]) + return new_oids + + def inspect_generic(self, host, identifier, cache): + # the snmp definition for the corresponding meter + meter_def = self.MAPPING[identifier] + # collect oids that needs to be queried + oids_to_query = self._find_missing_oids(meter_def, cache) + # query oids and populate into caches + if oids_to_query: + self._query_oids(host, oids_to_query, cache, + meter_def['matching_type'] == PREFIX) + # construct (value, metadata, extra) + oid_cache = cache[self._CACHE_KEY_OID] + # find all oids which needed to construct final sample values + # for matching type of EXACT, only 1 sample would be generated + # for matching type of PREFIX, multiple samples could be generated + oids_for_sample_values = self.find_matching_oids( + oid_cache, + meter_def['metric_oid'][0], + meter_def['matching_type'], + False) + for oid in oids_for_sample_values: + suffix = oid[len(meter_def['metric_oid'][0]):] + value = self.get_oid_value(oid_cache, + meter_def['metric_oid'], + suffix) + # get the metadata for this sample value + metadata = self.construct_metadata(oid_cache, + meter_def['metadata'], + suffix) + extra = {} + # call post_op for special cases + if meter_def['post_op']: + func = getattr(self, meter_def['post_op'], None) + if func: + value = func(host, cache, meter_def, + value, metadata, extra, + suffix) + yield (value, metadata, extra) + + def _post_op_net(self, host, cache, meter_def, + value, metadata, extra, suffix): + # add ip address into metadata + oid_cache = cache.setdefault(self._CACHE_KEY_OID, {}) + if not self.find_matching_oids(oid_cache, + self._interface_ip_oid, + PREFIX): + # populate the oid into cache + self._query_oids(host, [self._interface_ip_oid], cache, True) + oid_cache = cache[self._CACHE_KEY_OID] + ip_addr = '' + for k, v in oid_cache.iteritems(): + if k.startswith(self._interface_ip_oid) and v == int(suffix[1:]): + ip_addr = k.replace(self._interface_ip_oid + ".", "") + metadata.update(ip=ip_addr) + return value + def _get_or_walk_oid(self, oid, host, get=True): if get: func = self._cmdGen.getCmd diff --git a/ceilometer/tests/hardware/inspector/test_snmp.py b/ceilometer/tests/hardware/inspector/test_snmp.py index 4c8568b0a..26ce2fdac 100644 --- a/ceilometer/tests/hardware/inspector/test_snmp.py +++ b/ceilometer/tests/hardware/inspector/test_snmp.py @@ -190,6 +190,14 @@ NEXTCMD_MAP = { } +class FakeObjectName(object): + def __init__(self, name): + self.name = name + + def prettyPrint(self): + return str(self.name) + + def faux_getCmd(authData, transportTarget, oid): try: return GETCMD_MAP[oid] @@ -204,11 +212,46 @@ def faux_nextCmd(authData, transportTarget, oid): return ("faux_nextCmd Error", None, 0, []) +def faux_getCmd_new(authData, transportTarget, *oids, **kwargs): + varBinds = [(FakeObjectName(oid), + int(oid.split('.')[-1])) for oid in oids] + return (None, None, 0, varBinds) + + +def faux_bulkCmd_new(authData, transportTarget, nonRepeaters, maxRepetitions, + *oids, **kwargs): + varBindTable = [ + [(FakeObjectName(oid + ".%d" % i), i) for i in range(1, 3)] + for oid in oids + ] + return (None, None, 0, varBindTable) + + class TestSNMPInspector(Base, test_base.BaseTestCase): + mapping = { + 'test_exact': { + 'matching_type': snmp.EXACT, + 'metric_oid': ('1.3.6.1.4.1.2021.10.1.3.1', int), + 'metadata': { + 'meta': ('1.3.6.1.4.1.2021.10.1.3.8', int) + }, + 'post_op': '_fake_post_op', + }, + 'test_prefix': { + 'matching_type': snmp.PREFIX, + 'metric_oid': ('1.3.6.1.4.1.2021.9.1.8', int), + 'metadata': { + 'meta': ('1.3.6.1.4.1.2021.9.1.3', int) + }, + 'post_op': None, + }, + } + def setUp(self): super(TestSNMPInspector, self).setUp() self.inspector = snmp.SNMPInspector() self.host = network_utils.urlsplit("snmp://localhost") + self.inspector.MAPPING = self.mapping self.useFixture(mockpatch.PatchObject( self.inspector._cmdGen, 'getCmd', new=faux_getCmd)) self.useFixture(mockpatch.PatchObject( @@ -225,3 +268,56 @@ class TestSNMPInspector(Base, test_base.BaseTestCase): get_list, self.inspector.inspect_memory, self.host) + + def _fake_post_op(self, host, cache, meter_def, + value, metadata, extra, suffix): + metadata.update(post_op_meta=4) + extra.update(project_id=2) + return value + + def test_inspect_generic_exact(self): + self.useFixture(mockpatch.PatchObject( + self.inspector._cmdGen, 'getCmd', new=faux_getCmd_new)) + self.inspector._fake_post_op = self._fake_post_op + cache = {} + ret = list(self.inspector.inspect_generic(self.host, + 'test_exact', + cache)) + keys = cache[ins._CACHE_KEY_OID].keys() + self.assertIn('1.3.6.1.4.1.2021.10.1.3.1', keys) + self.assertIn('1.3.6.1.4.1.2021.10.1.3.8', keys) + self.assertEqual(1, len(ret)) + self.assertEqual(1, ret[0][0]) + self.assertEqual(8, ret[0][1]['meta']) + self.assertEqual(4, ret[0][1]['post_op_meta']) + self.assertEqual(2, ret[0][2]['project_id']) + + def test_inspect_generic_prefix(self): + self.useFixture(mockpatch.PatchObject( + self.inspector._cmdGen, 'bulkCmd', new=faux_bulkCmd_new)) + cache = {} + ret = list(self.inspector.inspect_generic(self.host, + 'test_prefix', + cache)) + keys = cache[ins._CACHE_KEY_OID].keys() + self.assertIn('1.3.6.1.4.1.2021.9.1.8' + '.1', keys) + self.assertIn('1.3.6.1.4.1.2021.9.1.8' + '.2', keys) + self.assertIn('1.3.6.1.4.1.2021.9.1.3' + '.1', keys) + self.assertIn('1.3.6.1.4.1.2021.9.1.3' + '.2', keys) + self.assertEqual(2, len(ret)) + self.assertIn(ret[0][0], (1, 2)) + self.assertEqual(ret[0][0], ret[0][1]['meta']) + + def test_post_op_net(self): + self.useFixture(mockpatch.PatchObject( + self.inspector._cmdGen, 'bulkCmd', new=faux_bulkCmd_new)) + cache = {} + metadata = {} + ret = self.inspector._post_op_net(self.host, cache, None, + value=8, + metadata=metadata, + extra={}, + suffix=".2") + self.assertEqual(8, ret) + self.assertIn('ip', metadata) + self.assertIn("2", metadata['ip']) diff --git a/ceilometer/tests/hardware/pollsters/base.py b/ceilometer/tests/hardware/pollsters/base.py index db98eab9e..401abc547 100644 --- a/ceilometer/tests/hardware/pollsters/base.py +++ b/ceilometer/tests/hardware/pollsters/base.py @@ -50,6 +50,9 @@ class FakeInspector(inspector_base.Inspector): def inspect_network(self, host): yield self.NET + def inspect_generic(self, host, identifier, cache): + yield (None, {}, {}) + class TestPollsterBase(test_base.BaseTestCase): def faux_get_inspector(url, namespace=None):