From: Angus Salkeld Date: Mon, 23 Apr 2012 09:54:11 +0000 (+1000) Subject: Initial cfn-hup (wip) X-Git-Tag: 2014.1~1923^2~20 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=12797d82299b9e5698a25b6b7284642bed72ab79;p=openstack-build%2Fheat-build.git Initial cfn-hup (wip) Signed-off-by: Angus Salkeld --- diff --git a/heat/cfntools/cfn-hup b/heat/cfntools/cfn-hup index af241150..7889ed22 100755 --- a/heat/cfntools/cfn-hup +++ b/heat/cfntools/cfn-hup @@ -15,3 +15,68 @@ """ Implements cfn-hup CloudFormation functionality """ +import argparse +import io +import logging +import os +import os.path +import sys + +if os.path.exists('/opt/aws/bin'): + sys.path.insert(0, '/opt/aws/bin') + from cfn_helper import * +else: + from heat.cfntools.cfn_helper import * + +log_format = '%(levelname)s [%(asctime)s] %(message)s' +# setup stdout logging +logging.basicConfig(format=log_format, level=logging.INFO) + +description = " " +parser = argparse.ArgumentParser(description=description) +parser.add_argument('-c', '--config', + dest="config_dir", + help="Hook Config Directory", + required=False, + default=None) +parser.add_argument('-f', '--no-daemon', + dest="no_deamon", + help="Do not run as a deamon", + required=False) +parser.add_argument('-v', '--verbose', + dest="verbose", + help="Verbose logging", + required=False) +args = parser.parse_args() +# FIXME: implement real arg + +config_files = [] +try: + config_files.append(open(os.path.join('/etc', 'cfn-hup.conf'))) +except OSError as exc: + logging.exception(exc) + pass + +if args.config_dir: + try: + for f in os.listdir(args.config_dir): + config_files.append(open(os.path.join(args.config_dir, f))) + + except OSError as exc: + logging.exception(exc) + pass + +mainconfig = HupConfig(config_files) + +for r in mainconfig.unique_resources_get(): + print r + metadata = Metadata(mainconfig.stack, + r, + credentials_file=mainconfig.credential_file, + region=mainconfig.region) + metadata.retrieve() + try: + metadata.cfn_hup(mainconfig.hooks) + except Exception as e: + logging.exception("Error processing metadata") + exit(1) diff --git a/heat/cfntools/cfn-init b/heat/cfntools/cfn-init index 224d4134..155289e4 100755 --- a/heat/cfntools/cfn-init +++ b/heat/cfntools/cfn-init @@ -70,8 +70,11 @@ parser.add_argument('--region', args = parser.parse_args() # FIXME: implement real arg -metadata = Metadata(stack, resource, access_key=access_key, - secret_key=secret_key, region=region) +metadata = Metadata(args.stack_name, + args.logical_resource_id, + access_key=args.access_key, + secret_key=args.secret_key, + region=args.region) metadata.retrieve() try: metadata.cfn_init() diff --git a/heat/cfntools/cfn_helper.py b/heat/cfntools/cfn_helper.py index a6eec665..fef28d5e 100644 --- a/heat/cfntools/cfn_helper.py +++ b/heat/cfntools/cfn_helper.py @@ -29,6 +29,7 @@ Not implemented yet: - placeholders are ignored """ +import ConfigParser import json import logging import os @@ -43,6 +44,82 @@ def to_boolean(b): return b in [True, 'true', 'yes', '1', 1] +class HupConfig(object): + def __init__(self, fp_list): + self.config = ConfigParser.SafeConfigParser(allow_no_value=True) + for fp in fp_list: + self.config.readfp(fp) + + self.hooks = {} + for s in self.config.sections(): + if s == 'main': + self.get_main_section() + else: + self.hooks[s] = Hook(s, + self.config.get(s, 'triggers'), + self.config.get(s, 'path'), + self.config.get(s, 'runas'), + self.config.get(s, 'action')) + + def get_main_section(self): + # required values + self.stack = self.config.get('main', 'stack') + self.credential_file = self.config.get('main', 'credential-file') + + # optional values + try: + self.region = self.config.get('main', 'region') + except ConfigParser.NoOptionError: + self.region = 'nova' + + try: + self.interval = self.config.getint('main', 'interval') + except ConfigParser.NoOptionError: + self.interval = 10 + + def __str__(self): + return '{stack: %s, credential_file: %s, region: %s, interval:%d}' % \ + (self.stack, self.credential_file, self.region, self.interval) + + def unique_resources_get(self): + resources = [] + for h in self.hooks: + r = self.hooks[h].resource_name_get() + if not r in resources: + resources.append(self.hooks[h].resource_name_get()) + return resources + + +class Hook(object): + def __init__(self, name, triggers, path, runas, action): + self.name = name + self.triggers = triggers + self.path = path + self.runas = runas + self.action = action + + def resource_name_get(self): + sp = self.path.split('.') + return sp[1] + + def event(self, ev_name, ev_object, ev_resource): + if self.resource_name_get() != ev_resource: + return + if ev_name in self.triggers: + print 'su %s -c %s' % (self.runas, self.action) + #CommandRunner(self.action).run() + else: + print 'miss: %s %s' % (ev_name, ev_object) + + def __str__(self): + return '{%s, %s, %s, %s, %s}' % \ + (self.name, + self.triggers, + self.path, + self.runas, + self.action) + + class CommandRunner(object): """ Helper class to run a command and store the output. @@ -382,8 +459,10 @@ class PackagesHandler(object): class ServicesHandler(object): _services = {} - def __init__(self, services): + def __init__(self, services, resource=None, hooks=None): self._services = services + self.resource = resource + self.hooks = hooks def _handle_sysv_command(self, service, command): service_exe = "/sbin/service" @@ -421,7 +500,7 @@ class ServicesHandler(object): command.run() return command - def _initialize_service(self, handler, service, properties): + def _handle_service(self, handler, service, properties): if "enabled" in properties: enable = to_boolean(properties["enabled"]) if enable: @@ -438,6 +517,9 @@ class ServicesHandler(object): if ensure_running and not running: logging.info("Starting service %s" % service) handler(self, service, "start") + for h in self.hooks: + self.hooks[h].event('service.restarted', + service, self.resource) elif not ensure_running and running: logging.info("Stopping service %s" % service) handler(self, service, "stop") @@ -491,6 +573,8 @@ class Metadata(object): self.credentials_file = credentials_file self.region = region + # TODO(asalkeld) is this metadata for the local resource? + self._is_local_metadata = True self._metadata = None def retrieve(self): @@ -498,8 +582,12 @@ class Metadata(object): Read the metadata from the given filename and return the string """ f = open("/var/lib/cloud/data/cfn-init-data") - self._metadata = f.read() + self._data = f.read() f.close() + if isinstance(self._data, str): + self._metadata = json.loads(self._data) + else: + self._metadata = self._data def _is_valid_metadata(self): """ @@ -544,3 +632,16 @@ class Metadata(object): raise Exception("invalid metadata") else: self._process_config() + + def cfn_hup(self, hooks): + """ + Process the resource metadata + """ + if not self._is_valid_metadata(): + raise Exception("invalid metadata") + else: + if self._is_local_metadata: + self._config = self._metadata["config"] + s = self._config.get("services") + sh = ServicesHandler(s, resource=self.resource, hooks=hooks) + sh.apply_services() diff --git a/heat/tests/test_cfn.py b/heat/tests/test_cfn.py new file mode 100644 index 00000000..025d6030 --- /dev/null +++ b/heat/tests/test_cfn.py @@ -0,0 +1,75 @@ +### +### an unparented test -- no encapsulating class, just any fn starting with +### 'test'. +## http://darcs.idyll.org/~t/projects/nose-demo/simple/tests/test_stuff.py.html +### + +import io +import sys +import nose +from nose.plugins.attrib import attr +from nose import with_setup + +from heat.cfntools.cfn_helper import * + + +@attr(tag=['cfn-hup']) +@attr(speed='fast') +def test_hup_conf1(): + good= """ +[main] +stack=stack-test +credential-file=/path/to/creds_file +region=unit-test-a +interval=3 +""" + c = HupConfig([io.BytesIO(good)]) + assert(c.stack == 'stack-test') + assert(c.credential_file == '/path/to/creds_file') + assert(c.region == 'unit-test-a') + assert(c.interval == 3) + + +@attr(tag=['cfn-hup']) +@attr(speed='fast') +def test_hup_default(): + good= """ +[main] +stack=stack-testr +credential-file=/path/to/creds_file +""" + c = HupConfig([io.BytesIO(good)]) + assert(c.stack == 'stack-testr') + assert(c.credential_file == '/path/to/creds_file') + assert(c.region == 'nova') + assert(c.interval == 10) + + +@attr(tag=['cfn-hup']) +@attr(speed='fast') +def test_hup_hook(): + good= """ +[main] +stack=stackname_is_fred +credential-file=/path/to/creds_file + +[bla] +triggers=post.update +path=Resources.webserver +action=systemctl reload httpd.service +runas=root +""" + c = HupConfig([io.BytesIO(good)]) + assert(c.stack == 'stackname_is_fred') + assert(c.credential_file == '/path/to/creds_file') + assert(c.region == 'nova') + assert(c.interval == 10) + + assert(c.hooks['bla'].triggers == 'post.update') + assert(c.hooks['bla'].path == 'Resources.webserver') + assert(c.hooks['bla'].action == 'systemctl reload httpd.service') + assert(c.hooks['bla'].runas == 'root') + +if __name__ == '__main__': + sys.argv.append(__file__) + nose.main()