]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Move common cfn code into cfn_helper.py
authorAngus Salkeld <asalkeld@redhat.com>
Mon, 23 Apr 2012 03:08:32 +0000 (13:08 +1000)
committerAngus Salkeld <asalkeld@redhat.com>
Mon, 23 Apr 2012 03:08:32 +0000 (13:08 +1000)
Signed-off-by: Angus Salkeld <asalkeld@redhat.com>
bin/heat
heat/cfntools/__init__.py [new file with mode: 0644]
heat/cfntools/cfn-init
heat/cfntools/cfn_helper.py [new file with mode: 0644]
heat/jeos/F16-i386-cfntools-jeos.tdl
heat/jeos/F16-x86_64-cfntools-jeos.tdl
heat/jeos/F17-i386-cfntools-jeos.tdl
heat/jeos/F17-x86_64-cfntools-jeos.tdl

index dc79e58799afaefe7b7d5fc8dae70d22df81f031..2b5e01b25d68249c9cd8499b736db7d26b3ee70d 100755 (executable)
--- a/bin/heat
+++ b/bin/heat
@@ -370,7 +370,7 @@ def jeos_create(options, arguments):
     # and injecting them into the TDL at the appropriate place
     if instance_type == 'cfntools':
         tdl_xml = libxml2.parseFile(tdl_path)
-        for cfnname in ['cfn-init', 'cfn-hup', 'cfn-signal']:
+        for cfnname in ['cfn-init', 'cfn-hup', 'cfn-signal', 'cfn_helper.py']:
             f = open('%s/%s' % (cfntools_path, cfnname), 'r')
             cfscript_e64 = base64.b64encode(f.read())
             f.close()
diff --git a/heat/cfntools/__init__.py b/heat/cfntools/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
index 4ea48f7ff2cb6b644d6355b32840e3efd0331190..224d41347899268a2b50834495dce465f883445a 100755 (executable)
@@ -31,543 +31,19 @@ Not implemented yet:
 """
 
 import argparse
-import json
 import logging
 import os
-import rpmUtils.updates as rpmupdates
-import rpmUtils.miscutils as rpmutils
-import subprocess
 import sys
 
 
-log_file_name = "/var/log/cfn-init.log"
+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)
-# setup file logging
-file_handler = logging.FileHandler(log_file_name)
-file_handler.setFormatter(logging.Formatter(log_format))
-logging.getLogger().addHandler(file_handler)
-
-class CommandRunner(object):
-    """
-    Helper class to run a command and store the output.
-    """
-
-    def __init__(self, command):
-        self._command = command
-        self._stdout = None
-        self._stderr = None
-        self._status = None
-
-
-    def __str__(self):
-        s = "CommandRunner:"
-        s += "\n\tcommand: %s" % self._command
-        if self._status:
-            s += "\n\tstatus: %s" % self._status
-        if self._stdout:
-            s += "\n\tstdout: %s" % self._stdout
-        if self._stderr:
-            s += "\n\tstderr: %s" % self._stderr
-        return s
-
-
-    def run(self):
-        """
-        Run the Command and return the output.
-
-        Returns:
-            self
-        """
-        logging.debug("Running command: %s" % self._command)
-        cmd = self._command.split()
-        subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-        output = subproc.communicate()
-
-        self._status = subproc.returncode
-        self._stdout = output[0]
-        self._stderr = output[1]
-
-        return self
-
-    @property
-    def stdout(self):
-        return self._stdout
-
-    @property
-    def stderr(self):
-        return self._stderr
-
-    @property
-    def status(self):
-        return self._status
-
-
-class RpmHelper(object):
-
-    _rpm_util = rpmupdates.Updates([], [])
-
-    @classmethod
-    def prepcache(cls):
-        """
-        Prepare the yum cache
-        """
-        CommandRunner("yum -y makecache").run()
-
-
-    @classmethod
-    def compare_rpm_versions(cls, v1, v2):
-        """
-        Compare two RPM version strings.
-
-        Arguments:
-            v1 -- a version string
-            v2 -- a version string
-
-        Returns:
-            0 -- the versions are equal
-            1 -- v1 is greater
-           -1 -- v2 is greater
-        """
-        if v1 and v2:
-            return rpmutils.compareVerOnly(v1, v2)
-        elif v1:
-            return 1
-        elif v2:
-            return -1
-        else:
-            return 0
-
-
-    @classmethod
-    def newest_rpm_version(cls, versions):
-        """
-        Returns the highest (newest) version from a list of versions.
-
-        Arguments:
-            versions -- A list of version strings
-                        e.g., ['2.0', '2.2', '2.2-1.fc16', '2.2.22-1.fc16']
-        """
-        if versions:
-            if isinstance(versions, basestring):
-                return versions
-            versions = sorted(versions, rpmutils.compareVerOnly,
-                    reverse=True)
-            return versions[0]
-        else:
-            return None
-
-
-    @classmethod
-    def rpm_package_version(cls, pkg):
-        """
-        Returns the version of an installed RPM.
-
-        Arguments:
-            pkg -- A package name
-        """
-        cmd = "rpm -q --queryformat '%{VERSION}-%{RELEASE}' %s" % pkg
-        command = CommandRunner(cmd).run()
-        return command.stdout
-
-
-    @classmethod
-    def rpm_package_installed(cls, pkg):
-        """
-        Indicates whether pkg is in rpm database.
-
-        Arguments:
-            pkg -- A package name (with optional version and release spec).
-                   e.g., httpd
-                   e.g., httpd-2.2.22
-                   e.g., httpd-2.2.22-1.fc16
-        """
-        command = CommandRunner("rpm -q %s" % pkg).run()
-        return command.status == 0
-
-
-    @classmethod
-    def yum_package_available(cls, pkg):
-        """
-        Indicates whether pkg is available via yum
-
-        Arguments:
-            pkg -- A package name (with optional version and release spec).
-                   e.g., httpd
-                   e.g., httpd-2.2.22
-                   e.g., httpd-2.2.22-1.fc16
-        """
-        command = CommandRunner("yum -C -y --showduplicates list available %s" % pkg).run()
-        return command.status == 0
-
-
-    @classmethod
-    def install(cls, packages, rpms=True):
-        """
-        Installs (or upgrades) a set of packages via RPM or via Yum.
-
-        Arguments:
-            packages -- a list of packages to install
-            rpms     -- if True:
-                        * use RPM to install the packages
-                        * packages must be a list of URLs to retrieve RPMs
-                        if False:
-                        * use Yum to install packages
-                        * packages is a list of:
-                          - pkg name (httpd), or
-                          - pkg name with version spec (httpd-2.2.22), or
-                          - pkg name with version-release spec (httpd-2.2.22-1.fc16)
-        """
-        if rpms:
-            cmd = "rpm -U --force --nosignature "
-            cmd += " ".join(packages)
-            logging.info("Installing packages: %s" % cmd)
-        else:
-            cmd = "yum -y install "
-            cmd += " ".join(packages)
-            logging.info("Installing packages: %s" % cmd)
-        command = CommandRunner(cmd).run()
-        if command.status:
-            logging.warn("Failed to install packages: %s" % cmd)
-
-
-    @classmethod
-    def downgrade(cls, packages, rpms=True):
-        """
-        Downgrades a set of packages via RPM or via Yum.
-
-        Arguments:
-            packages -- a list of packages to downgrade
-            rpms     -- if True:
-                        * use RPM to downgrade (replace) the packages
-                        * packages must be a list of URLs to retrieve the RPMs
-                        if False:
-                        * use Yum to downgrade packages
-                        * packages is a list of:
-                          - pkg name with version spec (httpd-2.2.22), or
-                          - pkg name with version-release spec (httpd-2.2.22-1.fc16)
-        """
-        if rpms:
-            cls.install(packages)
-        else:
-            cmd = "yum -y downgrade "
-            cmd += " ".join(packages)
-            logging.info("Downgrading packages: %s" % cmd)
-            command = Command(cmd).run()
-            if command.status:
-                logging.warn("Failed to downgrade packages: %s" % cmd)
-
-
-class PackagesHandler(object):
-    _packages = {}
-
-    _package_order = ["dpkg", "rpm", "apt", "yum"]
-
-    @staticmethod
-    def _pkgsort(pkg1, pkg2):
-        order = PackagesHandler._package_order
-        p1_name = pkg1[0]
-        p2_name = pkg2[0]
-        if p1_name in order and p2_name in order:
-            return cmp(order.index(p1_name), order.index(p2_name))
-        elif p1_name in order:
-            return -1
-        elif p2_name in order:
-            return 1
-        else:
-            return cmp(p1_name.lower(), p2_name.lower())
-
-
-    def __init__(self, packages):
-        self._packages = packages
-
-
-    def _handle_gem_packages(self, packages):
-        #FIXME: handle rubygems
-        pass
-
-
-    def _handle_python_packages(self, packages):
-        #FIXME: handle python easyinstall
-        pass
-
-
-    def _handle_yum_packages(self, packages):
-        """
-        Handle installation, upgrade, or downgrade of a set of packages via yum.
-
-        Arguments:
-        packages -- a package entries map of the form:
-                      "pkg_name" : "version",
-                      "pkg_name" : ["v1", "v2"],
-                      "pkg_name" : []
-
-        For each package entry:
-          * if no version is supplied and the package is already installed, do
-            nothing
-          * if no version is supplied and the package is _not_ already
-            installed, install it
-          * if a version string is supplied, and the package is already
-            installed, determine whether to downgrade or upgrade (or do nothing
-            if version matches installed package)
-          * if a version array is supplied, choose the highest version from the
-            array and follow same logic for version string above
-        """
-        # collect pkgs for batch processing at end
-        installs = []
-        downgrades = []
-        # update yum cache
-        RpmHelper.prepcache()
-        for pkg_name, versions in packages.iteritems():
-            ver = RpmHelper.newest_rpm_version(versions)
-            pkg = "%s-%s" % (pkg_name, ver) if ver else pkg_name
-            if RpmHelper.rpm_package_installed(pkg):
-                pass # FIXME:print non-error, but skipping pkg
-            elif not RpmHelper.yum_package_available(pkg):
-                logging.warn("Skipping package '%s'. Not available via yum" % pkg)
-            elif not ver:
-                installs.append(pkg)
-            else:
-                current_ver = RpmHelper.rpm_package_version(pkg)
-                rc = RpmHelper.compare_rpm_versions(current_ver, ver)
-                if rc < 0:
-                    installs.append(pkg)
-                elif rc > 0:
-                    downgrades.append(pkg)
-            if installs:
-                RpmHelper.install(installs, rpms=False)
-            if downgrades:
-                RpmHelper.downgrade(downgrades)
-
-
-    def _handle_rpm_packages(sef, packages):
-        """
-        Handle installation, upgrade, or downgrade of a set of packages via rpm.
-
-        Arguments:
-        packages -- a package entries map of the form:
-                      "pkg_name" : "url"
-
-        For each package entry:
-          * if the EXACT package is already installed, skip it
-          * if a different version of the package is installed, overwrite it
-          * if the package isn't installed, install it
-        """
-        #FIXME: handle rpm installs
-        pass
-
-
-    def _handle_apt_packages(self, packages):
-        #FIXME: handle apt-get
-        pass
-
-
-    # map of function pionters to handle different package managers
-    _package_handlers = {
-            "yum" : _handle_yum_packages,
-            "rpm" : _handle_rpm_packages,
-            "apt" : _handle_apt_packages,
-            "rubygems" : _handle_gem_packages,
-            "python" : _handle_python_packages
-    }
-
-    def _package_handler(self, manager_name):
-        handler = None
-        if manager_name in self._package_handlers:
-            handler = self._package_handlers[manager_name]
-        return handler
-
-
-    def apply_packages(self):
-        """
-        Install, upgrade, or downgrade packages listed
-        Each package is a dict containing package name and a list of versions
-        Install order:
-          * dpkg
-          * rpm
-          * apt
-          * yum
-        """
-        packages = sorted(self._packages.iteritems(), PackagesHandler._pkgsort)
-
-        for manager, package_entries in packages:
-            handler = self._package_handler(manager)
-            if not handler:
-                logging.warn("Skipping invalid package type: %s" % manager)
-            else:
-                handler(self, package_entries)
-
-
-class ServicesHandler(object):
-    _services = {}
-
-
-    def __init__(self, services):
-        self._services = services
-
-
-    def _handle_sysv_command(self, service, command):
-        service_exe = "/sbin/service"
-        enable_exe = "/sbin/chkconfig"
-        cmd = ""
-        if "enable" == command:
-            cmd = "%s %s on" % (enable_exe, service)
-        elif "disable" == command:
-            cmd = "%s %s off" % (enable_exe, service)
-        elif "start" == command:
-            cmd = "%s %s start" % (service_exe, service)
-        elif "stop" == command:
-            cmd = "%s %s stop" % (service_exe, service)
-        elif "status" == command:
-            cmd = "%s %s status" % (service_exe, service)
-        command = CommandRunner(cmd)
-        command.run()
-        return command
-
-
-    def _handle_systemd_command(self, service, command):
-        exe = "/bin/systemctl"
-        cmd = ""
-        service = '%s.service' % service
-        if "enable" == command:
-            cmd = "%s enable %s" % (exe, service)
-        elif "disable" == command:
-            cmd = "%s disable %s" % (exe, service)
-        elif "start" == command:
-            cmd = "%s start %s" % (exe, service)
-        elif "stop" == command:
-            cmd = "%s stop %s" % (exe, service)
-        elif "status" == command:
-            cmd = "%s status %s" % (exe, service)
-        command = CommandRunner(cmd)
-        command.run()
-        return command
-
-
-    def _handle_service(self, handler, service, properties):
-        if "enabled" in properties:
-            enable = to_boolean(properties["enabled"])
-            if enable:
-                logging.info("Enabling service %s" % service)
-                handler(self, service, "enable")
-            else:
-                logging.info("Disabling service %s" % service)
-                handler(self, service, "disable")
-
-        if "ensureRunning" in properties:
-            ensure_running = to_boolean(properties["ensureRunning"])
-            command = handler(self, service, "status")
-            running = command.status == 0
-            if ensure_running and not running:
-                logging.info("Starting service %s" % service)
-                handler(self, service, "start")
-            elif not ensure_running and running:
-                logging.info("Stopping service %s" % service)
-                handler(self, service, "stop")
-
-
-    def _handle_services(self, handler, services):
-        for service, properties in services.iteritems():
-            self._handle_service(handler, service, properties)
-
-
-    # map of function pointers to various service handlers
-    _service_handlers = {
-        "sysvinit" : _handle_sysv_command,
-        "systemd" : _handle_systemd_command
-    }
-
-
-    def _service_handler(self, manager_name):
-        handler = None
-        if manager_name in self._service_handlers:
-            handler = self._service_handlers[manager_name]
-        return handler
-
-
-    def apply_services(self):
-        """
-        Starts, stops, enables, disables services
-        """
-        for manager, service_entries in self._services.iteritems():
-            handler = self._service_handler(manager)
-            if not handler:
-                logging.warn("Skipping invalid service type: %s" % manager)
-            else:
-                self._handle_services(handler, service_entries)
-
-
-class Metadata(object):
-    _metadata = None
-    _init_key = "AWS::CloudFormation::Init"
-
-    def __init__(self, metadata):
-        self._metadata = json.loads(metadata)
-
-
-    def _is_valid_metadata(self):
-        """
-        Should find the AWS::CloudFormation::Init json key
-        """
-        is_valid = self._metadata and self._init_key in self._metadata and self._metadata[self._init_key]
-        if is_valid:
-            self._metadata = self._metadata[self._init_key]
-        return is_valid
-
-
-    def _process_config(self):
-        """
-        Parse and process a config section
-          * packages
-          * sources (not yet)
-          * users (not yet)
-          * groups (not yet)
-          * files (not yet)
-          * commands (not yet)
-          * services
-        """
-
-        self._config = self._metadata["config"]
-        PackagesHandler(self._config.get("packages")).apply_packages()
-        #FIXME: handle sources
-        #FIXME: handle users
-        #FIXME: handle groups
-        #FIXME: handle files
-        #FIXME: handle commands
-        ServicesHandler(self._config.get("services")).apply_services()
-
-
-    def process(self):
-        """
-        Process the resource metadata
-        """
-        # FIXME: when config sets are implemented, this should select the correct
-        # config set from the metadata, and send each config in the config set to
-        # process_config
-        if not self._is_valid_metadata():
-            raise Exception("invalid metadata")
-        else:
-            self._process_config()
-
-
-def to_boolean(b):
-    val = b.lower().strip() if isinstance(b, basestring) else b
-    return b in [True, 'true', 'yes', '1', 1]
-
-
-def get_metadata(fname):
-    """
-    Read the metadata from the given filename and return the string
-    """
-    f = open(fname)
-    meta = f.read()
-    f.close()
-    return meta
-
-
-## Main
-metadata_file = "/var/lib/cloud/data/cfn-init-data"
 
 description = " "
 parser = argparse.ArgumentParser(description=description)
@@ -594,9 +70,11 @@ parser.add_argument('--region',
 args = parser.parse_args()
 # FIXME: implement real arg
 
-metadata = Metadata(get_metadata(metadata_file))
+metadata = Metadata(stack, resource, access_key=access_key,
+                    secret_key=secret_key, region=region)
+metadata.retrieve()
 try:
-    metadata.process()
+    metadata.cfn_init()
 except Exception as e:
     logging.exception("Error processing metadata")
     exit(1)
diff --git a/heat/cfntools/cfn_helper.py b/heat/cfntools/cfn_helper.py
new file mode 100644 (file)
index 0000000..a6eec66
--- /dev/null
@@ -0,0 +1,546 @@
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+Implements cfn metadata handling
+
+Resource metadata currently implemented:
+    * config/packages
+    * config/services
+
+Not implemented yet:
+    * config sets
+    * config/sources
+    * config/commands
+    * config/files
+    * config/users
+    * config/groups
+    * command line args
+      - placeholders are ignored
+"""
+
+import json
+import logging
+import os
+import rpmUtils.updates as rpmupdates
+import rpmUtils.miscutils as rpmutils
+import subprocess
+import sys
+
+
+def to_boolean(b):
+    val = b.lower().strip() if isinstance(b, basestring) else b
+    return b in [True, 'true', 'yes', '1', 1]
+
+
+class CommandRunner(object):
+    """
+    Helper class to run a command and store the output.
+    """
+
+    def __init__(self, command):
+        self._command = command
+        self._stdout = None
+        self._stderr = None
+        self._status = None
+
+    def __str__(self):
+        s = "CommandRunner:"
+        s += "\n\tcommand: %s" % self._command
+        if self._status:
+            s += "\n\tstatus: %s" % self._status
+        if self._stdout:
+            s += "\n\tstdout: %s" % self._stdout
+        if self._stderr:
+            s += "\n\tstderr: %s" % self._stderr
+        return s
+
+    def run(self):
+        """
+        Run the Command and return the output.
+
+        Returns:
+            self
+        """
+        logging.debug("Running command: %s" % self._command)
+        cmd = self._command.split()
+        subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        output = subproc.communicate()
+
+        self._status = subproc.returncode
+        self._stdout = output[0]
+        self._stderr = output[1]
+
+        return self
+
+    @property
+    def stdout(self):
+        return self._stdout
+
+    @property
+    def stderr(self):
+        return self._stderr
+
+    @property
+    def status(self):
+        return self._status
+
+
+class RpmHelper(object):
+
+    _rpm_util = rpmupdates.Updates([], [])
+
+    @classmethod
+    def prepcache(cls):
+        """
+        Prepare the yum cache
+        """
+        CommandRunner("yum -y makecache").run()
+
+    @classmethod
+    def compare_rpm_versions(cls, v1, v2):
+        """
+        Compare two RPM version strings.
+
+        Arguments:
+            v1 -- a version string
+            v2 -- a version string
+
+        Returns:
+            0 -- the versions are equal
+            1 -- v1 is greater
+           -1 -- v2 is greater
+        """
+        if v1 and v2:
+            return rpmutils.compareVerOnly(v1, v2)
+        elif v1:
+            return 1
+        elif v2:
+            return -1
+        else:
+            return 0
+
+    @classmethod
+    def newest_rpm_version(cls, versions):
+        """
+        Returns the highest (newest) version from a list of versions.
+
+        Arguments:
+            versions -- A list of version strings
+                        e.g., ['2.0', '2.2', '2.2-1.fc16', '2.2.22-1.fc16']
+        """
+        if versions:
+            if isinstance(versions, basestring):
+                return versions
+            versions = sorted(versions, rpmutils.compareVerOnly,
+                    reverse=True)
+            return versions[0]
+        else:
+            return None
+
+    @classmethod
+    def rpm_package_version(cls, pkg):
+        """
+        Returns the version of an installed RPM.
+
+        Arguments:
+            pkg -- A package name
+        """
+        cmd = "rpm -q --queryformat '%{VERSION}-%{RELEASE}' %s" % pkg
+        command = CommandRunner(cmd).run()
+        return command.stdout
+
+    @classmethod
+    def rpm_package_installed(cls, pkg):
+        """
+        Indicates whether pkg is in rpm database.
+
+        Arguments:
+            pkg -- A package name (with optional version and release spec).
+                   e.g., httpd
+                   e.g., httpd-2.2.22
+                   e.g., httpd-2.2.22-1.fc16
+        """
+        command = CommandRunner("rpm -q %s" % pkg).run()
+        return command.status == 0
+
+    @classmethod
+    def yum_package_available(cls, pkg):
+        """
+        Indicates whether pkg is available via yum
+
+        Arguments:
+            pkg -- A package name (with optional version and release spec).
+                   e.g., httpd
+                   e.g., httpd-2.2.22
+                   e.g., httpd-2.2.22-1.fc16
+        """
+        cmd_str = "yum -C -y --showduplicates list available %s" % pkg
+        command = CommandRunner(cmd_str).run()
+        return command.status == 0
+
+    @classmethod
+    def install(cls, packages, rpms=True):
+        """
+        Installs (or upgrades) a set of packages via RPM or via Yum.
+
+        Arguments:
+            packages -- a list of packages to install
+            rpms     -- if True:
+                        * use RPM to install the packages
+                        * packages must be a list of URLs to retrieve RPMs
+                        if False:
+                        * use Yum to install packages
+                        * packages is a list of:
+                          - pkg name (httpd), or
+                          - pkg name with version spec (httpd-2.2.22), or
+                          - pkg name with version-release spec (httpd-2.2.22-1.fc16)
+        """
+        if rpms:
+            cmd = "rpm -U --force --nosignature "
+            cmd += " ".join(packages)
+            logging.info("Installing packages: %s" % cmd)
+        else:
+            cmd = "yum -y install "
+            cmd += " ".join(packages)
+            logging.info("Installing packages: %s" % cmd)
+        command = CommandRunner(cmd).run()
+        if command.status:
+            logging.warn("Failed to install packages: %s" % cmd)
+
+    @classmethod
+    def downgrade(cls, packages, rpms=True):
+        """
+        Downgrades a set of packages via RPM or via Yum.
+
+        Arguments:
+            packages -- a list of packages to downgrade
+            rpms     -- if True:
+                        * use RPM to downgrade (replace) the packages
+                        * packages must be a list of URLs to retrieve the RPMs
+                        if False:
+                        * use Yum to downgrade packages
+                        * packages is a list of:
+                          - pkg name with version spec (httpd-2.2.22), or
+                          - pkg name with version-release spec (httpd-2.2.22-1.fc16)
+        """
+        if rpms:
+            cls.install(packages)
+        else:
+            cmd = "yum -y downgrade "
+            cmd += " ".join(packages)
+            logging.info("Downgrading packages: %s" % cmd)
+            command = Command(cmd).run()
+            if command.status:
+                logging.warn("Failed to downgrade packages: %s" % cmd)
+
+
+class PackagesHandler(object):
+    _packages = {}
+
+    _package_order = ["dpkg", "rpm", "apt", "yum"]
+
+    @staticmethod
+    def _pkgsort(pkg1, pkg2):
+        order = PackagesHandler._package_order
+        p1_name = pkg1[0]
+        p2_name = pkg2[0]
+        if p1_name in order and p2_name in order:
+            return cmp(order.index(p1_name), order.index(p2_name))
+        elif p1_name in order:
+            return -1
+        elif p2_name in order:
+            return 1
+        else:
+            return cmp(p1_name.lower(), p2_name.lower())
+
+    def __init__(self, packages):
+        self._packages = packages
+
+    def _handle_gem_packages(self, packages):
+        #FIXME: handle rubygems
+        pass
+
+    def _handle_python_packages(self, packages):
+        #FIXME: handle python easyinstall
+        pass
+
+    def _handle_yum_packages(self, packages):
+        """
+        Handle installation, upgrade, or downgrade of a set of packages via yum.
+
+        Arguments:
+        packages -- a package entries map of the form:
+                      "pkg_name" : "version",
+                      "pkg_name" : ["v1", "v2"],
+                      "pkg_name" : []
+
+        For each package entry:
+          * if no version is supplied and the package is already installed, do
+            nothing
+          * if no version is supplied and the package is _not_ already
+            installed, install it
+          * if a version string is supplied, and the package is already
+            installed, determine whether to downgrade or upgrade (or do nothing
+            if version matches installed package)
+          * if a version array is supplied, choose the highest version from the
+            array and follow same logic for version string above
+        """
+        # collect pkgs for batch processing at end
+        installs = []
+        downgrades = []
+        # update yum cache
+        RpmHelper.prepcache()
+        for pkg_name, versions in packages.iteritems():
+            ver = RpmHelper.newest_rpm_version(versions)
+            pkg = "%s-%s" % (pkg_name, ver) if ver else pkg_name
+            if RpmHelper.rpm_package_installed(pkg):
+                # FIXME:print non-error, but skipping pkg
+                pass
+            elif not RpmHelper.yum_package_available(pkg):
+                logging.warn("Skipping package '%s'. Not available via yum" % pkg)
+            elif not ver:
+                installs.append(pkg)
+            else:
+                current_ver = RpmHelper.rpm_package_version(pkg)
+                rc = RpmHelper.compare_rpm_versions(current_ver, ver)
+                if rc < 0:
+                    installs.append(pkg)
+                elif rc > 0:
+                    downgrades.append(pkg)
+            if installs:
+                RpmHelper.install(installs, rpms=False)
+            if downgrades:
+                RpmHelper.downgrade(downgrades)
+
+    def _handle_rpm_packages(sef, packages):
+        """
+        Handle installation, upgrade, or downgrade of a set of packages via rpm.
+
+        Arguments:
+        packages -- a package entries map of the form:
+                      "pkg_name" : "url"
+
+        For each package entry:
+          * if the EXACT package is already installed, skip it
+          * if a different version of the package is installed, overwrite it
+          * if the package isn't installed, install it
+        """
+        #FIXME: handle rpm installs
+        pass
+
+    def _handle_apt_packages(self, packages):
+        #FIXME: handle apt-get
+        pass
+
+    # map of function pionters to handle different package managers
+    _package_handlers = {
+            "yum": _handle_yum_packages,
+            "rpm": _handle_rpm_packages,
+            "apt": _handle_apt_packages,
+            "rubygems": _handle_gem_packages,
+            "python": _handle_python_packages
+    }
+
+    def _package_handler(self, manager_name):
+        handler = None
+        if manager_name in self._package_handlers:
+            handler = self._package_handlers[manager_name]
+        return handler
+
+    def apply_packages(self):
+        """
+        Install, upgrade, or downgrade packages listed
+        Each package is a dict containing package name and a list of versions
+        Install order:
+          * dpkg
+          * rpm
+          * apt
+          * yum
+        """
+        packages = sorted(self._packages.iteritems(), PackagesHandler._pkgsort)
+
+        for manager, package_entries in packages:
+            handler = self._package_handler(manager)
+            if not handler:
+                logging.warn("Skipping invalid package type: %s" % manager)
+            else:
+                handler(self, package_entries)
+
+
+class ServicesHandler(object):
+    _services = {}
+
+    def __init__(self, services):
+        self._services = services
+
+    def _handle_sysv_command(self, service, command):
+        service_exe = "/sbin/service"
+        enable_exe = "/sbin/chkconfig"
+        cmd = ""
+        if "enable" == command:
+            cmd = "%s %s on" % (enable_exe, service)
+        elif "disable" == command:
+            cmd = "%s %s off" % (enable_exe, service)
+        elif "start" == command:
+            cmd = "%s %s start" % (service_exe, service)
+        elif "stop" == command:
+            cmd = "%s %s stop" % (service_exe, service)
+        elif "status" == command:
+            cmd = "%s %s status" % (service_exe, service)
+        command = CommandRunner(cmd)
+        command.run()
+        return command
+
+    def _handle_systemd_command(self, service, command):
+        exe = "/bin/systemctl"
+        cmd = ""
+        service = '%s.service' % service
+        if "enable" == command:
+            cmd = "%s enable %s" % (exe, service)
+        elif "disable" == command:
+            cmd = "%s disable %s" % (exe, service)
+        elif "start" == command:
+            cmd = "%s start %s" % (exe, service)
+        elif "stop" == command:
+            cmd = "%s stop %s" % (exe, service)
+        elif "status" == command:
+            cmd = "%s status %s" % (exe, service)
+        command = CommandRunner(cmd)
+        command.run()
+        return command
+
+    def _initialize_service(self, handler, service, properties):
+        if "enabled" in properties:
+            enable = to_boolean(properties["enabled"])
+            if enable:
+                logging.info("Enabling service %s" % service)
+                handler(self, service, "enable")
+            else:
+                logging.info("Disabling service %s" % service)
+                handler(self, service, "disable")
+
+        if "ensureRunning" in properties:
+            ensure_running = to_boolean(properties["ensureRunning"])
+            command = handler(self, service, "status")
+            running = command.status == 0
+            if ensure_running and not running:
+                logging.info("Starting service %s" % service)
+                handler(self, service, "start")
+            elif not ensure_running and running:
+                logging.info("Stopping service %s" % service)
+                handler(self, service, "stop")
+
+
+    def _handle_services(self, handler, services):
+        for service, properties in services.iteritems():
+            self._handle_service(handler, service, properties)
+
+    def _initialize_services(self, handler, services):
+        for service, properties in services.iteritems():
+            self._initialize_service(handler, service, properties)
+
+    # map of function pointers to various service handlers
+    _service_handlers = {
+        "sysvinit": _handle_sysv_command,
+        "systemd": _handle_systemd_command
+    }
+
+
+    def _service_handler(self, manager_name):
+        handler = None
+        if manager_name in self._service_handlers:
+            handler = self._service_handlers[manager_name]
+        return handler
+
+
+    def apply_services(self):
+        """
+        Starts, stops, enables, disables services
+        """
+        for manager, service_entries in self._services.iteritems():
+            handler = self._service_handler(manager)
+            if not handler:
+                logging.warn("Skipping invalid service type: %s" % manager)
+            else:
+                self._handle_services(handler, service_entries)
+
+
+class Metadata(object):
+    _metadata = None
+    _init_key = "AWS::CloudFormation::Init"
+
+    def __init__(self, stack, resource, access_key=None,
+                 secret_key=None, credentials_file=None, region=None):
+
+        self.stack = stack
+        self.resource = resource
+        self.access_key = access_key
+        self.secret_key = secret_key
+        self.credentials_file = credentials_file
+        self.region = region
+
+        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()
+        f.close()
+
+    def _is_valid_metadata(self):
+        """
+        Should find the AWS::CloudFormation::Init json key
+        """
+        is_valid = self._metadata and \
+                   self._init_key in self._metadata and \
+                   self._metadata[self._init_key]
+        if is_valid:
+            self._metadata = self._metadata[self._init_key]
+        return is_valid
+
+    def _process_config(self):
+        """
+        Parse and process a config section
+          * packages
+          * sources (not yet)
+          * users (not yet)
+          * groups (not yet)
+          * files (not yet)
+          * commands (not yet)
+          * services
+        """
+
+        self._config = self._metadata["config"]
+        PackagesHandler(self._config.get("packages")).apply_packages()
+        #FIXME: handle sources
+        #FIXME: handle users
+        #FIXME: handle groups
+        #FIXME: handle files
+        #FIXME: handle commands
+        ServicesHandler(self._config.get("services")).apply_services()
+
+    def cfn_init(self):
+        """
+        Process the resource metadata
+        """
+        # FIXME: when config sets are implemented, this should select the correct
+        # config set from the metadata, and send each config in the config set to
+        # process_config
+        if not self._is_valid_metadata():
+            raise Exception("invalid metadata")
+        else:
+            self._process_config()
index 0bcc971792fc9097df9401999462fbaa0037a5b5..6f556101e62db50dc6d6bc7ad8e08d24145ff07c 100644 (file)
@@ -21,5 +21,6 @@ EOF
     <file name='/opt/aws/bin/cfn-init'  type='base64'></file>
     <file name='/opt/aws/bin/cfn-hup'  type='base64'></file>
     <file name='/opt/aws/bin/cfn-signal'  type='base64'></file>
+    <file name='/opt/aws/bin/cfn_helper.py'  type='base64'></file>
   </files>
 </template>
index ab12ae0be3b23ef0a4dec951dad501ed36c951d6..fb6f9fafad4e13199c32a2b5162f27069ab220c2 100644 (file)
@@ -21,5 +21,6 @@ EOF
     <file name='/opt/aws/bin/cfn-init'  type='base64'></file>
     <file name='/opt/aws/bin/cfn-hup'  type='base64'></file>
     <file name='/opt/aws/bin/cfn-signal'  type='base64'></file>
+    <file name='/opt/aws/bin/cfn_helper.py'  type='base64'></file>
   </files>
 </template>
index 51c6d7fdaa3ceee55cd686832d497c775fffddbc..69a154ed64b846b269f32081935bda74e893256b 100644 (file)
@@ -21,5 +21,6 @@ EOF
     <file name='/opt/aws/bin/cfn-init'  type='base64'></file>
     <file name='/opt/aws/bin/cfn-hup'  type='base64'></file>
     <file name='/opt/aws/bin/cfn-signal'  type='base64'></file>
+    <file name='/opt/aws/bin/cfn_helper.py'  type='base64'></file>
   </files>
 </template>
index 6c622778831f4715505ab53170047c3c023d8cc7..dae5637aafb770a3cc38d32b69818b2e9a17d0e9 100644 (file)
@@ -21,6 +21,7 @@ EOF
     <file name='/opt/aws/bin/cfn-init'  type='base64'></file>
     <file name='/opt/aws/bin/cfn-hup'  type='base64'></file>
     <file name='/opt/aws/bin/cfn-signal'  type='base64'></file>
+    <file name='/opt/aws/bin/cfn_helper.py'  type='base64'></file>
   </files>
 
 </template>