Merge branch 'ha'
Conflicts: heat/db/sqlalchemy/session.py Signed-off-by: Angus Salkeld <asalkeld@redhat.com>
This commit is contained in:
2
bin/heat
2
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()
|
||||
|
||||
0
heat/cfntools/__init__.py
Normal file
0
heat/cfntools/__init__.py
Normal file
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
739
heat/cfntools/cfn_helper.py
Normal file
739
heat/cfntools/cfn_helper.py
Normal file
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
@@ -21,5 +21,6 @@ EOF
|
||||
<file name='/opt/aws/bin/cfn-init' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn-hup' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn-signal' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn_helper.py' type='base64'></file>
|
||||
</files>
|
||||
</template>
|
||||
|
||||
@@ -21,5 +21,6 @@ EOF
|
||||
<file name='/opt/aws/bin/cfn-init' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn-hup' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn-signal' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn_helper.py' type='base64'></file>
|
||||
</files>
|
||||
</template>
|
||||
|
||||
@@ -21,5 +21,6 @@ EOF
|
||||
<file name='/opt/aws/bin/cfn-init' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn-hup' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn-signal' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn_helper.py' type='base64'></file>
|
||||
</files>
|
||||
</template>
|
||||
|
||||
@@ -21,6 +21,7 @@ EOF
|
||||
<file name='/opt/aws/bin/cfn-init' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn-hup' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn-signal' type='base64'></file>
|
||||
<file name='/opt/aws/bin/cfn_helper.py' type='base64'></file>
|
||||
</files>
|
||||
|
||||
</template>
|
||||
|
||||
166
heat/tests/test_cfn.py
Normal file
166
heat/tests/test_cfn.py
Normal file
@@ -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()
|
||||
233
templates/WordPress_Single_Instance_With_HA.template
Normal file
233
templates/WordPress_Single_Instance_With_HA.template
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user