"""
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)
- placeholders are ignored
"""
+import ConfigParser
import json
import logging
import os
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.
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"
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:
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")
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):
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):
"""
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()
--- /dev/null
+###
+### 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()