232 lines
9.1 KiB
Python
232 lines
9.1 KiB
Python
#
|
|
# 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.
|
|
|
|
"""Dynamic pollster component
|
|
This component enables operators to create new pollsters on the fly
|
|
via configuration. The configuration files are read from
|
|
'/etc/ceilometer/pollsters.d/'. The pollster are defined in YAML files
|
|
similar to the idea used for handling notifications.
|
|
"""
|
|
|
|
from oslo_log import log
|
|
from oslo_utils import timeutils
|
|
from requests import RequestException
|
|
|
|
from ceilometer import declarative
|
|
from ceilometer.polling import plugin_base
|
|
from ceilometer import sample
|
|
|
|
|
|
import requests
|
|
from six.moves.urllib import parse as url_parse
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class DynamicPollster(plugin_base.PollsterBase):
|
|
|
|
OPTIONAL_POLLSTER_FIELDS = ['metadata_fields', 'skip_sample_values',
|
|
'value_mapping', 'default_value',
|
|
'metadata_mapping',
|
|
'preserve_mapped_metadata']
|
|
|
|
REQUIRED_POLLSTER_FIELDS = ['name', 'sample_type', 'unit',
|
|
'value_attribute', 'endpoint_type',
|
|
'url_path']
|
|
|
|
ALL_POLLSTER_FIELDS = OPTIONAL_POLLSTER_FIELDS + REQUIRED_POLLSTER_FIELDS
|
|
|
|
name = ""
|
|
|
|
def __init__(self, pollster_definitions, conf=None):
|
|
super(DynamicPollster, self).__init__(conf)
|
|
LOG.debug("Dynamic pollster created with [%s]",
|
|
pollster_definitions)
|
|
|
|
self.pollster_definitions = pollster_definitions
|
|
self.validate_pollster_definition()
|
|
|
|
if 'metadata_fields' in self.pollster_definitions:
|
|
LOG.debug("Metadata fields configured to [%s].",
|
|
self.pollster_definitions['metadata_fields'])
|
|
|
|
self.name = self.pollster_definitions['name']
|
|
self.obj = self
|
|
|
|
if 'skip_sample_values' not in self.pollster_definitions:
|
|
self.pollster_definitions['skip_sample_values'] = []
|
|
|
|
if 'value_mapping' not in self.pollster_definitions:
|
|
self.pollster_definitions['value_mapping'] = {}
|
|
|
|
if 'default_value' not in self.pollster_definitions:
|
|
self.pollster_definitions['default_value'] = -1
|
|
|
|
if 'preserve_mapped_metadata' not in self.pollster_definitions:
|
|
self.pollster_definitions['preserve_mapped_metadata'] = True
|
|
|
|
if 'metadata_mapping' not in self.pollster_definitions:
|
|
self.pollster_definitions['metadata_mapping'] = {}
|
|
|
|
def validate_pollster_definition(self):
|
|
missing_required_fields = \
|
|
[field for field in self.REQUIRED_POLLSTER_FIELDS
|
|
if field not in self.pollster_definitions]
|
|
|
|
if missing_required_fields:
|
|
raise declarative.DynamicPollsterDefinitionException(
|
|
"Required fields %s not specified."
|
|
% missing_required_fields, self.pollster_definitions)
|
|
|
|
sample_type = self.pollster_definitions['sample_type']
|
|
if sample_type not in sample.TYPES:
|
|
raise declarative.DynamicPollsterDefinitionException(
|
|
"Invalid sample type [%s]. Valid ones are [%s]."
|
|
% (sample_type, sample.TYPES), self.pollster_definitions)
|
|
|
|
for definition_key in self.pollster_definitions:
|
|
if definition_key not in self.ALL_POLLSTER_FIELDS:
|
|
LOG.warning(
|
|
"Field [%s] defined in [%s] is unknown "
|
|
"and will be ignored. Valid fields are [%s].",
|
|
definition_key, self.pollster_definitions,
|
|
self.ALL_POLLSTER_FIELDS)
|
|
|
|
def get_samples(self, manager, cache, resources):
|
|
if not resources:
|
|
LOG.debug("No resources received for processing.")
|
|
yield None
|
|
|
|
for endpoint in resources:
|
|
LOG.debug("Executing get sample on URL [%s].", endpoint)
|
|
|
|
samples = list([])
|
|
try:
|
|
samples = self.execute_request_get_samples(
|
|
keystone_client=manager._keystone, endpoint=endpoint)
|
|
except RequestException as e:
|
|
LOG.warning("Error [%s] while loading samples for [%s] "
|
|
"for dynamic pollster [%s].",
|
|
e, endpoint, self.name)
|
|
|
|
for pollster_sample in samples:
|
|
response_value_attribute_name = self.pollster_definitions[
|
|
'value_attribute']
|
|
value = pollster_sample[response_value_attribute_name]
|
|
|
|
skip_sample_values = \
|
|
self.pollster_definitions['skip_sample_values']
|
|
if skip_sample_values and value in skip_sample_values:
|
|
LOG.debug("Skipping sample [%s] because value [%s] "
|
|
"is configured to be skipped in skip list [%s].",
|
|
pollster_sample, value, skip_sample_values)
|
|
continue
|
|
|
|
value = self.execute_value_mapping(value)
|
|
|
|
user_id = None
|
|
if 'user_id' in pollster_sample:
|
|
user_id = pollster_sample["user_id"]
|
|
|
|
project_id = None
|
|
if 'project_id' in pollster_sample:
|
|
project_id = pollster_sample["project_id"]
|
|
|
|
metadata = []
|
|
if 'metadata_fields' in self.pollster_definitions:
|
|
metadata = dict((k, pollster_sample.get(k))
|
|
for k in self.pollster_definitions[
|
|
'metadata_fields'])
|
|
self.generate_new_metadata_fields(metadata=metadata)
|
|
yield sample.Sample(
|
|
timestamp=timeutils.isotime(),
|
|
|
|
name=self.pollster_definitions['name'],
|
|
type=self.pollster_definitions['sample_type'],
|
|
unit=self.pollster_definitions['unit'],
|
|
volume=value,
|
|
|
|
user_id=user_id,
|
|
project_id=project_id,
|
|
resource_id=pollster_sample["id"],
|
|
|
|
resource_metadata=metadata
|
|
)
|
|
|
|
def execute_value_mapping(self, value):
|
|
value_mapping = self.pollster_definitions['value_mapping']
|
|
if value_mapping:
|
|
if value in value_mapping:
|
|
old_value = value
|
|
value = value_mapping[value]
|
|
LOG.debug("Value mapped from [%s] to [%s]",
|
|
old_value, value)
|
|
else:
|
|
default_value = \
|
|
self.pollster_definitions['default_value']
|
|
LOG.warning(
|
|
"Value [%s] was not found in value_mapping [%s]; "
|
|
"therefore, we will use the default [%s].",
|
|
value, value_mapping, default_value)
|
|
value = default_value
|
|
return value
|
|
|
|
def generate_new_metadata_fields(self, metadata=None):
|
|
metadata_mapping = self.pollster_definitions['metadata_mapping']
|
|
if not metadata_mapping or not metadata:
|
|
return
|
|
|
|
metadata_keys = list(metadata.keys())
|
|
for k in metadata_keys:
|
|
if k not in metadata_mapping:
|
|
continue
|
|
|
|
new_key = metadata_mapping[k]
|
|
metadata[new_key] = metadata[k]
|
|
LOG.debug("Generating new key [%s] with content [%s] of key [%s]",
|
|
new_key, metadata[k], k)
|
|
if self.pollster_definitions['preserve_mapped_metadata']:
|
|
continue
|
|
|
|
k_value = metadata.pop(k)
|
|
LOG.debug("Removed key [%s] with value [%s] from "
|
|
"metadata set that is sent to Gnocchi.", k, k_value)
|
|
|
|
@property
|
|
def default_discovery(self):
|
|
return 'endpoint:' + self.pollster_definitions['endpoint_type']
|
|
|
|
def execute_request_get_samples(self, keystone_client, endpoint):
|
|
url = url_parse.urljoin(
|
|
endpoint, self.pollster_definitions['url_path'])
|
|
resp = keystone_client.session.get(url, authenticated=True)
|
|
if resp.status_code != requests.codes.ok:
|
|
resp.raise_for_status()
|
|
|
|
response_json = resp.json()
|
|
|
|
entry_size = len(response_json)
|
|
LOG.debug("Entries [%s] in the JSON for request [%s] "
|
|
"for dynamic pollster [%s].",
|
|
response_json, url, self.name)
|
|
|
|
if entry_size > 0:
|
|
first_entry_name = None
|
|
try:
|
|
first_entry_name = next(iter(response_json))
|
|
except RuntimeError as e:
|
|
LOG.debug("Generator threw a StopIteration "
|
|
"and we need to catch it [%s].", e)
|
|
return response_json[first_entry_name]
|
|
return []
|