Add support to host command dynamic pollster definitions

Problem description
===================
Today we have some hardcoded pollsters that are gathering
data from running virtual machines through libvirt or
different programs running in the compute nodes. However,
the Dynamic pollster definition does not support this kind of
operations to gather data, it only supports HTTP Rest
requests to collect data. Therefore, it is not possible to
use the dynamic pollster definition to create a YML based
pollster that runs and collects data from Libvirt in the
compute nodes.

Proposal
========
To allow host commands/scripts in the Dynamic pollsters,
we propose to add a new pollster definition using the
`os.subprocess` lib to run host commands to collect
Host/VMs data and store them in the configured backend.
This will provide more flexibility and make the
Dynamic pollsters able to be used in Ceilometer compute
 instances as well.

Change-Id: I50b8dc341ce457780416b41d138e35f5a0d083b6
Depends-On: https://review.opendev.org/c/openstack/ceilometer/+/850253
This commit is contained in:
Pedro Henrique 2022-08-03 10:09:00 -03:00
parent 225f1cd776
commit cb448a1dbc
4 changed files with 758 additions and 172 deletions

View File

@ -20,6 +20,7 @@
import copy
import json
import re
import subprocess
import time
import xmltodict
@ -205,7 +206,7 @@ class PollsterSampleExtractor(object):
metadata=metadata, pollster_definitions=pollster_definitions)
extra_metadata = self.definitions.retrieve_extra_metadata(
kwargs['manager'], pollster_sample)
kwargs['manager'], pollster_sample, kwargs['conf'])
for key in extra_metadata.keys():
if key in metadata.keys():
@ -518,7 +519,8 @@ class PollsterDefinitions(object):
default=3600),
PollsterDefinition(name='extra_metadata_fields'),
PollsterDefinition(name='response_handlers', default=['json'],
validator=validate_response_handler)
validator=validate_response_handler),
PollsterDefinition(name='base_metadata', default={})
]
extra_definitions = []
@ -572,114 +574,75 @@ class PollsterDefinitions(object):
"Required fields %s not specified."
% missing, self.configurations)
def retrieve_extra_metadata(self, manager, request_sample):
def retrieve_extra_metadata(self, manager, request_sample, pollster_conf):
extra_metadata_fields = self.configurations['extra_metadata_fields']
if extra_metadata_fields:
if isinstance(self, NonOpenStackApisPollsterDefinition):
raise declarative.NonOpenStackApisDynamicPollsterException(
"Not supported the use of extra metadata gathering for "
"non-openstack pollsters [%s] (yet)."
% self.configurations['name'])
return self._retrieve_extra_metadata(
extra_metadata_fields, manager, request_sample)
extra_metadata_samples = {}
extra_metadata_by_name = {}
if not isinstance(extra_metadata_fields, (list, tuple)):
extra_metadata_fields = [extra_metadata_fields]
for ext_metadata in extra_metadata_fields:
ext_metadata.setdefault(
'sample_type', self.configurations['sample_type'])
ext_metadata.setdefault('unit', self.configurations['unit'])
ext_metadata.setdefault(
'value_attribute', ext_metadata.get(
'value', self.configurations['value_attribute']))
ext_metadata['base_metadata'] = {
'extra_metadata_captured': extra_metadata_samples,
'extra_metadata_by_name': extra_metadata_by_name,
'sample': request_sample
}
parent_cache_ttl = self.configurations[
'extra_metadata_fields_cache_seconds']
cache_ttl = ext_metadata.get(
'extra_metadata_fields_cache_seconds', parent_cache_ttl
)
response_cache = self.response_cache
extra_metadata_pollster = DynamicPollster(
ext_metadata, conf=pollster_conf, cache_ttl=cache_ttl,
extra_metadata_responses_cache=response_cache,
)
resources = [None]
if ext_metadata.get('endpoint_type'):
resources = manager.discover([
extra_metadata_pollster.default_discovery], {})
samples = extra_metadata_pollster.get_samples(
manager, None, resources)
for sample in samples:
self.fill_extra_metadata_samples(
extra_metadata_by_name,
extra_metadata_samples,
sample)
return extra_metadata_samples
LOG.debug("No extra metadata to be captured for pollsters [%s] and "
"request sample [%s].", self.definitions, request_sample)
return {}
def _retrieve_extra_metadata(
self, extra_metadata_fields, manager, request_sample):
LOG.debug("Processing extra metadata fields [%s] for "
"sample [%s].", extra_metadata_fields,
request_sample)
extra_metadata_captured = {}
for extra_metadata in extra_metadata_fields:
extra_metadata_name = extra_metadata['name']
if extra_metadata_name in extra_metadata_captured.keys():
LOG.warning("Duplicated extra metadata name [%s]. Therefore, "
"we do not process this iteration [%s].",
extra_metadata_name, extra_metadata)
def fill_extra_metadata_samples(self, extra_metadata_by_name,
extra_metadata_samples, sample):
extra_metadata_samples[sample.name] = sample.volume
LOG.debug("Merging the sample metadata [%s] of the "
"extra_metadata_field [%s], with the "
"extra_metadata_samples [%s].",
sample.resource_metadata,
sample.name,
extra_metadata_samples)
for key, value in sample.resource_metadata.items():
if value is None and key in extra_metadata_samples:
LOG.debug("Metadata [%s] for extra_metadata_field [%s] "
"is None, skipping metadata override by None "
"value", key, sample.name)
continue
extra_metadata_samples[key] = value
extra_metadata_by_name[sample.name] = {
'value': sample.volume,
'metadata': sample.resource_metadata
}
LOG.debug("Processing extra metadata [%s] for sample [%s].",
extra_metadata_name, request_sample)
endpoint_type = 'endpoint:' + extra_metadata['endpoint_type']
if not endpoint_type.endswith(
PollsterDefinitions.EXTERNAL_ENDPOINT_TYPE):
response = self.execute_openstack_extra_metadata_gathering(
endpoint_type, extra_metadata, manager, request_sample,
extra_metadata_captured)
else:
raise declarative.NonOpenStackApisDynamicPollsterException(
"Not supported the use of extra metadata gathering for "
"non-openstack endpoints [%s] (yet)." % extra_metadata)
extra_metadata_extractor_kwargs = {
'value_attribute': extra_metadata['value'],
'sample': request_sample}
extra_metadata_value = \
self.sample_extractor.retrieve_attribute_nested_value(
response, **extra_metadata_extractor_kwargs)
LOG.debug("Generated extra metadata [%s] with value [%s].",
extra_metadata_name, extra_metadata_value)
extra_metadata_captured[extra_metadata_name] = extra_metadata_value
return extra_metadata_captured
def execute_openstack_extra_metadata_gathering(self, endpoint_type,
extra_metadata, manager,
request_sample,
extra_metadata_captured):
url_for_endpoint_type = manager.discover(
[endpoint_type], self.response_cache)
LOG.debug("URL [%s] found for endpoint type [%s].",
url_for_endpoint_type, endpoint_type)
if url_for_endpoint_type:
url_for_endpoint_type = url_for_endpoint_type[0]
self.sample_gatherer.generate_url_path(
extra_metadata, request_sample, extra_metadata_captured)
cached_response, max_ttl_for_cache = self.response_cache.get(
extra_metadata['url_path'], (None, None))
extra_metadata_fields_cache_seconds = extra_metadata.get(
'extra_metadata_fields_cache_seconds',
self.configurations['extra_metadata_fields_cache_seconds'])
current_time = time.time()
if cached_response and max_ttl_for_cache >= current_time:
LOG.debug("Returning response [%s] for request [%s] as the TTL "
"[max=%s, current_time=%s] has not expired yet.",
cached_response, extra_metadata['url_path'],
max_ttl_for_cache, current_time)
return cached_response
if cached_response:
LOG.debug("Cleaning cached response [%s] for request [%s] "
"as the TTL [max=%s, current_time=%s] has expired.",
cached_response, extra_metadata['url_path'],
max_ttl_for_cache, current_time)
response = self.sample_gatherer.execute_request_for_definitions(
extra_metadata, **{'manager': manager,
'keystone_client': manager._keystone,
'resource': url_for_endpoint_type,
'execute_id_overrides': False})
max_ttl_for_cache = time.time() + extra_metadata_fields_cache_seconds
cache_tuple = (response, max_ttl_for_cache)
self.response_cache[extra_metadata['url_path']] = cache_tuple
return response
LOG.debug("extra_metadata_samples after merging: [%s].",
extra_metadata_samples)
class MultiMetricPollsterDefinitions(PollsterDefinitions):
@ -739,6 +702,42 @@ class PollsterSampleGatherer(object):
url_path=definitions.configurations['url_path']
)
def get_cache_key(self, definitions, **kwargs):
return self.get_request_linked_samples_url(kwargs, definitions)
def get_cached_response(self, definitions, **kwargs):
if self.definitions.cache_ttl == 0:
return
cache_key = self.get_cache_key(definitions, **kwargs)
response_cache = self.definitions.response_cache
cached_response, max_ttl_for_cache = response_cache.get(
cache_key, (None, None))
current_time = time.time()
if cached_response and max_ttl_for_cache >= current_time:
LOG.debug("Returning response [%s] for request [%s] as the TTL "
"[max=%s, current_time=%s] has not expired yet.",
cached_response, definitions,
max_ttl_for_cache, current_time)
return cached_response
if cached_response and max_ttl_for_cache < current_time:
LOG.debug("Cleaning cached response [%s] for request [%s] "
"as the TTL [max=%s, current_time=%s] has expired.",
cached_response, definitions,
max_ttl_for_cache, current_time)
response_cache.pop(cache_key, None)
def store_cached_response(self, definitions, resp, **kwargs):
if self.definitions.cache_ttl == 0:
return
cache_key = self.get_cache_key(definitions, **kwargs)
extra_metadata_fields_cache_seconds = self.definitions.cache_ttl
max_ttl_for_cache = time.time() + extra_metadata_fields_cache_seconds
cache_tuple = (resp, max_ttl_for_cache)
self.definitions.response_cache[cache_key] = cache_tuple
@property
def default_discovery(self):
return 'endpoint:' + self.definitions.configurations['endpoint_type']
@ -748,10 +747,14 @@ class PollsterSampleGatherer(object):
self.definitions.configurations, **kwargs)
def execute_request_for_definitions(self, definitions, **kwargs):
resp, url = self._internal_execute_request_get_samples(
definitions=definitions, **kwargs)
if response_dict := self.get_cached_response(definitions, **kwargs):
url = 'cached'
else:
resp, url = self._internal_execute_request_get_samples(
definitions=definitions, **kwargs)
response_dict = self.response_handler_chain.handle(resp.text)
self.store_cached_response(definitions, response_dict, **kwargs)
response_dict = self.response_handler_chain.handle(resp.text)
entry_size = len(response_dict)
LOG.debug("Entries [%s] in the DICT for request [%s] "
"for dynamic pollster [%s].",
@ -790,21 +793,6 @@ class PollsterSampleGatherer(object):
self.generate_new_attributes_in_sample(
request_sample, resource_id_attribute, 'id')
def generate_url_path(self, extra_metadata, sample,
extra_metadata_captured):
if not extra_metadata.get('url_path_original'):
extra_metadata[
'url_path_original'] = extra_metadata['url_path']
extra_metadata['url_path'] = eval(
extra_metadata['url_path_original'])
LOG.debug("URL [%s] generated for pattern [%s] for sample [%s] and "
"extra metadata captured [%s].",
extra_metadata['url_path'],
extra_metadata['url_path_original'], sample,
extra_metadata_captured)
def generate_new_attributes_in_sample(
self, sample, attribute_key, new_attribute_key):
@ -881,6 +869,15 @@ class PollsterSampleGatherer(object):
def get_request_url(self, kwargs, url_path):
endpoint = kwargs['resource']
params = copy.deepcopy(
self.definitions.configurations.get(
'base_metadata', {}))
try:
url_path = eval(url_path, params)
except Exception:
LOG.debug("Cannot eval path [%s] with params [%s],"
" using [%s] instead.",
url_path, params, url_path)
return urlparse.urljoin(endpoint, url_path)
def retrieve_entries_from_response(self, response_json, definitions):
@ -919,6 +916,57 @@ class NonOpenStackApisPollsterDefinition(PollsterDefinitions):
return configurations.get('module')
class HostCommandPollsterDefinition(PollsterDefinitions):
extra_definitions = [
PollsterDefinition(name='endpoint_type', required=False),
PollsterDefinition(name='url_path', required=False),
PollsterDefinition(name='host_command', required=True)]
def __init__(self, configurations):
super(HostCommandPollsterDefinition, self).__init__(
configurations)
self.sample_gatherer = HostCommandSamplesGatherer(self)
@staticmethod
def is_field_applicable_to_definition(configurations):
return configurations.get('host_command')
class HostCommandSamplesGatherer(PollsterSampleGatherer):
class Response(object):
def __init__(self, text):
self.text = text
def get_cache_key(self, definitions, **kwargs):
return self.get_command(definitions)
def _internal_execute_request_get_samples(self, definitions, **kwargs):
command = self.get_command(definitions, **kwargs)
LOG.debug('Running Host command: [%s]', command)
result = subprocess.getoutput(command)
LOG.debug('Host command [%s] result: [%s]', command, result)
return self.Response(result), command
def get_command(self, definitions, next_sample_url=None, **kwargs):
command = next_sample_url or definitions['host_command']
params = copy.deepcopy(
self.definitions.configurations.get(
'base_metadata', {}))
try:
command = eval(command, params)
except Exception:
LOG.debug("Cannot eval command [%s] with params [%s],"
" using [%s] instead.",
command, params, command)
return command
@property
def default_discovery(self):
return 'local_node'
class NonOpenStackApisSamplesGatherer(PollsterSampleGatherer):
@property
@ -996,8 +1044,10 @@ class DynamicPollster(plugin_base.PollsterBase):
# Mandatory name field
name = ""
def __init__(self, pollster_definitions={}, conf=None,
supported_definitions=[NonOpenStackApisPollsterDefinition,
def __init__(self, pollster_definitions={}, conf=None, cache_ttl=0,
extra_metadata_responses_cache=None,
supported_definitions=[HostCommandPollsterDefinition,
NonOpenStackApisPollsterDefinition,
MultiMetricPollsterDefinitions,
SingleMetricPollsterDefinitions]):
super(DynamicPollster, self).__init__(conf)
@ -1007,6 +1057,10 @@ class DynamicPollster(plugin_base.PollsterBase):
self.definitions = PollsterDefinitionBuilder(
self.supported_definitions).build_definitions(pollster_definitions)
self.definitions.cache_ttl = cache_ttl
self.definitions.response_cache = extra_metadata_responses_cache
if extra_metadata_responses_cache is None:
self.definitions.response_cache = {}
self.pollster_definitions = self.definitions.configurations
if 'metadata_fields' in self.pollster_definitions:
LOG.debug("Metadata fields configured to [%s].",
@ -1040,9 +1094,12 @@ class DynamicPollster(plugin_base.PollsterBase):
for r in resources:
LOG.debug("Executing get sample for resource [%s].", r)
samples = self.load_samples(r, manager)
if not isinstance(samples, (list, tuple)):
samples = [samples]
for pollster_sample in samples:
kwargs = {'manager': manager, 'resource': r}
sample = self.extract_sample(pollster_sample, **kwargs)
sample = self.extract_sample(
pollster_sample, manager=manager,
resource=r, conf=self.conf)
if isinstance(sample, SkippedSample):
continue
yield from sample

View File

@ -388,6 +388,424 @@ class TestDynamicPollster(base.BaseTestCase):
self.assertEqual(4, len(samples))
@mock.patch('keystoneclient.v2_0.client.Client')
def test_execute_request_extra_metadata_fields_cache_disabled(
self, client_mock):
definitions = copy.deepcopy(
self.pollster_definition_only_required_fields)
extra_metadata_fields = {
'extra_metadata_fields_cache_seconds': 0,
'name': "project_name",
'endpoint_type': "identity",
'url_path': "'/v3/projects/' + str(sample['project_id'])",
'value': "name",
}
definitions['value_attribute'] = 'project_id'
definitions['extra_metadata_fields'] = extra_metadata_fields
pollster = dynamic_pollster.DynamicPollster(definitions)
return_value = self.FakeResponse()
return_value.status_code = requests.codes.ok
return_value._text = '''
{"projects": [
{"project_id": 9999, "name": "project1"},
{"project_id": 8888, "name": "project2"},
{"project_id": 7777, "name": "project3"},
{"project_id": 9999, "name": "project1"},
{"project_id": 8888, "name": "project2"},
{"project_id": 7777, "name": "project3"},
{"project_id": 9999, "name": "project1"},
{"project_id": 8888, "name": "project2"},
{"project_id": 7777, "name": "project3"}]
}
'''
return_value9999 = self.FakeResponse()
return_value9999.status_code = requests.codes.ok
return_value9999._text = '''
{"project":
{"project_id": 9999, "name": "project1"}
}
'''
return_value8888 = self.FakeResponse()
return_value8888.status_code = requests.codes.ok
return_value8888._text = '''
{"project":
{"project_id": 8888, "name": "project2"}
}
'''
return_value7777 = self.FakeResponse()
return_value7777.status_code = requests.codes.ok
return_value7777._text = '''
{"project":
{"project_id": 7777, "name": "project3"}
}
'''
def get(url, *args, **kwargs):
if '9999' in url:
return return_value9999
if '8888' in url:
return return_value8888
if '7777' in url:
return return_value7777
return return_value
client_mock.session.get.side_effect = get
manager = mock.Mock
manager._keystone = client_mock
def discover(*args, **kwargs):
return ["https://endpoint.server.name/"]
manager.discover = discover
samples = pollster.get_samples(
manager=manager, cache=None,
resources=["https://endpoint.server.name/"])
samples = list(samples)
n_calls = client_mock.session.get.call_count
self.assertEqual(9, len(samples))
self.assertEqual(10, n_calls)
@mock.patch('keystoneclient.v2_0.client.Client')
def test_execute_request_extra_metadata_fields_cache_enabled(
self, client_mock):
definitions = copy.deepcopy(
self.pollster_definition_only_required_fields)
extra_metadata_fields = {
'extra_metadata_fields_cache_seconds': 3600,
'name': "project_name",
'endpoint_type': "identity",
'url_path': "'/v3/projects/' + str(sample['project_id'])",
'value': "name",
}
definitions['value_attribute'] = 'project_id'
definitions['extra_metadata_fields'] = extra_metadata_fields
pollster = dynamic_pollster.DynamicPollster(definitions)
return_value = self.FakeResponse()
return_value.status_code = requests.codes.ok
return_value._text = '''
{"projects": [
{"project_id": 9999, "name": "project1"},
{"project_id": 8888, "name": "project2"},
{"project_id": 7777, "name": "project3"},
{"project_id": 9999, "name": "project4"},
{"project_id": 8888, "name": "project5"},
{"project_id": 7777, "name": "project6"},
{"project_id": 9999, "name": "project7"},
{"project_id": 8888, "name": "project8"},
{"project_id": 7777, "name": "project9"}]
}
'''
return_value9999 = self.FakeResponse()
return_value9999.status_code = requests.codes.ok
return_value9999._text = '''
{"project":
{"project_id": 9999, "name": "project1"}
}
'''
return_value8888 = self.FakeResponse()
return_value8888.status_code = requests.codes.ok
return_value8888._text = '''
{"project":
{"project_id": 8888, "name": "project2"}
}
'''
return_value7777 = self.FakeResponse()
return_value7777.status_code = requests.codes.ok
return_value7777._text = '''
{"project":
{"project_id": 7777, "name": "project3"}
}
'''
def get(url, *args, **kwargs):
if '9999' in url:
return return_value9999
if '8888' in url:
return return_value8888
if '7777' in url:
return return_value7777
return return_value
client_mock.session.get.side_effect = get
manager = mock.Mock
manager._keystone = client_mock
def discover(*args, **kwargs):
return ["https://endpoint.server.name/"]
manager.discover = discover
samples = pollster.get_samples(
manager=manager, cache=None,
resources=["https://endpoint.server.name/"])
samples = list(samples)
n_calls = client_mock.session.get.call_count
self.assertEqual(9, len(samples))
self.assertEqual(4, n_calls)
@mock.patch('keystoneclient.v2_0.client.Client')
def test_execute_request_extra_metadata_fields(
self, client_mock):
definitions = copy.deepcopy(
self.pollster_definition_only_required_fields)
extra_metadata_fields = [{
'name': "project_name",
'endpoint_type': "identity",
'url_path': "'/v3/projects/' + str(sample['project_id'])",
'value': "name",
'metadata_fields': ['meta']
}, {
'name': "project_alias",
'endpoint_type': "identity",
'url_path': "'/v3/projects/' + "
"str(extra_metadata_captured['project_name'])",
'value': "name",
'metadata_fields': ['meta']
}, {
'name': "project_meta",
'endpoint_type': "identity",
'url_path': "'/v3/projects/' + "
"str(extra_metadata_by_name['project_name']"
"['metadata']['meta'])",
'value': "project_id",
'metadata_fields': ['meta']
}]
definitions['value_attribute'] = 'project_id'
definitions['extra_metadata_fields'] = extra_metadata_fields
pollster = dynamic_pollster.DynamicPollster(definitions)
return_value = self.FakeResponse()
return_value.status_code = requests.codes.ok
return_value._text = '''
{"projects": [
{"project_id": 9999, "name": "project1"},
{"project_id": 8888, "name": "project2"},
{"project_id": 7777, "name": "project3"}]
}
'''
return_value9999 = self.FakeResponse()
return_value9999.status_code = requests.codes.ok
return_value9999._text = '''
{"project":
{"project_id": 9999, "name": "project1",
"meta": "m1"}
}
'''
return_value8888 = self.FakeResponse()
return_value8888.status_code = requests.codes.ok
return_value8888._text = '''
{"project":
{"project_id": 8888, "name": "project2",
"meta": "m2"}
}
'''
return_value7777 = self.FakeResponse()
return_value7777.status_code = requests.codes.ok
return_value7777._text = '''
{"project":
{"project_id": 7777, "name": "project3",
"meta": "m3"}
}
'''
return_valueP1 = self.FakeResponse()
return_valueP1.status_code = requests.codes.ok
return_valueP1._text = '''
{"project":
{"project_id": 7777, "name": "p1",
"meta": null}
}
'''
return_valueP2 = self.FakeResponse()
return_valueP2.status_code = requests.codes.ok
return_valueP2._text = '''
{"project":
{"project_id": 7777, "name": "p2",
"meta": null}
}
'''
return_valueP3 = self.FakeResponse()
return_valueP3.status_code = requests.codes.ok
return_valueP3._text = '''
{"project":
{"project_id": 7777, "name": "p3",
"meta": null}
}
'''
return_valueM1 = self.FakeResponse()
return_valueM1.status_code = requests.codes.ok
return_valueM1._text = '''
{"project":
{"project_id": "META1", "name": "p3",
"meta": null}
}
'''
return_valueM2 = self.FakeResponse()
return_valueM2.status_code = requests.codes.ok
return_valueM2._text = '''
{"project":
{"project_id": "META2", "name": "p3",
"meta": null}
}
'''
return_valueM3 = self.FakeResponse()
return_valueM3.status_code = requests.codes.ok
return_valueM3._text = '''
{"project":
{"project_id": "META3", "name": "p3",
"meta": null}
}
'''
def get(url, *args, **kwargs):
if '9999' in url:
return return_value9999
if '8888' in url:
return return_value8888
if '7777' in url:
return return_value7777
if 'project1' in url:
return return_valueP1
if 'project2' in url:
return return_valueP2
if 'project3' in url:
return return_valueP3
if 'm1' in url:
return return_valueM1
if 'm2' in url:
return return_valueM2
if 'm3' in url:
return return_valueM3
return return_value
client_mock.session.get = get
manager = mock.Mock
manager._keystone = client_mock
def discover(*args, **kwargs):
return ["https://endpoint.server.name/"]
manager.discover = discover
samples = pollster.get_samples(
manager=manager, cache=None,
resources=["https://endpoint.server.name/"])
samples = list(samples)
self.assertEqual(3, len(samples))
self.assertEqual(samples[0].volume, 9999)
self.assertEqual(samples[1].volume, 8888)
self.assertEqual(samples[2].volume, 7777)
self.assertEqual(samples[0].resource_metadata,
{'project_name': 'project1',
'project_alias': 'p1',
'meta': 'm1',
'project_meta': 'META1'})
self.assertEqual(samples[1].resource_metadata,
{'project_name': 'project2',
'project_alias': 'p2',
'meta': 'm2',
'project_meta': 'META2'})
self.assertEqual(samples[2].resource_metadata,
{'project_name': 'project3',
'project_alias': 'p3',
'meta': 'm3',
'project_meta': 'META3'})
@mock.patch('keystoneclient.v2_0.client.Client')
def test_execute_request_extra_metadata_fields_different_requests(
self, client_mock):
definitions = copy.deepcopy(
self.pollster_definition_only_required_fields)
command = ''' \'\'\'echo '{"project":
{"project_id": \'\'\'+ str(sample['project_id'])
+\'\'\' , "name": "project1"}}' \'\'\' '''.replace('\n', '')
command2 = ''' \'\'\'echo '{"project":
{"project_id": \'\'\'+ str(sample['project_id'])
+\'\'\' , "name": "project2"}}' \'\'\' '''.replace('\n', '')
extra_metadata_fields_embedded = {
'name': "project_name2",
'host_command': command2,
'value': "name",
}
extra_metadata_fields = {
'name': "project_id2",
'host_command': command,
'value': "project_id",
'extra_metadata_fields': extra_metadata_fields_embedded
}
definitions['value_attribute'] = 'project_id'
definitions['extra_metadata_fields'] = extra_metadata_fields
pollster = dynamic_pollster.DynamicPollster(definitions)
return_value = self.FakeResponse()
return_value.status_code = requests.codes.ok
return_value._text = '''
{"projects": [
{"project_id": 9999, "name": "project1"},
{"project_id": 8888, "name": "project2"},
{"project_id": 7777, "name": "project3"}]
}
'''
def get(url, *args, **kwargs):
return return_value
client_mock.session.get = get
manager = mock.Mock
manager._keystone = client_mock
def discover(*args, **kwargs):
return ["https://endpoint.server.name/"]
manager.discover = discover
samples = pollster.get_samples(
manager=manager, cache=None,
resources=["https://endpoint.server.name/"])
samples = list(samples)
self.assertEqual(3, len(samples))
self.assertEqual(samples[0].volume, 9999)
self.assertEqual(samples[1].volume, 8888)
self.assertEqual(samples[2].volume, 7777)
self.assertEqual(samples[0].resource_metadata,
{'project_id2': 9999,
'project_name2': 'project2'})
self.assertEqual(samples[1].resource_metadata,
{'project_id2': 8888,
'project_name2': 'project2'})
self.assertEqual(samples[2].resource_metadata,
{'project_id2': 7777,
'project_name2': 'project2'})
@mock.patch('keystoneclient.v2_0.client.Client')
def test_execute_request_xml_json_response_handler_invalid_response(
self, client_mock):
@ -410,8 +828,8 @@ class TestDynamicPollster(base.BaseTestCase):
keystone_client=client_mock,
resource="https://endpoint.server.name/")
xml_handling_error = logs.output[2]
json_handling_error = logs.output[3]
xml_handling_error = logs.output[3]
json_handling_error = logs.output[4]
self.assertIn(
'DEBUG:ceilometer.polling.dynamic_pollster:'
@ -479,6 +897,57 @@ class TestDynamicPollster(base.BaseTestCase):
resource="https://endpoint.server.name/")
self.assertEqual("Mock HTTP error.", str(exception))
def test_execute_host_command_paged_responses(self):
definitions = copy.deepcopy(
self.pollster_definition_only_required_fields)
definitions['host_command'] = '''
echo '{"server": [{"status": "ACTIVE"}], "next": ""}'
'''
str_json = "'{\\\"server\\\": [{\\\"status\\\": \\\"INACTIVE\\\"}]}'"
definitions['next_sample_url_attribute'] = \
"next|\"echo \"+value+\"" + str_json + '"'
pollster = dynamic_pollster.DynamicPollster(definitions)
samples = pollster.definitions.sample_gatherer. \
execute_request_get_samples()
resp_json = [{'status': 'ACTIVE'}, {'status': 'INACTIVE'}]
self.assertEqual(resp_json, samples)
def test_execute_host_command_response_handler(self):
definitions = copy.deepcopy(
self.pollster_definition_only_required_fields)
definitions['response_handlers'] = ['xml', 'json']
definitions['host_command'] = 'echo "<a><y>xml\n</y><s>xml</s></a>"'
entry = 'a'
definitions['response_entries_key'] = entry
definitions.pop('url_path')
definitions.pop('endpoint_type')
pollster = dynamic_pollster.DynamicPollster(definitions)
samples_xml = pollster.definitions.sample_gatherer. \
execute_request_get_samples()
definitions['host_command'] = 'echo \'{"a": {"y":"json",' \
'\n"s":"json"}}\''
samples_json = pollster.definitions.sample_gatherer. \
execute_request_get_samples()
resp_xml = {'a': {'y': 'xml', 's': 'xml'}}
resp_json = {'a': {'y': 'json', 's': 'json'}}
self.assertEqual(resp_xml[entry], samples_xml)
self.assertEqual(resp_json[entry], samples_json)
def test_execute_host_command_invalid_command(self):
definitions = copy.deepcopy(
self.pollster_definition_only_required_fields)
definitions['host_command'] = 'invalid-command'
definitions.pop('url_path')
definitions.pop('endpoint_type')
pollster = dynamic_pollster.DynamicPollster(definitions)
self.assertRaises(
declarative.InvalidResponseTypeException,
pollster.definitions.sample_gatherer.execute_request_get_samples)
def test_generate_new_metadata_fields_no_metadata_mapping(self):
metadata = {'name': 'someName',
'value': 1}
@ -1105,7 +1574,7 @@ class TestDynamicPollster(base.BaseTestCase):
sample = pollster.definitions.sample_extractor.generate_sample(
pollster_sample, pollster.definitions.configurations,
manager=mock.Mock())
manager=mock.Mock(), conf={})
self.assertEqual(1, sample.volume)
self.assertEqual(2, len(sample.resource_metadata))
@ -1127,7 +1596,7 @@ class TestDynamicPollster(base.BaseTestCase):
sample = pollster.definitions.sample_extractor.generate_sample(
pollster_sample, pollster.definitions.configurations,
manager=mock.Mock())
manager=mock.Mock(), conf={})
self.assertEqual(1, sample.volume)
self.assertEqual(3, len(sample.resource_metadata))
@ -1150,7 +1619,7 @@ class TestDynamicPollster(base.BaseTestCase):
sample = pollster.definitions.sample_extractor.generate_sample(
pollster_sample, pollster.definitions.configurations,
manager=mock.Mock())
manager=mock.Mock(), conf={})
self.assertEqual(1, sample.volume)
self.assertEqual(3, len(sample.resource_metadata))

View File

@ -466,6 +466,62 @@ ones), we can use the `successful_ops`.
resource_id_attribute: "user"
response_entries_key: "summary"
The dynamic pollsters system configuration (for local host commands)
--------------------------------------------------------------------
The dynamic pollster system can also be used for local host commands,
these commands must be installed in the system that is running the
Ceilometer compute agent.
To configure local hosts commands, one can use all but two attributes of
the Dynamic pollster system. The attributes that are not supported are
the ``endpoint_type`` and ``url_path``. The dynamic pollster system for
local host commands is activated automatically when one uses the
configuration ``host_command``.
The extra parameter (in addition to the original ones) that is available
when using the local host commands dynamic pollster sub-subsystem is the
following:
* ``host_command``: required parameter. It is the host command that will
be executed in the same host the Ceilometer dynamic pollster agent is
running. The output of the command will be processed by the pollster and
stored in the configured backend.
As follows we present an example on how to use the local host command:
.. code-block:: yaml
---
- name: "dynamic.host.command"
sample_type: "gauge"
unit: "request"
value_attribute: "value"
response_entries_key: "test"
host_command: "echo '<test><user_id>id1_u</user_id><project_id>id1_p</project_id><id>id1</id><meta>meta-data-to-store</meta><value>1</value></test>'"
metadata_fields:
- "meta"
response_handlers:
- xml
To execute multi page host commands, the `next_sample_url_attribute`
must generate the next sample command, like the following example:
.. code-block:: yaml
---
- name: "dynamic.s3.objects.size"
sample_type: "gauge"
unit: "request"
value_attribute: "Size"
project_id_attribute: "Owner.ID"
user_id_attribute: "Owner.ID"
resource_id_attribute: "Key"
response_entries_key: "Contents"
host_command: "aws s3api list-objects"
next_sample_url_attribute: NextToken | 'aws s3api list-objects --starting-token "' + value + '"'
Operations on extracted attributes
----------------------------------
@ -871,12 +927,10 @@ we only have the `tenant_id`, which must be used as the `project_id`. However,
for billing and later invoicing one might need/want the project name, domain
id, and other metadata that are available in Keystone (and maybe some others
that are scattered over other components). To achieve that, one can use the
OpenStack metadata enrichment option. This feature is only available
to *OpenStack pollsters*, and can only gather extra metadata from OpenStack
APIs. As follows we present an example that shows a dynamic pollster
configuration to gather virtual machine (VM) status, and to enrich the data
pushed to the storage backend (e.g. Gnocchi) with project name, domain ID,
and domain name.
OpenStack metadata enrichment option. As follows we present an example that
shows a dynamic pollster configuration to gather virtual machine (VM) status,
and to enrich the data pushed to the storage backend (e.g. Gnocchi) with
project name, domain ID, and domain name.
.. code-block:: yaml
@ -932,26 +986,59 @@ and domain name.
"Openstack-API-Version": "identity latest"
value: "name"
extra_metadata_fields_cache_seconds: 1800 # overriding the default cache policy
metadata_fields:
- id
- name: "domain_id"
endpoint_type: "identity"
url_path: "'/v3/projects/' + str(sample['project_id'])"
headers:
"Openstack-API-Version": "identity latest"
value: "domain_id"
metadata_fields:
- id
- name: "domain_name"
endpoint_type: "identity"
url_path: "'/v3/domains/' + str(extra_metadata_captured['domain_id'])"
headers:
"Openstack-API-Version": "identity latest"
value: "name"
metadata_fields:
- id
- name: "operating-system"
host_command: "'get-vm --vm-name ' + str(extra_metadata_by_name['project_name']['metadata']['id'])"
value: "os"
The above example can be used to gather and persist in the backend the
status of VMs. It will persist `1` in the backend as a measure for every
collecting period if the VM's status is `ACTIVE`, and `0` otherwise. This is
quite useful to create hashmap rating rules for running VMs in CloudKitty.
Then, to enrich the resource in the storage backend, we are adding extra
metadata that are collected in Keystone via the `extra_metadata_fields`
options.
metadata that are collected in Keystone and in the local host via the
`extra_metadata_fields` options. If you have multiples `extra_metadata_fields`
defining the same `metadata_field`, the last not `None` metadata value will
be used.
To operate values in the `extra_metadata_fields`, you can access 3 local
variables:
* ``sample``: it is a dictionary which holds the current data of the root
sample. The root sample is the final sample that will be persisted in the
configured storage backend.
* ``extra_metadata_captured``: it is a dictionary which holds the current
data of all `extra_metadata_fields` processed before this one.
If you have multiples `extra_metadata_fields` defining the same
`metadata_field`, the last not `None` metadata value will be used.
* ``extra_metadata_by_name``: it is a dictionary which holds the data of
all `extra_metadata_fields` processed before this one. No data is
overwritten in this variable. To access an specific `extra_metadata_field`
using this variable, you can do
`extra_metadata_by_name['<extra_metadata_field_name>']['value']` to get
its value, or
`extra_metadata_by_name['<extra_metadata_field_name>']['metadata']['<metadata>']`
to get its metadata.
The metadata enrichment feature has the following options:
@ -964,41 +1051,13 @@ The metadata enrichment feature has the following options:
value can be increased of decreased.
* ``extra_metadata_fields``: optional parameter. This option is a list of
objects, where each one of its elements is an extra metadata definition.
Each one of the extra metadata definition can have the options defined in
the dynamic pollsters such as to handle paged responses, operations on the
extracted values, headers and so on. The basic options that must be
defined for an extra metadata definitions are the following:
objects or a single one, where each one of its elements is an
dynamic pollster configuration set. Each one of the extra metadata
definition can have the same options defined in the dynamic pollsters,
including the `extra_metadata_fields` option, so this option is a
multi-level option. When defined, the result of the collected data will
be merged in the final sample resource metadata. If some of the required
dynamic pollster configuration is not set in the `extra_metadata_fields`,
will be used the parent pollster configuration, except the `name`.
* ``name``: This option is mandatory. The name of the extra metadata.
This is the name that is going to be used by the metadata. If there is
already any other metadata gathered via `metadata_fields` option or
transformed via `metadata_mapping` configuration, this metadata is
going to be discarded.
* ``endpoint_type``: The endpoint type that we want to execute the
call against. This option is mandatory. It works similarly to the
`endpoint_type` option in the dynamic pollster definition.
* ``url_path``: This option is mandatory. It works similarly to the
`url_path` option in the dynamic pollster definition. However, this
`one enables operators to execute/evaluate expressions in runtime, which
`allows one to retrieve the information from previously gathered
metadata via ``extra_metadata_captured` dictionary, or via the
`sample` itself.
* ``value``: This configuration is mandatory. It works similarly to the
`value_attribute` option in the dynamic pollster definition. It is
the value we want to extract from the response, and assign in the
metadata being generated.
* ``headers``: This option is optional. It works similarly to the
`headers` option in the dynamic pollster definition.
* ``next_sample_url_attribute``: This option is optional. It works
similarly to the `next_sample_url_attribute` option in the dynamic
pollster definition.
* ``response_entries_key``: This option is optional. It works
similarly to the `response_entries_key` option in the dynamic
pollster definition.

View File

@ -45,6 +45,7 @@ ceilometer.sample.endpoint =
ceilometer.discover.compute =
local_instances = ceilometer.compute.discovery:InstanceDiscovery
local_node = ceilometer.polling.discovery.localnode:LocalNodeDiscovery
ceilometer.discover.central =
barbican = ceilometer.polling.discovery.non_openstack_credentials_discovery:NonOpenStackCredentialsDiscovery