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"
+ }
+ }
+}