congress/congress/cfg_validator/agent/agent.py

435 lines
14 KiB
Python

#
# Copyright (c) 2017 Orange.
#
# 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.
#
"""Agent is the main entry point for the configuration validator agent.
The agent is executed on the different nodes of the cloud and sends back
configuration values and metadata to the configuration validator datasource
driver.
"""
import json
import os
import sys
from oslo_config import cfg
from oslo_config import generator
from oslo_log import log as logging
from oslo_service import service
import six
from congress.common import config
from congress.cfg_validator.agent import generator as validator_generator
from congress.cfg_validator.agent import opts as validator_opts
from congress.cfg_validator.agent import rpc
from congress.cfg_validator import parsing
from congress.cfg_validator import utils
LOG = logging.getLogger(__name__)
class Config(object):
"""Encapsulates a configuration file and its meta-data.
Attributes:
:ivar path: Path to the configuration on the local file system.
:ivar template: A Template object to use for parsing the configuration.
:ivar data: The normalized Namespace loaded by oslo-config, contains
the parsed values.
:ivar hash: Hash of the configuration file, salted with the hostname
and the template hash
:ivar service_name: The associated service name
"""
# pylint: disable=protected-access
def __init__(self, path, template, service_name):
self.path = path
self.template = template
self.data = None
self.hash = None
self.service = service_name
def parse(self, host):
"""Parses the config at the path given. Updates data and hash.
host: the name of the host where the config is. Used for building a
unique hash.
"""
namespaces_data = [ns.data for ns in self.template.namespaces]
conf = parsing.parse_config_file(namespaces_data, self.path)
Config.sanitize_config(conf)
self.data = conf._namespace._normalized
self.hash = utils.compute_hash(host, self.template.hash,
json.dumps(self.data, sort_keys=True))
@staticmethod
def sanitize_config(conf):
"""Sanitizes some cfg.ConfigOpts values, given its options meta-data.
:param conf: A cfg.ConfigOpts object, pre-loaded with its options
meta-data and with its configurations values.
"""
normalized = getattr(conf._namespace,
'_normalized', None)
if not normalized:
return
normalized = normalized[0]
# Obfuscate values of options declared secret
def _sanitize(opt, group_name='DEFAULT'):
if not opt.secret:
return
if group_name not in normalized:
return
if opt.name in normalized[group_name]:
normalized[group_name][opt.name] = ['*' * 4]
for option in six.itervalues(conf._opts):
_sanitize(option['opt'])
for group_name, group in six.iteritems(conf._groups):
for option in six.itervalues(group._opts):
_sanitize(option['opt'], group_name)
def get_info(self):
"""Information on the configuration file.
:return: a quadruple made of:
* the hash of the template,
* the path to the file,
* the content
* the service name.
"""
return {'template': self.template.hash, 'path': self.path,
'data': self.data, 'service': self.service}
class Namespace(object):
"""Encapsulates a namespace, as defined by oslo-config-generator.
It contains the actual meta-data of the options. The data is loaded from
the service source code, by means of oslo-config-generator.
Attributes:
name: The name, as used by oslo-config-generator.
data: The meta-data of the configuration options.
hash: Hash of the namespace.
"""
def __init__(self, name):
self.name = name
self.data = None
self.hash = None
@staticmethod
def load(name):
"""Loads a namespace from disk
:param name: the name of namespace to load.
:return: a fully configured namespace.
"""
namespace = Namespace(name)
saved_conf = cfg.CONF
cfg.CONF = cfg.ConfigOpts()
try:
json_data = validator_generator.generate_ns_data(name)
finally:
cfg.CONF = saved_conf
namespace.hash = utils.compute_hash(json_data)
namespace.data = json.loads(json_data)
return namespace
def get_info(self):
"""Information on the namespace
:return: a tuple containing
* data: the content of the namespace
* name: the name of the namespace
"""
return {'data': self.data, 'name': self.name}
class Template(object):
"""Describes a template, as defined by oslo-config-generator.
Attributes:
:ivar name: The name, as used by oslo-config-generator.
:ivar path: The path to the template configuration file, as defined by
oslo-config-generator, on the local file system.
:ivar output_file: The default output path for this template.
:ivar namespaces: A set of Namespace objects, which make up this
template.
"""
# pylint: disable=protected-access
def __init__(self, path, output_file):
self.path = path
self.output_file = output_file
self.namespaces = []
self.hash = None
name = os.path.basename(output_file)
self.name = os.path.splitext(name)[0] if name.endswith('.sample') \
else name
@staticmethod
def _parse_template_conf(template_path):
"""Parses a template configuration file"""
conf = cfg.ConfigOpts()
conf.register_opts(generator._generator_opts)
conf(['--config-file', template_path])
return conf.namespace, conf.output_file
@staticmethod
def load(template_path):
"""Loads a template configuration file
:param template_path: path to the template
:return: a fully configured Template object.
"""
namespaces, output_file = Template._parse_template_conf(template_path)
template = Template(template_path, output_file)
for namespace in namespaces:
template.namespaces.append(Namespace.load(namespace))
template.hash = utils.compute_hash(
sorted([ns.hash for ns in template.namespaces]))
return template
def get_info(self):
"""Info on the template
:return: a quadruple made of:
* path: the path to the template path
* name: the name of the template
* output_fle:
* namespaces: an array of namespace hashes.
"""
return {'path': self.path, 'name': self.name,
'output_file': self.output_file,
'namespaces': [ns.hash for ns in self.namespaces]}
class ConfigManager(object):
"""Manages the services configuration files on a node and their meta-data.
Attributes:
:ivar host: A hostname.
:ivar configs: A dict mapping config hashes to their associated Config
object.
:ivar templates: A dict mapping template hashes to their associated
Template object.
:ivar namespaces: A dict mapping namespace hashes to their associated
Namespace object.
"""
def __init__(self, host, services_files):
self.host = host
self.configs = {}
self.templates = {}
self.namespaces = {}
for service_name, files in six.iteritems(services_files):
self.register_service(service_name, files)
def get_template_by_path(self, template_path):
"""Given a path finds the corresponding template if it is registered
:param template_path: the path of the searched template
:return: None or the template
"""
for template in six.itervalues(self.templates):
if template.path == template_path:
return template
def add_template(self, template_path):
"""Adds a new template (loads it from path).
:param template_path: a valid path to the template file.
"""
template = Template.load(template_path)
self.templates[template.hash] = template
self.namespaces.update({ns.hash: ns for ns in template.namespaces})
return template
def register_config(self, config_path, template_path, service_name):
"""Register a configuration file and its associated template.
Template and config are actually parsed and loaded.
:param config_path: a valid path to the config file.
:param template_path: a valid path to the template file.
"""
template = (self.get_template_by_path(template_path)
or self.add_template(template_path))
conf = Config(config_path, template, service_name)
conf.parse(self.host)
self.configs[conf.hash] = conf
LOG.info('{hash: %s, path:%s}' % (conf.hash, conf.path))
def register_service(self, service_name, files):
"""Register all configs for an identified service.
Inaccessible files are ignored and files registration pursues.
:param service_name: The name of the service
:param files: A dict, mapping a configuration path to
its associated template path
"""
for config_path, template_path in six.iteritems(files):
try:
self.register_config(config_path, template_path, service_name)
except (IOError, cfg.ConfigFilesNotFoundError):
LOG.error(('Error while registering config %s with template'
' %s for service %s') %
(config_path, template_path, service_name))
class ValidatorAgentEndpoint(object):
"""Validator Agent.
It is used as an RPC endpoint.
Attributes:
config_manager: ConfigManager object.
driver_api: RPC client to communicate with the driver.
"""
# pylint: disable=unused-argument,too-many-instance-attributes
def __init__(self, conf=None):
self.conf = conf or cfg.CONF
validator_conf = self.conf.agent
self.host = validator_conf.host
self.version = validator_conf.version
self.max_delay = validator_conf.max_delay
self.driver_api = rpc.ValidatorDriverClient()
self.services = list(validator_conf.services.keys())
service_files = validator_conf.services
self.config_manager = ConfigManager(self.host, service_files)
def publish_configs_hashes(self, context, **kwargs):
""""Sends back all configuration hashes"""
LOG.info('Sending config hashes')
conf = set(self.config_manager.configs)
self.driver_api.process_configs_hashes({}, conf, self.host)
def publish_templates_hashes(self, context, **kwargs):
""""Sends back all template hashes"""
LOG.info('Sending template hashes')
tpl = set(self.config_manager.templates)
self.driver_api.process_templates_hashes({}, tpl, self.host)
def get_namespace(self, context, **kwargs):
""""Sends back a namespace
:param context: the RPC context
:param hash: the hash of the namespace to send
:return: the namespace or None if not found
"""
ns_hash = kwargs['ns_hash']
LOG.info('Sending namespace %s' % ns_hash)
namespace = self.config_manager.namespaces.get(ns_hash, None)
if namespace is None:
return None
ret = namespace.get_info()
ret['version'] = self.version
return ret
def get_template(self, context, **kwargs):
""""Sends back a template
:param context: the RPC context
:param hash: the hash of the template to send
:return: the template or None if not found
"""
template_hash = kwargs['tpl_hash']
LOG.info('Sending template %s' % template_hash)
template = self.config_manager.templates.get(template_hash, None)
if template is None:
return None
ret = template.get_info()
ret['version'] = self.version
return ret
def get_config(self, context, **kwargs):
""""Sends back a config
:param context: the RPC context
:param hash: the hash of the config to send
:return: the config or None if not found
"""
config_hash = kwargs['cfg_hash']
LOG.info('Sending config %s' % config_hash)
conf = self.config_manager.configs.get(config_hash, None)
if conf is None:
return None
ret = conf.get_info()
ret['version'] = self.version
return ret
def main():
"""Agent entry point"""
validator_opts.register_validator_agent_opts(cfg.CONF)
config.init(sys.argv[1:])
config.setup_logging()
if not cfg.CONF.config_file:
sys.exit("ERROR: Unable to find configuration file via default "
"search paths ~/.congress/, ~/, /etc/congress/, /etc/) and "
"the '--config-file' option!")
agent = ValidatorAgentEndpoint()
server = rpc.AgentService(utils.AGENT_TOPIC, [agent])
service.launch(agent.conf, server).wait()
if __name__ == '__main__':
main()