Merge "Added snmp declarative hardware pollster"

This commit is contained in:
Jenkins 2015-09-03 19:15:26 +00:00 committed by Gerrit Code Review
commit 577ffa8595
8 changed files with 724 additions and 9 deletions
ceilometer
agent
hardware
tests/unit/hardware
inspector
pollsters

@ -24,6 +24,7 @@ from oslo_context import context
from oslo_log import log
import oslo_messaging
import six
from stevedore import extension
from ceilometer.i18n import _
from ceilometer import messaging
@ -232,6 +233,33 @@ class PollsterBase(PluginBase):
"""
@classmethod
def build_pollsters(cls):
"""Return a list of tuple (name, pollster).
The name is the meter name which the pollster would return, the
pollster is a pollster object instance. The pollster which implements
this method should be registered in the namespace of
ceilometer.builder.xxx instead of ceilometer.poll.xxx.
"""
return []
@classmethod
def get_pollsters_extensions(cls):
"""Return a list of stevedore extensions.
The returned stevedore extensions wrap the pollster object instances
returned by build_pollsters.
"""
extensions = []
try:
for name, pollster in cls.build_pollsters():
ext = extension.Extension(name, None, cls, pollster)
extensions.append(ext)
except Exception as err:
raise ExtensionLoadError(err)
return extensions
@six.add_metaclass(abc.ABCMeta)
class DiscoveryBase(object):

@ -25,15 +25,26 @@ import six
@six.add_metaclass(abc.ABCMeta)
class Inspector(object):
@abc.abstractmethod
def inspect_generic(self, host, identifier, cache, extra_metadata=None):
def inspect_generic(self, host, identifier, cache,
extra_metadata=None,
param=None):
"""A generic inspect function.
:param host: the target host
:param identifier: the identifier of the metric
:param cache: cache passed from the pollster
:param extra_metadata: extra dict to be used as metadata
:param param: a dict of inspector specific param
: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 metadata to help constructing sample
"""
def prepare_params(self, param):
"""Parse the params to a format which the inspector itself recognizes.
:param param: inspector params from meter definition file
:return: a dict of param which the inspector recognized
"""
return {}

@ -19,6 +19,7 @@
# under the License.
"""Inspector for collecting data over SNMP"""
import copy
from pysnmp.entity.rfc3413.oneliner import cmdgen
import six
@ -111,7 +112,7 @@ class SNMPInspector(base.Inspector):
_CACHE_KEY_OID = "snmp_cached_oid"
'''
"""
The following mapping define how to construct
(value, metadata, extra) returned by inspect_generic
@ -166,7 +167,7 @@ class SNMPInspector(base.Inspector):
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': {
@ -370,9 +371,14 @@ class SNMPInspector(base.Inspector):
new_oids.append(metadata[0])
return new_oids
def inspect_generic(self, host, identifier, cache, extra_metadata=None):
def inspect_generic(self, host, identifier, cache,
extra_metadata=None,
param=None):
# the snmp definition for the corresponding meter
meter_def = self.MAPPING[identifier]
if param:
meter_def = param
else:
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
@ -389,7 +395,8 @@ class SNMPInspector(base.Inspector):
meter_def['metric_oid'][0],
meter_def['matching_type'],
False)
extra_metadata = extra_metadata or {}
input_extra_metadata = extra_metadata
for oid in oids_for_sample_values:
suffix = oid[len(meter_def['metric_oid'][0]):]
value = self.get_oid_value(oid_cache,
@ -399,6 +406,7 @@ class SNMPInspector(base.Inspector):
metadata = self.construct_metadata(oid_cache,
meter_def['metadata'],
suffix)
extra_metadata = copy.deepcopy(input_extra_metadata) or {}
# call post_op for special cases
if meter_def['post_op']:
func = getattr(self, meter_def['post_op'], None)
@ -431,6 +439,14 @@ class SNMPInspector(base.Inspector):
metadata.update(ip=ip_addr)
return value
def _post_op_disk(self, host, cache, meter_def,
value, metadata, extra, suffix):
if metadata.get('device'):
res_id = extra.get('resource_id') or host.hostname
res_id = res_id + ".%s" % metadata.get('device')
extra.update(resource_id=res_id)
return value
@staticmethod
def _get_auth_strategy(host):
if host.password:
@ -438,5 +454,14 @@ class SNMPInspector(base.Inspector):
authKey=host.password)
else:
auth_strategy = cmdgen.CommunityData(host.username or 'public')
return auth_strategy
def prepare_params(self, param):
processed = {}
processed['matching_type'] = param['matching_type']
processed['metric_oid'] = (param['oid'], eval(param['type']))
processed['post_op'] = param.get('post_op', None)
processed['metadata'] = {}
for k, v in six.iteritems(param.get('metadata', {})):
processed['metadata'][k] = (v['oid'], eval(v['type']))
return processed

@ -0,0 +1,181 @@
---
metric:
# cpu
- name: hardware.cpu.load.1min
unit: process
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.10.1.3.1"
type: "lambda x: float(str(x))"
- name: hardware.cpu.load.5min
unit: process
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.10.1.3.2"
type: "lambda x: float(str(x))"
- name: hardware.cpu.load.15min
unit: process
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.10.1.3.3"
type: "lambda x: float(str(x))"
# disk
- name: hardware.disk.size.total
unit: KB
type: gauge
snmp_inspector:
matching_type: "type_prefix"
oid: "1.3.6.1.4.1.2021.9.1.6"
type: "int"
metadata: &disk_metadata
path:
oid: "1.3.6.1.4.1.2021.9.1.2"
type: "str"
device:
oid: "1.3.6.1.4.1.2021.9.1.3"
type: "str"
post_op: "_post_op_disk"
- name: hardware.disk.size.used
unit: KB
type: gauge
snmp_inspector:
matching_type: "type_prefix"
oid: "1.3.6.1.4.1.2021.9.1.8"
type: "int"
metadata: *disk_metadata
post_op: "_post_op_disk"
# memory
- name: hardware.memory.total
unit: KB
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.4.5.0"
type: "int"
- name: hardware.memory.used
unit: KB
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.4.6.0"
type: "int"
post_op: "_post_op_memory_avail_to_used"
- name: hardware.memory.swap.total
unit: KB
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.4.3.0"
type: "int"
- name: hardware.memory.swap.avail
unit: KB
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.4.4.0"
type: "int"
- name: hardware.memory.buffer
unit: KB
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.4.14.0"
type: "int"
- name: hardware.memory.cached
unit: KB
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.4.15.0"
type: "int"
# network interface
- name: hardware.network.incoming.bytes
unit: B
type: cumulative
snmp_inspector:
matching_type: "type_prefix"
oid: "1.3.6.1.2.1.2.2.1.10"
type: "int"
metadata: &net_metadata
name:
oid: "1.3.6.1.2.1.2.2.1.2"
type: "str"
speed:
oid: "1.3.6.1.2.1.2.2.1.5"
type: "lambda x: int(x) / 8"
mac:
oid: "1.3.6.1.2.1.2.2.1.6"
type: "lambda x: x.prettyPrint().replace('0x', '')"
post_op: "_post_op_net"
- name: hardware.network.outgoing.bytes
unit: B
type: cumulative
snmp_inspector:
matching_type: "type_prefix"
oid: "1.3.6.1.2.1.2.2.1.16"
type: "int"
metadata: *net_metadata
post_op: "_post_op_net"
- name: hardware.network.outgoing.errors
unit: packet
type: cumulative
snmp_inspector:
matching_type: "type_prefix"
oid: "1.3.6.1.2.1.2.2.1.20"
type: "int"
metadata: *net_metadata
post_op: "_post_op_net"
#network aggregate
- name: hardware.network.ip.outgoing.datagrams
unit: datagrams
type: cumulative
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.2.1.4.10.0"
type: "int"
- name: hardware.network.ip.incoming.datagrams
unit: datagrams
type: cumulative
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.2.1.4.3.0"
type: "int"
#system stats
- name: hardware.system_stats.cpu.idle
unit: "%"
type: gauge
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.11.11.0"
type: "int"
- name: hardware.system_stats.io.outgoing.blocks
unit: blocks
type: cumulative
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.11.57.0"
type: "int"
- name: hardware.system_stats.io.incoming.blocks
unit: blocks
type: cumulative
snmp_inspector:
matching_type: "type_exact"
oid: "1.3.6.1.4.1.2021.11.58.0"
type: "int"

@ -0,0 +1,264 @@
#
# Copyright 2015 Intel Corp.
#
# 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 itertools
import os
import pkg_resources
import yaml
from oslo_config import cfg
from oslo_log import log
from oslo_utils import netutils
import six
from ceilometer.agent import plugin_base
from ceilometer.hardware import inspector as insloader
from ceilometer.hardware.pollsters import util
from ceilometer.i18n import _LE, _LI, _LW
from ceilometer import sample
OPTS = [
cfg.StrOpt('meter_definitions_file',
default="snmp.yaml",
help="Configuration file for defining hardware snmp meters."
),
]
cfg.CONF.register_opts(OPTS, group='hardware')
LOG = log.getLogger(__name__)
class MeterDefinitionException(Exception):
def __init__(self, message, definition_cfg):
super(MeterDefinitionException, self).__init__(message)
self.message = message
self.definition_cfg = definition_cfg
def __str__(self):
return '%s %s: %s' % (self.__class__.__name__,
self.definition_cfg, self.message)
class MeterDefinition(object):
required_fields = ['name', 'unit', 'type']
def __init__(self, definition_cfg):
self.cfg = definition_cfg
for fname, fval in self.cfg.items():
if (isinstance(fname, six.string_types) and
(fname in self.required_fields or
fname.endswith('_inspector'))):
setattr(self, fname, fval)
else:
LOG.warn(_LW("Ignore unrecognized field %s"), fname)
for fname in self.required_fields:
if not getattr(self, fname, None):
raise MeterDefinitionException(
_LE("Missing field %s") % fname, self.cfg)
if self.type not in sample.TYPES:
raise MeterDefinitionException(
_LE("Unrecognized type value %s") % self.type, self.cfg)
class GenericHardwareDeclarativePollster(plugin_base.PollsterBase):
CACHE_KEY = 'hardware.generic'
mapping = None
def __init__(self):
super(GenericHardwareDeclarativePollster, self).__init__()
self.inspectors = {}
def _update_meter_definition(self, definition):
self.meter_definition = definition
self.cached_inspector_params = {}
@property
def default_discovery(self):
return 'tripleo_overcloud_nodes'
@staticmethod
def _parse_resource(res):
"""Parse resource from discovery.
Either URL can be given or dict. Dict has to contain at least
keys 'resource_id' and 'resource_url', all the dict keys will be stored
as metadata.
:param res: URL or dict containing all resource info.
:return parsed_url, resource_id, metadata: Returns parsed URL used for
SNMP query, unique identifier of the resource and metadata
of the resource.
"""
parsed_url, resource_id, metadata = (None, None, None)
if isinstance(res, dict):
if 'resource_url' not in res or 'resource_id' not in res:
LOG.error(_LE('Passed resource dict must contain keys '
'resource_id and resource_url.'))
else:
metadata = res
parsed_url = netutils.urlsplit(res['resource_url'])
resource_id = res['resource_id']
else:
metadata = {}
parsed_url = netutils.urlsplit(res)
resource_id = res
return parsed_url, resource_id, metadata
def _get_inspector(self, parsed_url):
if parsed_url.scheme not in self.inspectors:
try:
driver = insloader.get_inspector(parsed_url)
self.inspectors[parsed_url.scheme] = driver
except Exception as err:
LOG.exception(_LE("Cannot load inspector %(name)s: %(err)s"),
dict(name=parsed_url.scheme,
err=err))
raise err
return self.inspectors[parsed_url.scheme]
def get_samples(self, manager, cache, resources=None):
"""Return an iterable of Sample instances from polling the resources.
:param manager: The service manager invoking the plugin
:param cache: A dictionary for passing data between plugins
:param resources: end point to poll data from
"""
resources = resources or []
h_cache = cache.setdefault(self.CACHE_KEY, {})
sample_iters = []
# Get the meter identifiers to poll
identifier = self.meter_definition.name
for resource in resources:
parsed_url, res, extra_metadata = self._parse_resource(resource)
if parsed_url is None:
LOG.error(_LE("Skip invalid resource %s"), resource)
continue
ins = self._get_inspector(parsed_url)
try:
# Call hardware inspector to poll for the data
i_cache = h_cache.setdefault(res, {})
# Prepare inspector parameters and cache it for performance
param_key = parsed_url.scheme + '.' + identifier
inspector_param = self.cached_inspector_params.get(param_key)
if not inspector_param:
param = getattr(self.meter_definition,
parsed_url.scheme + '_inspector', {})
inspector_param = ins.prepare_params(param)
self.cached_inspector_params[param_key] = inspector_param
if identifier not in i_cache:
i_cache[identifier] = list(ins.inspect_generic(
parsed_url,
identifier,
i_cache,
extra_metadata,
param=inspector_param))
# Generate samples
if i_cache[identifier]:
sample_iters.append(self.generate_samples(
parsed_url,
i_cache[identifier]))
except Exception as err:
LOG.exception(_LE('inspector call failed for %(ident)s '
'host %(host)s: %(err)s'),
dict(ident=identifier,
host=parsed_url.hostname,
err=err))
return itertools.chain(*sample_iters)
def generate_samples(self, host_url, data):
"""Generate a list of Sample from the data returned by inspector
:param host_url: host url of the endpoint
:param data: list of data returned by the corresponding inspector
"""
samples = []
definition = self.meter_definition
for (value, metadata, extra) in data:
s = util.make_sample_from_host(host_url,
name=definition.name,
sample_type=definition.type,
unit=definition.unit,
volume=value,
res_metadata=metadata,
extra=extra,
name_prefix=None)
samples.append(s)
return samples
@classmethod
def build_pollsters(cls):
if not cls.mapping:
cls.mapping = load_definition(setup_meters_config())
pollsters = []
for name in cls.mapping:
pollster = cls()
pollster._update_meter_definition(cls.mapping[name])
pollsters.append((name, pollster))
return pollsters
def get_config_file():
config_file = cfg.CONF.hardware.meter_definitions_file
if not os.path.exists(config_file):
config_file = cfg.CONF.find_file(config_file)
if not config_file:
config_file = pkg_resources.resource_filename(
__name__, "data/snmp.yaml")
return config_file
def setup_meters_config():
"""load the meters definitions from yaml config file."""
config_file = get_config_file()
LOG.debug("Hardware snmp meter definition file: %s" % config_file)
with open(config_file) as cf:
config = cf.read()
try:
meters_config = yaml.safe_load(config)
except yaml.YAMLError as err:
if hasattr(err, 'problem_mark'):
mark = err.problem_mark
errmsg = (_LE("Invalid YAML syntax in Meter Definitions file "
"%(file)s at line: %(line)s, column: %(column)s.")
% dict(file=config_file,
line=mark.line + 1,
column=mark.column + 1))
else:
errmsg = (_LE("YAML error reading Meter Definitions file "
"%(file)s")
% dict(file=config_file))
LOG.error(errmsg)
raise
LOG.info(_LI("Meter Definitions: %s") % meters_config)
return meters_config
def load_definition(config_def):
mappings = {}
for meter_def in config_def.get('metric', []):
meter = MeterDefinition(meter_def)
mappings[meter.name] = meter
return mappings

@ -41,15 +41,18 @@ def make_resource_metadata(res_metadata=None, host_url=None):
def make_sample_from_host(host_url, name, sample_type, unit, volume,
project_id=None, user_id=None, resource_id=None,
res_metadata=None, extra=None):
res_metadata=None, extra=None,
name_prefix='hardware'):
extra = extra or {}
resource_metadata = make_resource_metadata(res_metadata, host_url)
resource_metadata.update(extra)
res_id = resource_id or extra.get('resource_id') or host_url.hostname
if name_prefix:
name = name_prefix + '.' + name
return sample.Sample(
name='hardware.' + name,
name=name,
type=sample_type,
unit=unit,
volume=volume,

@ -143,3 +143,38 @@ class TestSNMPInspector(test_base.BaseTestCase):
self.assertEqual(8, ret)
self.assertIn('ip', metadata)
self.assertIn("2", metadata['ip'])
def test_post_op_disk(self):
cache = {}
metadata = dict(device='/dev/sda1',
path='/')
extra = {}
ret = self.inspector._post_op_disk(self.host, cache, None,
value=8,
metadata=metadata,
extra=extra,
suffix=None)
self.assertEqual(8, ret)
self.assertIn('resource_id', extra)
self.assertEqual("localhost./dev/sda1", extra['resource_id'])
def test_prepare_params(self):
param = {'post_op': '_post_op_disk',
'oid': '1.3.6.1.4.1.2021.9.1.6',
'type': 'int',
'matching_type': 'type_prefix',
'metadata': {
'device': {'oid': '1.3.6.1.4.1.2021.9.1.3',
'type': 'str'},
'path': {'oid': '1.3.6.1.4.1.2021.9.1.2',
'type': "lambda x: str(x)"}}}
processed = self.inspector.prepare_params(param)
self.assertEqual('_post_op_disk', processed['post_op'])
self.assertEqual('1.3.6.1.4.1.2021.9.1.6', processed['metric_oid'][0])
self.assertEqual(int, processed['metric_oid'][1])
self.assertEqual(snmp.PREFIX, processed['matching_type'])
self.assertEqual(2, len(processed['metadata'].keys()))
self.assertEqual('1.3.6.1.4.1.2021.9.1.2',
processed['metadata']['path'][0])
self.assertEqual("4",
processed['metadata']['path'][1](4))

@ -0,0 +1,168 @@
#
# Copyright 2015 Intel Corp.
#
# 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 mock
import six
import yaml
from oslo_config import fixture as fixture_config
from oslo_utils import fileutils
from oslotest import mockpatch
from ceilometer.hardware.inspector import base as inspector_base
from ceilometer.hardware.pollsters import generic
from ceilometer import sample
from ceilometer import service as ceilometer_service
from ceilometer.tests import base as test_base
class TestMeterDefinition(test_base.BaseTestCase):
def test_config_definition(self):
cfg = dict(name='test',
type='gauge',
unit='B',
snmp_inspector={})
definition = generic.MeterDefinition(cfg)
self.assertEqual('test', definition.name)
self.assertEqual('gauge', definition.type)
self.assertEqual('B', definition.unit)
self.assertEqual({}, definition.snmp_inspector)
def test_config_missing_field(self):
cfg = dict(name='test', type='gauge')
try:
generic.MeterDefinition(cfg)
except generic.MeterDefinitionException as e:
self.assertEqual("Missing field unit", e.message)
def test_config_invalid_field(self):
cfg = dict(name='test',
type='gauge',
unit='B',
invalid={})
definition = generic.MeterDefinition(cfg)
self.assertEqual("foobar", getattr(definition, 'invalid', 'foobar'))
def test_config_invalid_type_field(self):
cfg = dict(name='test',
type='invalid',
unit='B',
snmp_inspector={})
try:
generic.MeterDefinition(cfg)
except generic.MeterDefinitionException as e:
self.assertEqual("Unrecognized type value invalid", e.message)
class FakeInspector(inspector_base.Inspector):
net_metadata = dict(name='test.teest',
mac='001122334455',
ip='10.0.0.2',
speed=1000)
DATA = {
'hardware.test1': (0.99, {}, {}),
'hardware.test2.abc': (90, net_metadata, {}),
}
def inspect_generic(self, host, identifier, cache,
extra_metadata=None, param=None):
yield self.DATA[identifier]
class TestGenericPollsters(test_base.BaseTestCase):
@staticmethod
def faux_get_inspector(url, namespace=None):
return FakeInspector()
def setUp(self):
super(TestGenericPollsters, self).setUp()
self.conf = self.useFixture(fixture_config.Config()).conf
self.resources = ["snmp://test", "snmp://test2"]
self.useFixture(mockpatch.Patch(
'ceilometer.hardware.inspector.get_inspector',
self.faux_get_inspector))
ceilometer_service.prepare_service(argv=[], config_files=[])
self.pollster = generic.GenericHardwareDeclarativePollster()
def test_fallback_meter_path(self):
self.useFixture(mockpatch.PatchObject(self.conf,
'find_file', return_value=None))
fall_bak_path = generic.get_config_file()
self.assertIn("hardware/pollsters/data/snmp.yaml", fall_bak_path)
def __setup_meter_def_file(self, cfg):
if six.PY3:
cfg = cfg.encode('utf-8')
meter_cfg_file = fileutils.write_to_tempfile(content=cfg,
prefix="snmp",
suffix="yaml")
self.conf.set_override(
'meter_definitions_file',
meter_cfg_file, group='hardware')
cfg = generic.setup_meters_config()
return cfg
@mock.patch('ceilometer.pipeline.setup_pipeline', mock.MagicMock())
def _check_get_samples(self, name, definition,
expected_value, expected_type, expected_unit=None):
self.pollster._update_meter_definition(definition)
cache = {}
samples = list(self.pollster.get_samples(None, cache,
self.resources))
self.assertTrue(samples)
self.assertIn(self.pollster.CACHE_KEY, cache)
for resource in self.resources:
self.assertIn(resource, cache[self.pollster.CACHE_KEY])
self.assertEqual(set([name]),
set([s.name for s in samples]))
match = [s for s in samples if s.name == name]
self.assertEqual(expected_value, match[0].volume)
self.assertEqual(expected_type, match[0].type)
if expected_unit:
self.assertEqual(expected_unit, match[0].unit)
def test_get_samples(self):
param = dict(matching_type='type_exact',
oid='1.3.6.1.4.1.2021.10.1.3.1',
type='lambda x: float(str(x))')
meter_def = generic.MeterDefinition(dict(type='gauge',
name='hardware.test1',
unit='process',
snmp_inspector=param))
self._check_get_samples('hardware.test1',
meter_def,
0.99, sample.TYPE_GAUGE,
expected_unit='process')
def test_get_pollsters_extensions(self):
param = dict(matching_type='type_exact',
oid='1.3.6.1.4.1.2021.10.1.3.1',
type='lambda x: float(str(x))')
meter_cfg = yaml.dump(
{'metric': [dict(type='gauge',
name='hardware.test1',
unit='process',
snmp_inspector=param),
dict(type='gauge',
name='hardware.test2.abc',
unit='process',
snmp_inspector=param)]})
self.__setup_meter_def_file(meter_cfg)
pollster = generic.GenericHardwareDeclarativePollster
exts = pollster.get_pollsters_extensions()
self.assertEqual(2, len(exts))
self.assertIn(exts[0].name, ['hardware.test1', 'hardware.test2.abc'])
self.assertIn(exts[1].name, ['hardware.test1', 'hardware.test2.abc'])