Structural changes to allow a single detection plugin to setup multiple plugins

Added a Plugins object to allow merging of various configs before writing the file.
This commit is contained in:
Tim Kuhlman 2014-06-09 10:34:16 -06:00
parent 40d1cb7bee
commit 9853526cfb
8 changed files with 162 additions and 95 deletions

View File

@ -1,85 +0,0 @@
#!/usr/bin/env python
""" Detect running daemons then configure and start the agent.
"""
import argparse
import logging
import os
import socket
import subprocess
import sys
from detection import network, nova
from service import sysv
# List of all detection plugins to run
DETECTION_PLUGINS = [network.Network, nova.Nova]
# Map OS to service type
OS_SERVICE_MAP = {'linux': sysv.SysV}
log = logging.getLogger(__name__)
def main(argv=None):
parser = argparse.ArgumentParser(description='Detect running daemons then configure and start the agent.')
parser.add_argument('-u', '--username', help="Keystone username used to post metrics", required=True)
parser.add_argument('-p', '--password', help="Keystone password used to post metrics", required=True)
parser.add_argument('-s', '--service', help="Service this node is associated with.", required=True)
parser.add_argument('--keystone_url', help="Keystone url", required=True)
parser.add_argument('--mon_url', help="Mon API url", required=True)
parser.add_argument('--config_dir', help="Alternative configuration directory", default='/etc/mon-agent')
parser.add_argument('--template_dir', help="Alternative template directory", default='/usr/local/share/mon/agent')
parser.add_argument('--headless', help="Run in a non-interactive mode", action="store_true")
parser.add_argument('--overwrite', help="Overwrite existing configuration", action="store_true")
parser.add_argument('-v', '--verbose', help="Verbose Output", action="store_true")
#todo provide an option to exclude certain detection plugins
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
else:
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
# todo implement a way to include some predefined configuration, useful for active checks
# todo if overwrite is set I should either warn or just delete any config files not in the new config
# todo add the ability to build the config in a temp dir then diff
# Detect os
detected_os = 'linux' # todo add detection
# Service enable, includes setup of config directories so must be done before configuration
# todo is there a better place for basic directories to be made then the service enabling?
agent_service = OS_SERVICE_MAP[detected_os](os.path.join(args.template_dir, 'mon-agent.init'), args.config_dir)
# Todo add logic for situations where either enable or start is not needed or if not running as root isn't possible
agent_service.enable()
# Write the main agent.conf
log.info('Configuring base Agent settings.')
agent_template = open(os.path.join(args.template_dir, 'agent.conf.template'), 'r')
agent_conf = open(os.path.join(args.config_dir, 'agent.conf'), 'w')
agent_conf.write(agent_template.read().format(args=args, hostname=socket.gethostname()))
agent_template.close()
agent_conf.close()
# Link the supervisor.conf
supervisor_path = os.path.join(args.config_dir, 'supervisor.conf')
if os.path.exists(supervisor_path):
os.remove(supervisor_path)
os.symlink(os.path.join(args.template_dir, 'supervisor.conf'), supervisor_path)
# Run through detection and config building for the plugins
for detect_class in DETECTION_PLUGINS:
detect = detect_class(args.config_dir, args.template_dir, args.overwrite)
if detect.available:
log.info('Configuring {0}'.format(detect.name))
detect.build_config()
#todo add option to install dependencies
# Now that the config is build start the service
try:
agent_service.start(restart=True)
except subprocess.CalledProcessError:
log.error('The service did not startup correctly see /var/log/mon-agent')
if __name__ == "__main__":
sys.exit(main())

20
monsetup/agent_config.py Normal file
View File

@ -0,0 +1,20 @@
"""Classes to aid in configuration of the agent."""
import collections
class Plugins(collections.defaultdict):
"""A container for the plugin configurations used by the mon-agent.
This is essentially a defaultdict(dict) but put into a class primarily to make the interface clear, also
to add a couple of helper methods.
Each plugin config is stored with the key being its config name (excluding .yaml).
The value a dict which will convert to yaml.
"""
def __init__(self):
super(Plugins, self).__init__(dict)
# todo Possibly enforce the key being a string without .yaml in it.
def diff(self, other_plugins):
raise NotImplementedError

View File

@ -4,12 +4,12 @@
class Plugin(object):
"""Abstract class implemented by the mon-agent plugin detection classes"""
"""Abstract class implemented by the mon-agent plugin detection classes
"""
# todo these should include dependency detection
def __init__(self, config_dir, template_dir, overwrite=True):
def __init__(self, template_dir, overwrite=True):
self.available = False
self.config_dir = config_dir
self.template_dir = template_dir
self.dependencies = ()
self.overwrite = overwrite
@ -20,6 +20,8 @@ class Plugin(object):
raise NotImplementedError
def build_config(self):
"""Build the config as a Plugins object and return.
"""
raise NotImplementedError
def dependencies_installed(self):

View File

@ -0,0 +1,19 @@
from . import Plugin
from monsetup import agent_config
class MySQL(Plugin):
"""Detect MySQL daemons and setup configuration to monitor them."""
def _detect(self):
"""Run detection, set self.available True if the service is detected."""
self.available = True
def build_config(self):
"""Build the config as a Plugins object and return.
"""
return agent_config.Plugins()
def dependencies_installed(self):
pass

View File

@ -1,7 +1,8 @@
import os
import shutil
import yaml
from . import Plugin
from monsetup import agent_config
class Network(Plugin):
@ -9,13 +10,18 @@ class Network(Plugin):
"""
def _detect(self):
"""Run detection"""
"""Run detection, set self.available True if the service is detected."""
self.available = True
def build_config(self):
"""No detection just copy the config"""
shutil.copyfile(os.path.join(self.template_dir, 'conf.d/network.yaml'),
os.path.join(self.config_dir, 'conf.d/network.yaml'))
"""Build the config as a Plugins object and return.
"""
# A bit silly to parse the yaml only for it to be converted back but this plugin is the exception not the rule
with open(os.path.join(self.template_dir, 'conf.d/network.yaml'), 'r') as network_template:
default_net_config = yaml.load(network_template.read())
config = agent_config.Plugins()
config['network'] = default_net_config
return config
def dependencies_installed(self):
return True

View File

@ -1,4 +1,5 @@
from . import Plugin
from monsetup import agent_config
class Nova(Plugin):
@ -9,7 +10,9 @@ class Nova(Plugin):
self.available = True
def build_config(self):
pass
"""Build the config as a Plugins object and return.
"""
return agent_config.Plugins()
def dependencies_installed(self):
pass

102
monsetup/main.py Normal file
View File

@ -0,0 +1,102 @@
#!/usr/bin/env python
""" Detect running daemons then configure and start the agent.
"""
import argparse
import logging
import os
import socket
import subprocess
import sys
import yaml
import agent_config
from detection import mysql, network, nova
from service import sysv
# List of all detection plugins to run
DETECTION_PLUGINS = [mysql.MySQL, network.Network, nova.Nova]
# Map OS to service type
OS_SERVICE_MAP = {'linux': sysv.SysV}
log = logging.getLogger(__name__)
def main(argv=None):
parser = argparse.ArgumentParser(description='Detect running daemons then configure and start the agent.')
parser.add_argument('-u', '--username', help="Keystone username used to post metrics", required=True)
parser.add_argument('-p', '--password', help="Keystone password used to post metrics", required=True)
parser.add_argument('-s', '--service', help="Service this node is associated with.", required=True)
parser.add_argument('--keystone_url', help="Keystone url", required=True)
parser.add_argument('--mon_url', help="Mon API url", required=True)
parser.add_argument('--config_dir', help="Alternative configuration directory", default='/etc/mon-agent')
parser.add_argument('--template_dir', help="Alternative template directory", default='/usr/local/share/mon/agent')
parser.add_argument('--headless', help="Run in a non-interactive mode", action="store_true")
parser.add_argument('--overwrite',
help="Overwrite existing plugin configuration." +
"The default is to merge. Agent.conf is always overwritten.",
action="store_true")
parser.add_argument('-v', '--verbose', help="Verbose Output", action="store_true")
#todo provide an option to exclude certain detection plugins
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
else:
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
# todo implement a way to include some predefined configuration, useful for active checks
# Detect os
detected_os = 'linux' # todo add detection
# Service enable, includes setup of config directories so must be done before configuration
# todo is there a better place for basic directories to be made then the service enabling?
agent_service = OS_SERVICE_MAP[detected_os](os.path.join(args.template_dir, 'mon-agent.init'), args.config_dir)
# Todo add logic for situations where either enable or start is not needed or if not running as root isn't possible
agent_service.enable()
# Write the main agent.conf - Note this is always overwritten
log.info('Configuring base Agent settings.')
with open(os.path.join(args.template_dir, 'agent.conf.template'), 'r') as agent_template:
with open(os.path.join(args.config_dir, 'agent.conf'), 'w') as agent_conf:
agent_conf.write(agent_template.read().format(args=args, hostname=socket.gethostname()))
# Link the supervisor.conf
supervisor_path = os.path.join(args.config_dir, 'supervisor.conf')
if os.path.exists(supervisor_path):
os.remove(supervisor_path)
os.symlink(os.path.join(args.template_dir, 'supervisor.conf'), supervisor_path)
# Run through detection and config building for the plugins
plugin_config = agent_config.Plugins()
for detect_class in DETECTION_PLUGINS:
detect = detect_class(args.template_dir, args.overwrite)
if detect.available:
log.info('Configuring {0}'.format(detect.name))
new_config = detect.build_config()
plugin_config.update(new_config)
#todo add option to install dependencies
# Write out the plugin config
for key, value in plugin_config.iteritems():
# todo if overwrite is set I should either warn or just delete any config files not in the new config
# todo add the ability to show a diff before overwriting or merging config
config_path = os.path.join(args.config_dir, 'conf.d', key + '.yaml')
if (not args.overwrite) and os.path.exists(config_path): # merge old and new config, new has precedence
with open(config_path, 'r') as config_file:
old_config = yaml.load(config_file.read())
if old_config is not None:
value = old_config.update(value)
with open(config_path, 'w') as config_file:
config_file.write(yaml.dump(value))
# Now that the config is build start the service
try:
agent_service.start(restart=True)
except subprocess.CalledProcessError:
log.error('The service did not startup correctly see /var/log/mon-agent')
if __name__ == "__main__":
sys.exit(main())

View File

@ -134,7 +134,7 @@ setup(
'mon-forwarder = monagent.forwarder:main',
'mon-collector = monagent.collector.daemon:main',
'monstatsd = monagent.monstatsd:main',
'mon-setup = monsetup:main'
'mon-setup = monsetup.main:main'
],
},
include_package_data=True,