monasca-agent/monasca_setup/main.py

285 lines
15 KiB
Python

#!/usr/bin/env python
# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP
""" Detect running daemons then configure and start the agent.
"""
import argparse
from glob import glob
import logging
import os
import pwd
import socket
import subprocess
import sys
import agent_config
import monasca_setup.utils as utils
from monasca_setup.utils import write_template
from service.detection import detect_init
log = logging.getLogger(__name__)
CUSTOM_PLUGIN_PATH = '/usr/lib/monasca/agent/custom_detect.d'
# dirname is called twice to get the dir 1 above the location of the script
PREFIX_DIR = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))
def main(argv=None):
parser = argparse.ArgumentParser(description='Configure and setup the agent. In a full run it will detect running' +
' daemons then configure and start the agent.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
args = parse_arguments(parser)
if args.verbose:
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
else:
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
if args.dry_run:
log.info("Running in dry run mode, no changes will be made only reported")
# Detect and if possibly enable the agent service
agent_service = detect_init(PREFIX_DIR, args.config_dir, args.log_dir, args.template_dir, username=args.user)
# Skip base setup if only installing plugins or running specific detection
# plugins
if not args.install_plugins_only and args.detection_plugins is None:
if not args.skip_enable:
agent_service.enable()
# Verify required options
if args.username is None or args.password is None or args.keystone_url is None:
log.error('Username, password and keystone_url are required when running full configuration.')
parser.print_help()
sys.exit(1)
base_configuration(args)
# Collect the set of detection plugins to run
detected_plugins = utils.discover_plugins(CUSTOM_PLUGIN_PATH)
if args.system_only:
from detection.plugins.system import System
plugins = [System]
elif args.detection_plugins is not None:
plugins = utils.select_plugins(args.detection_plugins, detected_plugins)
else:
plugins = detected_plugins
plugin_names = [p.__name__ for p in plugins]
if args.remove: # Remove entries for each plugin from the various plugin config files
changes = remove_config(args, plugin_names)
else:
# Run detection for all the plugins, halting on any failures if plugins were specified in the arguments
detected_config = plugin_detection(plugins, args.template_dir, args.detection_args,
skip_failed=(args.detection_plugins is None))
if detected_config is None:
return 1 # Indicates detection problem, skip remaining steps and give non-zero exit code
changes = modify_config(args, detected_config)
# Don't restart if only doing detection plugins and no changes found
if args.detection_plugins is not None and not changes:
log.info('No changes found for plugins {0}, skipping restart of Monasca Agent'.format(plugin_names))
return 0
elif args.dry_run:
log.info('Running in dry mode, skipping changes and restart of Monasca Agent')
return 0
# Now that the config is built, start the service
if args.install_plugins_only:
log.info('Command line option install_plugins_only set, skipping '
'service (re)start.')
else:
try:
agent_service.start(restart=True)
except subprocess.CalledProcessError:
log.error('The service did not startup correctly see %s' % args.log_dir)
def base_configuration(args):
"""Write out the primary Agent configuration and setup the service.
:param args: Arguments from the command line
:return: None
"""
gid = pwd.getpwnam(args.user).pw_gid
# Write the main agent.yaml - Note this is always overwritten
log.info('Configuring base Agent settings.')
dimensions = {}
# Join service in with the dimensions
if args.service:
dimensions.update({'service': args.service})
if args.dimensions:
dimensions.update(dict(item.strip().split(":") for item in args.dimensions.split(",")))
args.dimensions = dict((name, value) for (name, value) in dimensions.iteritems())
write_template(os.path.join(args.template_dir, 'agent.yaml.template'),
os.path.join(args.config_dir, 'agent.yaml'),
{'args': args, 'hostname': socket.getfqdn()},
gid,
is_yaml=True)
# Write the supervisor.conf
write_template(os.path.join(args.template_dir, 'supervisor.conf.template'),
os.path.join(args.config_dir, 'supervisor.conf'),
{'prefix': PREFIX_DIR, 'log_dir': args.log_dir, 'monasca_user': args.user},
gid)
def modify_config(args, detected_config):
changes = False
# Compare existing and detected config for each check plugin and write out the plugin config if changes
for key, value in detected_config.iteritems():
if args.overwrite:
changes = True
if args.dry_run:
continue
else:
agent_config.save_plugin_config(args.config_dir, key, args.user, value)
else:
old_config = agent_config.read_plugin_config_from_disk(args.config_dir, key)
# merge old and new config, new has precedence
if old_config is not None:
if key is "http_check":
old_config_urls = [i['url'] for i in old_config['instances'] if 'url' in i]
value, old_config = agent_config.check_endpoint_changes(value, old_config)
agent_config.merge_by_name(value['instances'], old_config['instances'])
# Sort before compare, if instances have no name the sort will fail making order changes significant
try:
value['instances'].sort(key=lambda k: k['name'])
old_config['instances'].sort(key=lambda k: k['name'])
except Exception:
pass
value_urls = [i['url'] for i in value['instances'] if 'url' in i]
if key is "http_check":
if value_urls is old_config_urls: # Don't write config if no change
continue
else:
if value is old_config:
continue
changes = True
if args.dry_run:
log.info("Changes would be made to the config file for the {0} check plugin".format(key))
else:
agent_config.save_plugin_config(args.config_dir, key, args.user, value)
return changes
def parse_arguments(parser):
parser.add_argument(
'-u', '--username', help="Username used for keystone authentication. Required for basic configuration.")
parser.add_argument(
'-p', '--password', help="Password used for keystone authentication. Required for basic configuration.")
parser.add_argument('--user_domain_id', help="User domain id for keystone authentication", default='')
parser.add_argument('--user_domain_name', help="User domain name for keystone authentication", default='')
parser.add_argument('--keystone_url', help="Keystone url. Required for basic configuration.")
parser.add_argument('--project_name', help="Project name for keystone authentication", default='')
parser.add_argument('--project_domain_id', help="Project domain id for keystone authentication", default='')
parser.add_argument('--project_domain_name', help="Project domain name for keystone authentication", default='')
parser.add_argument('--project_id', help="Keystone project id for keystone authentication", default='')
parser.add_argument('--monasca_url', help="Monasca API url, if not defined the url is pulled from keystone",
default='')
parser.add_argument('--system_only', help="Setup the service but only configure the base config and system " +
"metrics (cpu, disk, load, memory, network).",
action="store_true", default=False)
parser.add_argument('-d', '--detection_plugins', nargs='*',
help="Skip base config and service setup and only configure this space separated list. " +
"This assumes the base config has already run.")
parser.add_argument('-a', '--detection_args', help="A string of arguments that will be passed to detection " +
"plugins. Only certain detection plugins use arguments.")
parser.add_argument('--check_frequency', help="How often to run metric collection in seconds", type=int, default=30)
parser.add_argument('--dimensions', help="Additional dimensions to set for all metrics. A comma separated list " +
"of name/value pairs, 'name:value,name2:value2'")
parser.add_argument('--ca_file', help="Sets the path to the ca certs file if using certificates. " +
"Required only if insecure is set to False", default='')
parser.add_argument('--insecure', help="Set whether certificates are used for Keystone authentication",
default=False)
parser.add_argument('--config_dir', help="Configuration directory", default='/etc/monasca/agent')
parser.add_argument('--log_dir', help="monasca-agent log directory", default='/var/log/monasca/agent')
parser.add_argument('--log_level', help="monasca-agent logging level (ERROR, WARNING, INFO, DEBUG)", required=False,
default='WARN')
parser.add_argument('--template_dir', help="Alternative template directory",
default=os.path.join(PREFIX_DIR, 'share/monasca/agent'))
parser.add_argument('--overwrite',
help="Overwrite existing plugin configuration. " +
"The default is to merge. agent.yaml is always overwritten.",
action="store_true")
parser.add_argument('-r', '--remove', help="Rather than add the detected configuration remove it.",
action="store_true", default=False)
parser.add_argument('--skip_enable', help="By default the service is enabled, " +
"which requires the script run as root. Set this to skip that step.",
action="store_true")
parser.add_argument('--install_plugins_only', help="Only update plugin "
"configuration, do not configure services, users, etc."
" or restart services",
action="store_true")
parser.add_argument('--user', help="User name to run monasca-agent as", default='mon-agent')
parser.add_argument('-s', '--service', help="Service this node is associated with, added as a dimension.")
parser.add_argument('--amplifier', help="Integer for the number of additional measurements to create. " +
"Additional measurements contain the 'amplifier' dimension. " +
"Useful for load testing; not for production use.", default=0)
parser.add_argument('-v', '--verbose', help="Verbose Output", action="store_true")
parser.add_argument('--dry_run', help="Make no changes just report on changes", action="store_true")
return parser.parse_args()
def plugin_detection(plugins, template_dir, detection_args, skip_failed=True):
"""Runs the detection step for each plugin in the list and returns the complete detected agent config.
:param plugins: A list of detection plugin classes
:param template_dir: Location of plugin configuration templates
:param detection_args: Arguments passed to each detection plugin
:param skip_failed: When False any detection failure causes the run to halt and return None
:return: An agent_config instance representing the total configuration from all detection plugins run.
"""
plugin_config = agent_config.Plugins()
for detect_class in plugins:
# todo add option to install dependencies
detect = detect_class(template_dir, False, detection_args)
if detect.available:
log.info('Configuring {0}'.format(detect.name))
new_config = detect.build_config_with_name()
if new_config is not None:
plugin_config.merge(new_config)
elif not skip_failed:
log.warn('Failed detection of plugin {0}.'.format(detect.name) +
"\n\tPossible causes: Service not found or missing arguments.")
return None
return plugin_config
def remove_config(args, plugin_names):
"""Parse all configuration removing any configuration built by plugins in plugin_names
Note there is no concept of overwrite for removal.
:param args: specified arguments
:param plugin_names: A list of the plugin names to remove from the config
:return: True if changes, false otherwise
"""
changes = False
existing_config_files = glob(os.path.join(args.config_dir, 'conf.d', '*.yaml'))
for file_path in existing_config_files:
deletes = False
plugin_name = os.path.splitext(os.path.basename(file_path))[0]
config = agent_config.read_plugin_config_from_disk(args.config_dir, plugin_name)
new_instances = [] # To avoid odd issues from iterating over a list you delete from build a new instead
for i in range(len(config['instances'])):
inst = config['instances'][i]
if 'built_by' in inst and inst['built_by'] in plugin_names:
changes = True
deletes = True
continue
new_instances.append(inst)
config['instances'] = new_instances
if deletes:
if args.dry_run:
log.info("Changes would be made to the config file {0}".format(file_path))
else:
if len(config['instances']) == 0:
log.info("Removing configuration file {0} it is no longer needed.".format(file_path))
os.remove(file_path)
else:
log.info("Saving changes to configuration file {0}.".format(file_path))
agent_config.save_plugin_config(args.config_dir, plugin_name, args.user, config)
return changes
if __name__ == "__main__":
sys.exit(main())