diff --git a/bin/heat b/bin/heat index dc79e58799..2b5e01b25d 100755 --- 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 index 0000000000..e69de29bb2 diff --git a/heat/cfntools/cfn-hup b/heat/cfntools/cfn-hup index af2411500d..1291e67bde 100755 --- a/heat/cfntools/cfn-hup +++ b/heat/cfntools/cfn-hup @@ -15,3 +15,100 @@ """ 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 * + +description = " " +parser = argparse.ArgumentParser(description=description) +parser.add_argument('-c', '--config', + dest="config_dir", + help="Hook Config Directory", + required=False, + default='/etc/cfn/hooks.d') +parser.add_argument('-f', '--no-daemon', + dest="no_deamon", + action="store_true", + help="Do not run as a deamon", + required=False) +parser.add_argument('-v', '--verbose', + action="store_true", + dest="verbose", + help="Verbose logging", + required=False) +args = parser.parse_args() +# FIXME: implement real arg + +logger = logging.getLogger('cfn-hup') +log_file_name = "/var/log/cfn-hup.log" +log_format = '%(levelname)s [%(asctime)s] %(message)s' +file_handler = logging.FileHandler(log_file_name) +file_handler.setFormatter(logging.Formatter(log_format)) +logging.getLogger().addHandler(file_handler) + +if args.verbose: + logging.basicConfig(format=log_format, level=logging.DEBUG) +else: + logging.basicConfig(format=log_format, level=logging.INFO) + +main_conf_path = '/etc/cfn/cfn-hup.conf' +try: + main_config_file = open(main_conf_path) +except IOError as exc: + logger.error('Could not open main configuration at %s' % main_conf_path) + exit(1) + +config_files = [] +hooks_conf_path = '/etc/cfn/hooks.conf' +if os.path.exists(hooks_conf_path): + try: + config_files.append(open(hooks_conf_path)) + except IOError as exc: + logger.exception(exc) + +if args.config_dir and os.path.exists(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: + logger.exception(exc) + +if not config_files: + logger.error('No hook files found at %s or %s' % (hooks_conf_path, + args.config_dir)) + exit(1) + +try: + mainconfig = HupConfig([main_config_file] + config_files) +except Exception as ex: + logger.error('Cannot load configuration: %s' % str(ex)) + exit(1) + +if not mainconfig.unique_resources_get(): + logger.error('No hooks were found. Add some to %s or %s' % (hooks_conf_path, + args.config_dir)) + exit(1) + +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: + logger.exception("Error processing metadata") + exit(1) diff --git a/heat/cfntools/cfn-init b/heat/cfntools/cfn-init index 4ea48f7ff2..6eb9715b00 100755 --- a/heat/cfntools/cfn-init +++ b/heat/cfntools/cfn-init @@ -31,543 +31,24 @@ 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 +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 * + +logger = logging.getLogger('cfn-init') log_file_name = "/var/log/cfn-init.log" 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" +logging.basicConfig(format=log_format, level=logging.DEBUG) description = " " parser = argparse.ArgumentParser(description=description) @@ -594,9 +75,14 @@ parser.add_argument('--region', args = parser.parse_args() # FIXME: implement real arg -metadata = Metadata(get_metadata(metadata_file)) +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.process() + metadata.cfn_init() except Exception as e: - logging.exception("Error processing metadata") + logger.exception("Error processing metadata") exit(1) diff --git a/heat/cfntools/cfn_helper.py b/heat/cfntools/cfn_helper.py new file mode 100644 index 0000000000..3b6e491a45 --- /dev/null +++ b/heat/cfntools/cfn_helper.py @@ -0,0 +1,739 @@ +# +# 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 ConfigParser +import errno +import grp +import json +import logging +import os +import os.path +import pwd +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 val 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.load_main_section() + + self.hooks = {} + for s in self.config.sections(): + if s != 'main': + 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 load_main_section(self): + # required values + self.stack = self.config.get('main', 'stack') + self.credential_file = self.config.get('main', 'credential-file') + try: + with open(self.credential_file) as f: + self.credentials = f.read() + except: + raise Exception("invalid credentials file %s" % + self.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 and \ + ev_name in self.triggers: + CommandRunner(self.action).run(user=self.runas) + else: + logging.debug('event: {%s, %s, %s} did not match %s' % \ + (ev_name, ev_object, ev_resource, self.__str__())) + + 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. + """ + + 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, user='root'): + """ + 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 + """ + if not self._packages: + return + 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 FilesHandler(object): + def __init__(self, files): + self._files = files + + def apply_files(self): + if not self._files: + return + for fdest, meta in self._files.iteritems(): + dest = fdest.encode() + try: + os.makedirs(os.path.dirname(dest)) + except OSError as e: + if e.errno == errno.EEXIST: + logging.debug(str(e)) + else: + logging.exception(e) + + if 'content' in meta: + if isinstance(meta['content'], basestring): + f = open(dest, 'w') + f.write(meta['content']) + f.close() + else: + f = open(dest, 'w') + f.write(json.dumps(meta['content'], indent=4)) + f.close() + elif 'source' in meta: + CommandRunner('wget -O %s %s' % (dest, meta['source'])).run() + else: + logging.error('%s %s' % (dest, str(meta))) + continue + + uid = -1 + gid = -1 + if 'owner' in meta: + try: + user_info = pwd.getpwnam(meta['owner']) + uid = user_info[2] + except KeyError as ex: + pass + + if 'group' in meta: + try: + group_info = grp.getgrnam(meta['group']) + gid = group_info[2] + except KeyError as ex: + pass + + os.chown(dest, uid, gid) + if 'mode' in meta: + os.chmod(dest, int(meta['mode'], 8)) + + +class ServicesHandler(object): + _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" + 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 _monitor_service(self, handler, service, properties): + 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("Restarting service %s" % service) + start_cmd = handler(self, service, "start") + if start_cmd.status != 0: + logging.warning('Service %s did not start. STDERR: %s' % + (service, start_cmd.stderr)) + return + for h in self.hooks: + self.hooks[h].event('service.restarted', + service, self.resource) + + def _monitor_services(self, handler, services): + for service, properties in services.iteritems(): + self._monitor_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 + """ + if not self._services: + return + 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._initialize_services(handler, service_entries) + + def monitor_services(self): + """ + Restarts failed services, and runs hooks. + """ + if not self._services: + return + 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._monitor_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 + + # TODO(asalkeld) is this metadata for the local resource? + self._is_local_metadata = True + self._metadata = None + + def retrieve(self, meta_str=None): + """ + Read the metadata from the given filename + """ + if meta_str: + self._data = meta_str + else: + f = open("/var/lib/cloud/data/cfn-init-data") + 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): + """ + 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 + * 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 + FilesHandler(self._config.get("files")).apply_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() + + 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.monitor_services() diff --git a/heat/engine/parser.py b/heat/engine/parser.py index a4a698e77f..5c1c6020b1 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -43,6 +43,10 @@ class Stack(object): self.name = stack_name self.parsed_template_id = 0 + self.parms['AWS::StackName'] = {"Description": "AWS StackName", + "Type": "String", + "Value": stack_name} + self.parms['AWS::Region'] = {"Description": "AWS Regions", "Type": "String", "Default": "ap-southeast-1", diff --git a/heat/jeos/F16-i386-cfntools-jeos.tdl b/heat/jeos/F16-i386-cfntools-jeos.tdl index 0bcc971792..6f556101e6 100644 --- a/heat/jeos/F16-i386-cfntools-jeos.tdl +++ b/heat/jeos/F16-i386-cfntools-jeos.tdl @@ -21,5 +21,6 @@ EOF + diff --git a/heat/jeos/F16-x86_64-cfntools-jeos.tdl b/heat/jeos/F16-x86_64-cfntools-jeos.tdl index ab12ae0be3..fb6f9fafad 100644 --- a/heat/jeos/F16-x86_64-cfntools-jeos.tdl +++ b/heat/jeos/F16-x86_64-cfntools-jeos.tdl @@ -21,5 +21,6 @@ EOF + diff --git a/heat/jeos/F17-i386-cfntools-jeos.tdl b/heat/jeos/F17-i386-cfntools-jeos.tdl index 51c6d7fdaa..69a154ed64 100644 --- a/heat/jeos/F17-i386-cfntools-jeos.tdl +++ b/heat/jeos/F17-i386-cfntools-jeos.tdl @@ -21,5 +21,6 @@ EOF + diff --git a/heat/jeos/F17-x86_64-cfntools-jeos.tdl b/heat/jeos/F17-x86_64-cfntools-jeos.tdl index 6c62277883..dae5637aaf 100644 --- a/heat/jeos/F17-x86_64-cfntools-jeos.tdl +++ b/heat/jeos/F17-x86_64-cfntools-jeos.tdl @@ -21,6 +21,7 @@ EOF + diff --git a/heat/tests/test_cfn.py b/heat/tests/test_cfn.py new file mode 100644 index 0000000000..8f6d523722 --- /dev/null +++ b/heat/tests/test_cfn.py @@ -0,0 +1,166 @@ +### +### 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 +import shutil + +from heat.cfntools.cfn_helper import * + + +@attr(tag=['unit', 'cfn_helper']) +@attr(speed='fast') +def test_boolean(): + + assert(to_boolean('true')) + assert(to_boolean(True)) + assert(to_boolean('TRUE')) + assert(to_boolean('True')) + assert(to_boolean('Yes')) + assert(to_boolean('YES')) + assert(to_boolean('yes')) + assert(to_boolean('1')) + assert(to_boolean(1)) + + assert(not to_boolean('tru')) + assert(not to_boolean(False)) + assert(not to_boolean('False')) + assert(not to_boolean('FALSE')) + assert(not to_boolean('No')) + assert(not to_boolean('NO')) + assert(not to_boolean('no')) + assert(not to_boolean('0334')) + assert(not to_boolean(0)) + assert(not to_boolean(56)) + + +def setUp_credential_file(): + f = open('/tmp/incredible', 'w') + f.write('junk, just junk') + f.close() + +def tearDown_credential_file(): + shutil.rmtree('/tmp/incredible', ignore_errors=True) + +@with_setup(setUp_credential_file, tearDown_credential_file) +@attr(tag=['unit', 'cfn-hup']) +@attr(speed='fast') +def test_hup_conf1(): + good= """ +[main] +stack=stack-test +credential-file=/tmp/incredible +region=unit-test-a +interval=3 +""" + c = HupConfig([io.BytesIO(good)]) + assert(c.stack == 'stack-test') + assert(c.credential_file == '/tmp/incredible') + assert(c.region == 'unit-test-a') + assert(c.interval == 3) + + +@with_setup(setUp_credential_file, tearDown_credential_file) +@attr(tag=['unit', 'cfn-hup']) +@attr(speed='fast') +def test_hup_default(): + good= """ +[main] +stack=stack-testr +credential-file=/tmp/incredible +""" + c = HupConfig([io.BytesIO(good)]) + assert(c.stack == 'stack-testr') + assert(c.credential_file == '/tmp/incredible') + assert(c.region == 'nova') + assert(c.interval == 10) + + +@with_setup(setUp_credential_file, tearDown_credential_file) +@attr(tag=['unit', 'cfn-hup']) +@attr(speed='fast') +def test_hup_hook(): + good= """ +[main] +stack=stackname_is_fred +credential-file=/tmp/incredible + +[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 == '/tmp/incredible') + 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') + + +def tearDown_metadata_files(): + shutil.rmtree('/tmp/_files_test_', ignore_errors=True) + + +@with_setup(None, tearDown_metadata_files) +@attr(tag=['unit', 'cfn-metadata']) +@attr(speed='fast') +def test_metadata_files(): + + j = ''' { + "AWS::CloudFormation::Init" : { + "config" : { + "files" : { + "/tmp/_files_test_/epel.repo" : { + "source" : "https://raw.github.com/heat-api/heat/master/README.rst", + "mode" : "000644" + }, + "/tmp/_files_test_/_with/some/dirs/to/make/small.conf" : { + "content" : "not much really", + "mode" : "000777" + }, + "/tmp/_files_test_/node.json": { + "content": { + "myapp": { + "db": { + "database": "RefDBName", + "user": "RefDBUser", + "host": "Fn::GetAttDBInstance.Endpoint.Address", + "password": "RefDBPassword" + } + }, + "run_list": ["recipe[wordpress]", "bla"] + }, + "mode": "000600" + } + } + } + } + } +''' + + metadata = Metadata('tester', + 'ronald') + metadata.retrieve(j) + metadata.cfn_init() + + # mask out the file type + mask = int('007777', 8) + assert(os.stat('/tmp/_files_test_/node.json').st_mode & mask == 0600) + assert(os.stat('/tmp/_files_test_/epel.repo').st_mode & mask == 0644) + assert(os.stat('/tmp/_files_test_/_with/some/dirs/to/make/small.conf').st_mode & mask == 0777) + +if __name__ == '__main__': + sys.argv.append(__file__) + nose.main() diff --git a/templates/WordPress_Single_Instance_With_HA.template b/templates/WordPress_Single_Instance_With_HA.template new file mode 100644 index 0000000000..f0feeac7f1 --- /dev/null +++ b/templates/WordPress_Single_Instance_With_HA.template @@ -0,0 +1,233 @@ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + + "Description" : "AWS CloudFormation Sample Template WordPress_Multi_Instance: WordPress is web software you can use to create a beautiful website or blog. This template installs two instances: one running a WordPress deployment and the other using a local MySQL database to store the data.", + + "Parameters" : { + + "KeyName" : { + "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances", + "Type" : "String" + }, + + "InstanceType" : { + "Description" : "WebServer EC2 instance type", + "Type" : "String", + "Default" : "m1.large", + "AllowedValues" : [ "t1.micro", "m1.small", "m1.large", "m1.xlarge", "m2.xlarge", "m2.2xlarge", "m2.4xlarge", "c1.medium", "c1.xlarge", "cc1.4xlarge" ], + "ConstraintDescription" : "must be a valid EC2 instance type." + }, + + "DBName": { + "Default": "wordpress", + "Description" : "The WordPress database name", + "Type": "String", + "MinLength": "1", + "MaxLength": "64", + "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*", + "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters." + }, + + "DBUsername": { + "Default": "admin", + "NoEcho": "true", + "Description" : "The WordPress database admin account username", + "Type": "String", + "MinLength": "1", + "MaxLength": "16", + "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*", + "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters." + }, + + "DBPassword": { + "Default": "admin", + "NoEcho": "true", + "Description" : "The WordPress database admin account password", + "Type": "String", + "MinLength": "1", + "MaxLength": "41", + "AllowedPattern" : "[a-zA-Z0-9]*", + "ConstraintDescription" : "must contain only alphanumeric characters." + }, + + "DBRootPassword": { + "Default": "admin", + "NoEcho": "true", + "Description" : "Root password for MySQL", + "Type": "String", + "MinLength": "1", + "MaxLength": "41", + "AllowedPattern" : "[a-zA-Z0-9]*", + "ConstraintDescription" : "must contain only alphanumeric characters." + }, + "LinuxDistribution": { + "Default": "F16", + "Description" : "Distribution of choice", + "Type": "String", + "AllowedValues" : [ "F16", "F17", "U10", "RHEL-6.1", "RHEL-6.2", "RHEL-6.3" ] + }, + "HupPollInterval": { + "Default": "1", + "Description" : "Interval for cfn-hup", + "Type": "String" + } + }, + + "Mappings" : { + "AWSInstanceType2Arch" : { + "t1.micro" : { "Arch" : "32" }, + "m1.small" : { "Arch" : "32" }, + "m1.large" : { "Arch" : "64" }, + "m1.xlarge" : { "Arch" : "64" }, + "m2.xlarge" : { "Arch" : "64" }, + "m2.2xlarge" : { "Arch" : "64" }, + "m2.4xlarge" : { "Arch" : "64" }, + "c1.medium" : { "Arch" : "32" }, + "c1.xlarge" : { "Arch" : "64" }, + "cc1.4xlarge" : { "Arch" : "64" } + }, + "DistroArch2AMI": { + "F16" : { "32" : "F16-i386-cfntools", "64" : "F16-x86_64-cfntools" }, + "F17" : { "32" : "F17-i386-cfntools", "64" : "F17-x86_64-cfntools" }, + "U10" : { "32" : "U10-i386-cfntools", "64" : "U10-x86_64-cfntools" }, + "RHEL-6.1" : { "32" : "rhel61-i386-cfntools", "64" : "rhel61-x86_64-cfntools" }, + "RHEL-6.2" : { "32" : "rhel62-i386-cfntools", "64" : "rhel62-x86_64-cfntools" }, + "RHEL-6.3" : { "32" : "rhel63-i386-cfntools", "64" : "rhel63-x86_64-cfntools" } + } + }, + + "Resources" : { + "WikiDatabase": { + "Type": "AWS::EC2::Instance", + "Metadata" : { + "AWS::CloudFormation::Init" : { + "config" : { + "files" : { + "/opt/aws/bin/cfn-init" : { + "source" : "https://raw.github.com/heat-api/heat/ha/heat/cfntools/cfn-init", + "mode" : "000755", + "owner" : "root", + "group" : "root" + }, + "/opt/aws/bin/cfn-hup" : { + "source" : "https://raw.github.com/heat-api/heat/ha/heat/cfntools/cfn-hup", + "mode" : "000755", + "owner" : "root", + "group" : "root" + }, + "/opt/aws/bin/cfn_helper.py" : { + "source" : "https://raw.github.com/heat-api/heat/ha/heat/cfntools/cfn_helper.py", + "mode" : "000644", + "owner" : "root", + "group" : "root" + }, + + "/etc/cfn/cfn-credentials" : { + "content" : { "Fn::Join" : ["", [ + "AWSAccessKeyId=GobbleGobble\n", + "AWSSecretKey=Fn_GetAtt_WebServerKeys_SecretAccessKey\n" + ]]}, + "mode" : "000400", + "owner" : "root", + "group" : "root" + }, + + "/etc/cfn/cfn-hup.conf" : { + "content" : { "Fn::Join" : ["", [ + "[main]\n", + "stack=", { "Ref" : "AWS::StackName" }, "\n", + "credential-file=/etc/cfn/cfn-credentials\n", + "region=", { "Ref" : "AWS::Region" }, "\n", + "interval=", { "Ref" : "HupPollInterval" }, "\n" + ]]}, + "mode" : "000400", + "owner" : "root", + "group" : "root" + }, + + "/etc/cfn/notify-on-httpd-restarted" : { + "content" : { "Fn::Join" : ["", [ + "#!/bin/sh\n", + "logger -t cfn-event 'http got restarted'\n" + ]]}, + "mode" : "000700", + "owner" : "root", + "group" : "root" + }, + + "/tmp/cfn-hup-crontab.txt" : { + "content" : { "Fn::Join" : ["", [ + "MAIL=\"\"\n", + "\n", + "* * * * * /opt/aws/bin/cfn-hup -f\n" + ]]}, + "mode" : "000600", + "owner" : "root", + "group" : "root" + }, + + "/etc/cfn/hooks.conf" : { + "content": { "Fn::Join" : ["", [ + "[cfn-http-restarted]\n", + "triggers=service.restarted\n", + "path=Resources.WikiDatabase.Metadata\n", + "action=/etc/cfn/notify-on-httpd-restarted\n", + "runas=root\n" + ]]}, + "mode" : "000400", + "owner" : "root", + "group" : "root" + } + }, + "packages" : { + "yum" : { + "cronie" : [], + "mysql" : [], + "mysql-server" : [], + "httpd" : [], + "wordpress" : [] + } + }, + "services" : { + "systemd" : { + "mysqld" : { "enabled" : "true", "ensureRunning" : "true" }, + "httpd" : { "enabled" : "true", "ensureRunning" : "true" }, + "crond" : { "enabled" : "true", "ensureRunning" : "true" } + } + } + } + } + }, + "Properties": { + "ImageId" : { "Fn::FindInMap" : [ "DistroArch2AMI", { "Ref" : "LinuxDistribution" }, + { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] }, + "InstanceType" : { "Ref" : "InstanceType" }, + "KeyName" : { "Ref" : "KeyName" }, + "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ + "#!/bin/bash -v\n", + "/opt/aws/bin/cfn-init\n", + "# Setup MySQL root password and create a user\n", + "mysqladmin -u root password '", { "Ref" : "DBRootPassword" }, "'\n", + "cat << EOF | mysql -u root --password='", { "Ref" : "DBRootPassword" }, "'\n", + "CREATE DATABASE ", { "Ref" : "DBName" }, ";\n", + "GRANT ALL PRIVILEGES ON ", { "Ref" : "DBName" }, ".* TO \"", { "Ref" : "DBUsername" }, "\"@\"localhost\"\n", + "IDENTIFIED BY \"", { "Ref" : "DBPassword" }, "\";\n", + "FLUSH PRIVILEGES;\n", + "EXIT\n", + "EOF\n", + "sed --in-place --e s/database_name_here/", { "Ref" : "DBName" }, "/ --e s/username_here/", { "Ref" : "DBUsername" }, "/ --e s/password_here/", { "Ref" : "DBPassword" }, "/ /usr/share/wordpress/wp-config.php\n", + + "# install cfn-hup crontab\n", + "crontab /tmp/cfn-hup-crontab.txt\n" + ]]}} + } + } + }, + + "Outputs" : { + "WebsiteURL" : { + "Value" : { "Fn::Join" : ["", ["http://", { "Fn::GetAtt" : [ "WikiDatabase", "PublicIp" ]}, "/wordpress"]] }, + "Description" : "URL for Wordpress wiki" + } + } +}