]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Initial cfn-hup (wip)
authorAngus Salkeld <asalkeld@redhat.com>
Mon, 23 Apr 2012 09:54:11 +0000 (19:54 +1000)
committerAngus Salkeld <asalkeld@redhat.com>
Mon, 23 Apr 2012 09:54:11 +0000 (19:54 +1000)
Signed-off-by: Angus Salkeld <asalkeld@redhat.com>
heat/cfntools/cfn-hup
heat/cfntools/cfn-init
heat/cfntools/cfn_helper.py
heat/tests/test_cfn.py [new file with mode: 0644]

index af2411500dbcc468524208cfd334c74025ade5b6..7889ed2268f9c65affce18b0fdd603a28a85d3c5 100755 (executable)
 """
 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)
index 224d41347899268a2b50834495dce465f883445a..155289e4f8af319dae87ab8fdcb8724f3253be52 100755 (executable)
@@ -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()
index a6eec665ded0087add281882f4c4ba2fd0317f7b..fef28d5e34522d5f0cf6ee0a9ae7af1cf3141877 100644 (file)
@@ -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 (file)
index 0000000..025d603
--- /dev/null
@@ -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()