Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial import

  • Loading branch information...
commit 3e88215d34a30da7e6b7de17b56df939930de4d9 0 parents
@lachie lachie authored
Showing with 5,417 additions and 0 deletions.
  1. +1 −0  .gitignore
  2. +4 −0 MANIFEST.in
  3. +11 −0 PKG-INFO
  4. +97 −0 bin/cfn-elect-cmd-leader
  5. +106 −0 bin/cfn-get-metadata
  6. +152 −0 bin/cfn-hup
  7. +142 −0 bin/cfn-init
  8. +94 −0 bin/cfn-send-cmd-result
  9. +90 −0 bin/cfn-signal
  10. +84 −0 cfnbootstrap/__init__.py
  11. +124 −0 cfnbootstrap/apt_tool.py
  12. +154 −0 cfnbootstrap/auth.py
  13. +297 −0 cfnbootstrap/aws_client.py
  14. +238 −0 cfnbootstrap/cfn_client.py
  15. +126 −0 cfnbootstrap/command_tool.py
  16. +524 −0 cfnbootstrap/construction.py
  17. +71 −0 cfnbootstrap/construction_errors.py
  18. +212 −0 cfnbootstrap/file_tool.py
  19. +159 −0 cfnbootstrap/lang_package_tools.py
  20. +135 −0 cfnbootstrap/msi_tool.py
  21. +92 −0 cfnbootstrap/platform_utils.py
  22. +161 −0 cfnbootstrap/posix_security.py
  23. +306 −0 cfnbootstrap/rpm_tools.py
  24. +29 −0 cfnbootstrap/security.py
  25. +327 −0 cfnbootstrap/service_tools.py
  26. +191 −0 cfnbootstrap/sources_tool.py
  27. +183 −0 cfnbootstrap/sqs_client.py
  28. +550 −0 cfnbootstrap/update_hooks.py
  29. +86 −0 cfnbootstrap/user_group_tools.py
  30. +250 −0 cfnbootstrap/util.py
  31. +99 −0 cfnbootstrap/winhup.py
  32. +61 −0 init/cfn-hup
  33. +174 −0 license/LICENSE.txt
  34. +4 −0 license/NOTICE.txt
  35. +2 −0  setup.cfg
  36. +81 −0 setup.py
1  .gitignore
@@ -0,0 +1 @@
+.DS_Store
4 MANIFEST.in
@@ -0,0 +1,4 @@
+include license/LICENSE.txt
+include license/NOTICE.txt
+include MANIFEST.in
+include init/cfn-hup
11 PKG-INFO
@@ -0,0 +1,11 @@
+Metadata-Version: 1.1
+Name: aws-cfn-bootstrap
+Version: 1.3
+Summary: An EC2 bootstrapper for CloudFormation
+Home-page: http://aws.amazon.com/cloudformation/
+Author: AWS CloudFormation
+Author-email: UNKNOWN
+License: Apache 2.0
+Description: Bootstraps EC2 instances by retrieving and processing the Metadata block of a CloudFormation resource.
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: Apache Software License
97 bin/cfn-elect-cmd-leader
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+from cfnbootstrap import util
+from cfnbootstrap.aws_client import Credentials
+from cfnbootstrap.cfn_client import CloudFormationClient
+from cfnbootstrap.construction import Contractor
+from optparse import OptionGroup, OptionParser
+import cfnbootstrap
+import logging
+import os
+import sys
+
+parser = OptionParser()
+creds_group = OptionGroup(parser, "AWS Credentials", "Options for specifying AWS Account Credentials. The credential file supercedes any other specification of credentials.")
+creds_group.add_option("-f", "--credential-file", help="A credential file, readable only by the owner, with keys 'AWSAccessKeyId' and 'AWSSecretKey'",
+ type="string", dest="credential_file")
+creds_group.add_option("", "--access-key", help="An AWS Access Key",
+ type="string", dest="access_key")
+creds_group.add_option("", "--secret-key", help="An AWS Secret Key",
+ type="string", dest="secret_key")
+
+parser.add_option_group(creds_group)
+parser.add_option("-s", "--stack", help="A CloudFormation stack",
+ type="string", dest="stack_name", default=os.environ.get('STACK_NAME'))
+parser.add_option("-c", "--command-name", help="The command name",
+ type="string", dest="command_name", default=os.environ.get('CMD_NAME'))
+parser.add_option("-i", "--invocation-id", help="The invocation ID",
+ type="string", dest="invocation_id", default=os.environ.get('INVOCATION_ID'))
+parser.add_option("-l", "--listener-id", help="The listener ID",
+ type="string", dest="listener_id", default=os.environ.get('LISTENER_ID'))
+
+parser.add_option("-u", "--url", help="The CloudFormation service URL. The endpoint URL must match the region option. Use of this parameter is discouraged.",
+ type="string", dest="endpoint")
+parser.add_option("", "--region", help="The CloudFormation region. Default: us-east-1.",
+ type="string", dest="region", default="us-east-1")
+
+parser.add_option("-v", "--verbose", help="Enables verbose logging",
+ action="store_true", dest="verbose")
+
+(options, args) = parser.parse_args()
+
+def fail_on_unspecified(value, name):
+ if not value:
+ print >> sys.stderr, "Error: You must specify %s" % name
+ parser.print_help(sys.stderr)
+ sys.exit(1)
+
+fail_on_unspecified(options.stack_name, "StackName")
+fail_on_unspecified(options.command_name, "CommandName")
+fail_on_unspecified(options.invocation_id, "InvocationId")
+fail_on_unspecified(options.listener_id, "ListenerId")
+
+cfnbootstrap.configureLogging("DEBUG" if options.verbose else "INFO")
+
+if options.credential_file:
+ try:
+ access_key, secret_key = util.extract_credentials(options.credential_file)
+ except IOError, e:
+ print >> sys.stderr, "Error retrieving credentials from file:\n\t%s" % e.strerror
+ sys.exit(1)
+else:
+ access_key = options.access_key
+ secret_key = options.secret_key
+
+url = CloudFormationClient.endpointForRegion(options.region)
+if options.endpoint:
+ url = options.endpoint
+
+client = CloudFormationClient(Credentials(access_key, secret_key), url=url, region=options.region)
+
+try:
+ leader = client.elect_command_leader(options.stack_name,
+ options.command_name,
+ options.invocation_id,
+ options.listener_id)
+ sys.exit(0 if leader == options.listener_id else 5)
+except IOError, e:
+ if e.strerror:
+ print >> sys.stderr, e.strerror
+ else:
+ print >> sys.stderr, "Unknown error electing command leader"
+ sys.exit(1)
106 bin/cfn-get-metadata
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+import cfnbootstrap
+from cfnbootstrap.aws_client import Credentials
+from cfnbootstrap.cfn_client import CloudFormationClient
+from optparse import OptionParser
+from optparse import OptionGroup
+from cfnbootstrap.construction import Contractor
+from cfnbootstrap import util
+import sys
+import logging
+import os
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+parser = OptionParser()
+creds_group = OptionGroup(parser, "AWS Credentials", "Options for specifying AWS Account Credentials. The credential file supercedes any other specification of credentials.")
+creds_group.add_option("-f", "--credential-file", help="A credential file, readable only by the owner, with keys 'AWSAccessKeyId' and 'AWSSecretKey'",
+ type="string", dest="credential_file")
+creds_group.add_option("", "--access-key", help="An AWS Access Key",
+ type="string", dest="access_key")
+creds_group.add_option("", "--secret-key", help="An AWS Secret Key",
+ type="string", dest="secret_key")
+
+parser.add_option_group(creds_group)
+parser.add_option("-s", "--stack", help="A CloudFormation stack",
+ type="string", dest="stack_name")
+parser.add_option("-r", "--resource", help="A CloudFormation logical resource ID",
+ type="string", dest="logical_resource_id")
+parser.add_option("-k", "--key", help="Retrieve the value at <key> in the Metadata object; must be in dotted object notation (parent.child.leaf)",
+ type="string", dest="key")
+
+parser.add_option("-u", "--url", help="The CloudFormation service URL. The endpoint URL must match the region option. Use of this parameter is discouraged.",
+ type="string", dest="endpoint")
+parser.add_option("", "--region", help="The CloudFormation region. Default: us-east-1.",
+ type="string", dest="region", default="us-east-1")
+
+parser.add_option("-v", "--verbose", help="Enables verbose logging",
+ action="store_true", dest="verbose")
+
+(options, args) = parser.parse_args()
+
+if not options.stack_name or not options.logical_resource_id:
+ print >> sys.stderr, "Error: You must specify both a stack name and logical resource id"
+ parser.print_help(sys.stderr)
+ sys.exit(1)
+
+cfnbootstrap.configureLogging("DEBUG" if options.verbose else "INFO")
+
+if options.credential_file:
+ try:
+ access_key, secret_key = util.extract_credentials(options.credential_file)
+ except IOError, e:
+ print >> sys.stderr, "Error retrieving credentials from file:\n\t%s" % e.strerror
+ sys.exit(1)
+else:
+ access_key = options.access_key
+ secret_key = options.secret_key
+
+url = CloudFormationClient.endpointForRegion(options.region)
+if options.endpoint:
+ url = options.endpoint
+
+try:
+ detail = CloudFormationClient(Credentials(access_key, secret_key), url=url, region=options.region).describe_stack_resource(options.logical_resource_id, options.stack_name)
+except IOError, e:
+ if e.strerror:
+ print >> sys.stderr, e.strerror
+ else:
+ print >> sys.stderr, "Unknown error retrieving %s" % options.logical_resource_id
+ sys.exit(1)
+
+if not detail.metadata:
+ print >> sys.stderr, "Error: %s does not specify any metadata" % detail.logicalResourceId
+ sys.exit(1)
+
+metadata_to_dump = detail.metadata
+
+if options.key:
+ metadata_to_dump = util.extract_value(metadata_to_dump, options.key)
+ if not metadata_to_dump:
+ print >> sys.stderr, "Error: %s is not present in the metadata for %s" % (options.key, detail.logicalResourceId)
+ sys.exit(1)
+
+if isinstance(metadata_to_dump, basestring):
+ print >> sys.stdout, metadata_to_dump
+else:
+ json.dump(metadata_to_dump, sys.stdout, indent=4)
+ print >> sys.stdout, "" # This removes the annoying no-EOL symbols from some consoles, as json.dump does not end with a newline
152 bin/cfn-hup
@@ -0,0 +1,152 @@
+#!/usr/bin/env python
+
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+import cfnbootstrap
+from cfnbootstrap import update_hooks, util
+from optparse import OptionParser
+import logging
+import os
+import threading
+import datetime
+import sys
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+if os.name == 'nt':
+ default_confdir = os.path.expandvars('${SystemDrive}\cfn')
+else:
+ default_confdir = '/etc/cfn'
+
+parser = OptionParser()
+parser.add_option("-c", "--config", help="The configuration directory (default: %s)" % default_confdir,
+ type="string", dest="config_path", default=default_confdir)
+parser.add_option("", "--no-daemon", help="Do not daemonize",
+ dest="no_daemon", action="store_true")
+
+parser.add_option("-v", "--verbose", help="Enables verbose logging",
+ action="store_true", dest="verbose")
+
+(options, args) = parser.parse_args()
+
+def main():
+ cfnbootstrap.configureLogging("DEBUG", filename='cfn-hup.log')
+
+ if not options.config_path:
+ logging.error("Error: A configuration path must be specified")
+ parser.print_help(sys.stderr)
+ sys.exit(1)
+
+ if not os.path.isdir(options.config_path):
+ logging.error("Error: Could not find configuration at %s", options.config_path)
+ sys.exit(1)
+
+ try:
+ main_config, processor, cmd_processor = update_hooks.parse_config(options.config_path)
+ except ValueError, e:
+ logging.error("Error: %s", str(e))
+ sys.exit(1)
+
+ verbose = options.verbose or main_config.has_option('main', 'verbose') and main_config.getboolean('main', 'verbose')
+ cfnbootstrap.configureLogging("DEBUG" if verbose else "INFO", filename='cfn-hup.log')
+
+ if options.no_daemon:
+ if processor:
+ processor.process()
+ if cmd_processor:
+ cmd_processor.register()
+ cmd_processor.process()
+ else:
+ fatal_event = threading.Event()
+
+ interval = 15
+ if main_config.has_option('main', 'interval'):
+ interval = main_config.getint('main', 'interval')
+ if interval < 1:
+ logging.error("Invalid interval (must be 1 minute or greater): %s", interval)
+ sys.exit(1)
+
+ def do_process(last_log=datetime.datetime.utcnow()):
+ if datetime.datetime.utcnow() - last_log > datetime.timedelta(minutes=5):
+ last_log = datetime.datetime.utcnow()
+ logging.info("cfn-hup processing is alive.")
+
+ try:
+ processor.process()
+ except update_hooks.FatalUpdateError, e:
+ logging.exception("Fatal exception")
+ fatal_event.set()
+ except Exception, e:
+ logging.exception("Unhandled exception")
+ threading.Timer(interval * 60, do_process, (), {'last_log' : last_log}).start()
+
+ def do_cmd_process(last_log=datetime.datetime.utcnow()):
+ if datetime.datetime.utcnow() - last_log > datetime.timedelta(minutes=5):
+ last_log = datetime.datetime.utcnow()
+ logging.info("command processing is alive.")
+
+ delay = 1
+ try:
+ if not cmd_processor.is_registered():
+ cmd_processor.register()
+ if cmd_processor.creds_expired():
+ logging.error("Expired credentials found; skipping process")
+ delay = 20
+ else:
+ cmd_processor.process()
+ except update_hooks.FatalUpdateError, e:
+ logging.exception("Fatal exception")
+ fatal_event.set()
+ except Exception, e:
+ logging.exception("Unhandled exception")
+ threading.Timer(delay, do_cmd_process, (), {'last_log' : last_log}).start()
+
+ if processor:
+ do_process()
+ if cmd_processor:
+ do_cmd_process()
+
+ while True:
+ # do this instead of wait() without timeout
+ # as for some reason interrupts will not happen unless you wait for a specified time
+ # (even if the wait is for a long time, the interrupt comes immediately)
+ fatal_event.wait(60)
+
+if options.no_daemon:
+ main()
+elif os.name == 'nt':
+ logging.error("Error: cfn-hup cannot be run directly in daemon mode on Windows")
+ sys.exit(1)
+else:
+ try:
+ import daemon
+ except ImportError:
+ print >> sys.stderr, "Daemon library was not installed; please install python-daemon"
+ sys.exit(1)
+
+ try:
+ from daemon import pidlockfile
+ except ImportError:
+ from daemon import pidfile as pidlockfile
+
+ with daemon.DaemonContext(pidfile=pidlockfile.TimeoutPIDLockFile('/var/run/cfn-hup.pid', 300)):
+ try:
+ main()
+ except Exception, e:
+ logging.exception("Unhandled exception: %s", str(e))
+ sys.exit(1)
142 bin/cfn-init
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+import sys
+import cfnbootstrap
+from cfnbootstrap.aws_client import Credentials
+from cfnbootstrap.cfn_client import CloudFormationClient
+from optparse import OptionParser
+from optparse import OptionGroup
+from cfnbootstrap.construction import Contractor, WorkLog
+from cfnbootstrap import util
+import logging
+import os
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+parser = OptionParser()
+creds_group = OptionGroup(parser, "AWS Credentials", "Options for specifying AWS Account Credentials. The credential file supercedes any other specification of credentials.")
+creds_group.add_option("-f", "--credential-file", help="A credential file, readable only by the owner, with keys 'AWSAccessKeyId' and 'AWSSecretKey'",
+ type="string", dest="credential_file")
+creds_group.add_option("", "--access-key", help="An AWS Access Key",
+ type="string", dest="access_key")
+creds_group.add_option("", "--secret-key", help="An AWS Secret Key",
+ type="string", dest="secret_key")
+
+parser.add_option_group(creds_group)
+parser.add_option("-s", "--stack", help="A CloudFormation stack",
+ type="string", dest="stack_name")
+parser.add_option("-r", "--resource", help="A CloudFormation logical resource ID",
+ type="string", dest="logical_resource_id")
+
+parser.add_option("-c", "--configsets", help='An optional list of configSets (default: "default")',
+ type="string", dest="configsets")
+
+parser.add_option("-u", "--url", help="The CloudFormation service URL. The endpoint URL must match the region option. Use of this parameter is discouraged.",
+ type="string", dest="endpoint")
+parser.add_option("", "--region", help="The CloudFormation region. Default: us-east-1.",
+ type="string", dest="region", default="us-east-1")
+
+parser.add_option("-v", "--verbose", help="Enables verbose logging",
+ action="store_true", dest="verbose")
+
+if os.name == "nt":
+ parser.add_option("", "--resume", help="Resume from a previous cfn-init run",
+ action="store_true", dest="resume")
+
+(options, args) = parser.parse_args()
+
+cfnbootstrap.configureLogging("DEBUG" if options.verbose else "INFO")
+
+worklog = WorkLog()
+
+if os.name == "nt" and options.resume:
+
+ if not worklog.has_key('metadata'):
+ print >> sys.stderr, "Error: cannot resume from previous session; no metadata stored"
+ sys.exit(1)
+
+ try:
+ worklog.resume()
+ except Exception, e:
+ print >> sys.stderr, "Error occurred during resume: %s" % str(e)
+ logging.exception("Unhandled exception during resume: %s", str(e))
+ sys.exit(1)
+ sys.exit(0)
+
+if not options.stack_name or not options.logical_resource_id:
+ print >> sys.stderr, "Error: You must specify both a stack name and logical resource id"
+ parser.print_help(sys.stderr)
+ sys.exit(1)
+
+if options.credential_file:
+ try:
+ access_key, secret_key = util.extract_credentials(options.credential_file)
+ except IOError, e:
+ print >> sys.stderr, "Error retrieving credentials from file:\n\t%s" % e.strerror
+ sys.exit(1)
+else:
+ access_key = options.access_key
+ secret_key = options.secret_key
+
+url = CloudFormationClient.endpointForRegion(options.region)
+if options.endpoint:
+ url = options.endpoint
+
+configSets = ["default"]
+if options.configsets:
+ configSets = options.configsets.split(',')
+
+try:
+ detail = CloudFormationClient(Credentials(access_key, secret_key), url=url, region=options.region).describe_stack_resource(options.logical_resource_id, options.stack_name)
+except IOError, e:
+ if e.strerror:
+ print >> sys.stderr, e.strerror
+ else:
+ print >> sys.stderr, "Unknown error retrieving %s" % options.logical_resource_id
+ sys.exit(1)
+
+if not detail.metadata:
+ print >> sys.stderr, "Error: %s does not specify any metadata" % detail.logicalResourceId
+ sys.exit(1)
+
+if os.name == 'nt':
+ data_dir = os.path.expandvars(r'${SystemDrive}\cfn\cfn-init\data')
+else:
+ data_dir = '/var/lib/cfn-init/data'
+if not os.path.isdir(data_dir) and not os.path.exists(data_dir):
+ os.makedirs(data_dir)
+
+if os.path.isdir(data_dir):
+ with file(os.path.join(data_dir, 'metadata.json'), 'w') as f:
+ json.dump(detail.metadata, f, indent=4)
+else:
+ print >> sys.stderr, "Could not create %s to store metadata" % data_dir
+ logging.error("Could not create %s to store metadata", data_dir)
+
+if Contractor.metadataValid(detail.metadata):
+ try:
+ worklog.build(detail.metadata, configSets)
+ except Exception, e:
+ print >> sys.stderr, "Error occurred during build: %s" % str(e)
+ logging.exception("Unhandled exception during build: %s" % str(e))
+ sys.exit(1)
+else:
+ print >> sys.stderr, "No work to do, exiting."
+ sys.exit(0)
94 bin/cfn-send-cmd-result
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+from cfnbootstrap.aws_client import Credentials
+from cfnbootstrap.sqs_client import SQSClient
+from cfnbootstrap import util
+from optparse import OptionParser
+import os
+import sys
+
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+parser = OptionParser(usage="usage: %prog [options] [Command Result Data]")
+parser.add_option("", "--access-key", help="An AWS Access Key",
+ type="string", dest="access_key", default=os.environ.get('RESULT_ACCESS_KEY'))
+parser.add_option("", "--secret-key", help="An AWS Secret Key",
+ type="string", dest="secret_key", default=os.environ.get('RESULT_SECRET_KEY'))
+parser.add_option("", "--token", help="An AWS Session Token",
+ type="string", dest="security_token", default=os.environ.get('RESULT_SESSION_TOKEN'))
+parser.add_option("-q", "--queue-url", help="SQS Queue URL for storing the command result",
+ type="string", dest="queue_url", default=os.environ.get('RESULT_QUEUE'))
+
+
+parser.add_option("-d", "--dispatcher-id", help="The dispatcher ID",
+ type="string", dest="dispatcher_id", default=os.environ.get('DISPATCHER_ID'))
+parser.add_option("-c", "--command-name", help="The command name",
+ type="string", dest="command_name", default=os.environ.get('CMD_NAME'))
+parser.add_option("-i", "--invocation-id", help="The invocation ID",
+ type="string", dest="invocation_id", default=os.environ.get('INVOCATION_ID'))
+parser.add_option("-l", "--listener-id", help="The listener ID",
+ type="string", dest="listener_id", default=os.environ.get('LISTENER_ID'))
+
+#Optional arguments
+parser.add_option("-s", "--success", help="If true, signal success; if false, signal failure. Default: true",
+ dest="success", action="store", type="choice", choices=["true", "false"], default="true")
+parser.add_option("-e", "--exit-code", help="Derive success or failure from specified exit code. Note: This takes precedence over the success flag", dest="exit_code", type="int", action="store")
+
+(options, args) = parser.parse_args()
+
+def fail_on_unspecified(value, name):
+ if not value:
+ print >> sys.stderr, "Error: You must specify %s" % name
+ parser.print_help(sys.stderr)
+ sys.exit(1)
+
+fail_on_unspecified(options.dispatcher_id, "DispatcherId")
+fail_on_unspecified(options.command_name, "CommandName")
+fail_on_unspecified(options.invocation_id, "InvocationId")
+fail_on_unspecified(options.listener_id, "ListenerId")
+fail_on_unspecified(options.queue_url, "QueueUrl")
+
+if (not options.access_key or not options.secret_key or not options.security_token):
+ print >> sys.stderr, "Error: You must specify a token and an access key/secret key pair"
+ parser.print_help(sys.stderr)
+ exit(1)
+
+if options.exit_code == None:
+ success = options.success == 'true'
+else:
+ success = options.exit_code == 0
+
+result_msg = {
+ 'DispatcherId' : options.dispatcher_id,
+ 'CommandName' : options.command_name,
+ 'ListenerId' : options.listener_id,
+ 'InvocationId' : options.invocation_id,
+ 'Data' : ' '.join(args) if success else '',
+ 'Message' : ' '.join(args) if not success else '',
+ 'Status' : 'SUCCESS' if success else 'FAILURE',
+ }
+
+client = SQSClient(Credentials(options.access_key, options.secret_key, options.security_token))
+try:
+ client.send_message(options.queue_url,json.dumps(result_msg))
+except Exception, e:
+ print >> sys.stderr, "Error: Could not send command result: " + str(e)
+ sys.exit(1)
90 bin/cfn-signal
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+from cfnbootstrap import util
+from optparse import OptionParser
+import base64
+import platform
+import re
+import requests
+import socket
+import sys
+import time
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+default_id = util.get_instance_id()
+if not default_id:
+ default_id = socket.getfqdn()
+
+parser = OptionParser(usage="usage: %prog [options] [WaitConditionHandle URL]")
+parser.add_option("-s", "--success", help="If true, signal success to CloudFormation; if false, signal failure. Default: true",
+ dest="success", action="store", type="choice", choices=["true", "false"], default="true")
+parser.add_option("-r", "--reason", help="The reason for success/failure", dest="reason", type="string", action="store", default="")
+parser.add_option("-d", "--data", help="Data to include with the WaitCondition signal", dest="data", type="string", action="store", default="")
+parser.add_option("-i", "--id", help="A unique ID to send with the WaitCondition signal", dest="id", type="string", action="store", default=default_id)
+parser.add_option("-e", "--exit-code", help="Derive success or failure from specified exit code", dest="exit_code", type="int", action="store")
+
+(options, args) = parser.parse_args()
+
+signal_success = True
+
+if options.exit_code:
+ signal_success = False
+elif options.success != "true":
+ signal_success = False
+
+if not args or not args[0]:
+ print >> sys.stderr, "Error: No WaitConditionHandle URL specified"
+ parser.print_help(sys.stderr)
+ sys.exit(1)
+
+if not options.reason and not signal_success:
+ options.reason="Configuration failed."
+
+data = { 'Status': 'SUCCESS' if signal_success else 'FAILURE',
+ 'Reason' : options.reason,
+ 'Data' : options.data,
+ 'UniqueId' : options.id }
+
+try:
+ url = args[0] if re.match(r'https?://.*', args[0]) else base64.b64decode(args[0])
+except TypeError:
+ print >> sys.stderr, "Error: Invalid WaitConditionHandle URL specified: %s" % args[0]
+ sys.exit(1)
+
+if not re.match(r'https?://.*', url):
+ print >> sys.stderr, "Error: Invalid WaitConditionHandle URL specified: %s" % args[0]
+ sys.exit(1)
+
+@util.retry_on_failure()
+def send(url, data):
+ requests.put(url,
+ data=json.dumps(data),
+ headers={"Content-Type" : ""},
+ verify=util.get_cert(),
+ config={'danger_mode' : True})
+
+try:
+ send(url, data)
+ print 'CloudFormation signaled successfully with %s.' % data['Status']
+ sys.exit(0)
+except IOError, e:
+ print >> sys.stderr, 'Error signaling CloudFormation: %s' % str(e)
+ sys.exit(1)
84 cfnbootstrap/__init__.py
@@ -0,0 +1,84 @@
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+import logging.config
+import os.path
+import sys
+import StringIO
+
+_config ="""[loggers]
+keys=root,cfninit,cfnclient,cfnhup
+[handlers]
+keys=%(conf_handler)s
+[formatters]
+keys=amzn
+[logger_root]
+level=NOTSET
+handlers=%(conf_handler)s
+[logger_cfninit]
+level=NOTSET
+handlers=%(conf_handler)s
+qualname=cfn.init
+propagate=0
+[logger_cfnhup]
+level=NOTSET
+handlers=%(conf_handler)s
+qualname=cfn.hup
+propagate=0
+[logger_cfnclient]
+level=NOTSET
+handlers=%(conf_handler)s
+qualname=cfn.client
+propagate=0
+[handler_default]
+class=handlers.RotatingFileHandler
+level=%(conf_level)s
+formatter=amzn
+args=('%(conf_file)s', 'a', 5242880, 5)
+[handler_tostderr]
+class=StreamHandler
+level=%(conf_level)s
+formatter=amzn
+args=(sys.stderr,)
+[formatter_amzn]
+format=%(asctime)s [%(levelname)s] %(message)s
+datefmt=
+class=logging.Formatter
+"""
+
+def _getLogFile(filename):
+ if os.name == 'nt':
+ logdir = os.path.expandvars(r'${SystemDrive}\cfn\log')
+ if not os.path.exists(logdir):
+ os.makedirs(logdir)
+ return logdir + os.path.sep + filename
+
+ return '/var/log/%s' % filename
+
+
+def configureLogging(level='INFO', quiet=False, filename='cfn-init.log', log_dir=None):
+ if not log_dir:
+ output_file=_getLogFile(filename)
+ else:
+ output_file = os.path.join(log_dir, filename)
+
+ try:
+ logging.config.fileConfig(StringIO.StringIO(_config), {'conf_level' : level, 'conf_handler' : 'default', 'conf_file' : output_file})
+ except IOError:
+ if not quiet:
+ print >> sys.stderr, "Could not open %s for logging. Using stderr instead." % output_file
+ logging.config.fileConfig(StringIO.StringIO(_config), {'conf_level' : level, 'conf_handler' : 'tostderr'})
+
+configureLogging(quiet=True)
124 cfnbootstrap/apt_tool.py
@@ -0,0 +1,124 @@
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+
+from cfnbootstrap.util import ProcessHelper
+import logging
+import os
+from cfnbootstrap.construction_errors import ToolError
+import subprocess
+
+log = logging.getLogger("cfn.init")
+
+class AptTool(object):
+ """
+ Installs packages via APT
+
+ """
+
+ def apply(self, action, auth_config=None):
+ """
+ Install a set of packages via APT, returning the packages actually installed or updated.
+
+ Arguments:
+ action -- a dict of package name to version; version can be empty, a single string or a list of strings
+
+ Exceptions:
+ ToolError -- on expected failures (such as a non-zero exit code)
+ """
+
+ pkgs_changed = []
+
+ if not action:
+ log.debug("No packages specified for APT")
+ return pkgs_changed
+
+ cache_result = ProcessHelper(['apt-cache', '-q', 'gencaches']).call()
+
+ if cache_result.returncode:
+ log.error("APT gencache failed. Output: %s", cache_result.stdout)
+ raise ToolError("Could not create apt cache", cache_result.returncode)
+
+ pkg_specs = []
+
+ for pkg_name in action:
+ if action[pkg_name]:
+ if isinstance(action[pkg_name], basestring):
+ pkg_keys = ['%s=%s' % (pkg_name, action[pkg_name])]
+ else:
+ pkg_keys = ['%s=%s' % (pkg_name, ver) if ver else pkg_name for ver in action[pkg_name]]
+ else:
+ pkg_keys = [pkg_name]
+
+ pkgs_filtered = [pkg_key for pkg_key in pkg_keys if self._pkg_filter(pkg_key, pkg_name)]
+ if pkgs_filtered:
+ pkg_specs.extend(pkgs_filtered)
+ pkgs_changed.append(pkg_name)
+
+ if not pkg_specs:
+ log.info("All APT packages were already installed")
+ return []
+
+ log.info("Attempting to install %s via APT", pkg_specs)
+
+ env = dict(os.environ)
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+
+ result = ProcessHelper(['apt-get', '-q', '-y', 'install'] + pkg_specs, env=env).call()
+
+ if result.returncode:
+ log.error("apt-get failed. Output: %s", result.stdout)
+ raise ToolError("Could not successfully install APT packages", result.returncode)
+
+ log.info("APT installed %s", pkgs_changed)
+ log.debug("APT output: %s", result.stdout)
+
+ return pkgs_changed
+
+ def _pkg_filter(self, pkg, pkg_name):
+ if self._pkg_installed(pkg, pkg_name):
+ log.debug("%s will not be installed as it is already present", pkg)
+ return False
+ elif not self._pkg_available(pkg):
+ log.error("%s is not available to be installed", pkg)
+ raise ToolError("APT does not have %s available for installation" % pkg)
+ else:
+ return True
+
+ def _pkg_available(self, pkg):
+ result = ProcessHelper(['apt-cache', '-q', 'show', pkg]).call()
+
+ return result.returncode == 0
+
+ def _pkg_installed(self, pkg, pkg_name):
+ """
+ Test if a package is installed (exact version match if version is specified), returning a boolean.
+
+ Arguments:
+ pkg -- the full package specification (including version if specified) in pkg=version format
+ pkg_name -- the name of the package
+ """
+
+ result = ProcessHelper(['dpkg-query', '-f', '${Status}|${Package}=${Version}', '-W', pkg_name], stderr=subprocess.PIPE).call()
+
+ if result.returncode or not result.stdout:
+ return False
+
+ status,divider,spec = result.stdout.strip().partition('|')
+
+ if status.rpartition(" ")[2] != 'installed':
+ return False
+
+ return spec.startswith(pkg)
154 cfnbootstrap/auth.py
@@ -0,0 +1,154 @@
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+from requests.auth import AuthBase, HTTPBasicAuth
+import base64
+import datetime
+import hashlib
+import hmac
+import logging
+import re
+import urlparse
+
+log = logging.getLogger("cfn.init")
+
+class S3Signer(object):
+
+ def __init__(self, access_key, secret_key):
+ self._access_key = access_key
+ self._secret_key = secret_key
+
+ def sign(self, req):
+ if 'Date' not in req.headers:
+ req.headers['X-Amz-Date'] = datetime.datetime.utcnow().replace(microsecond=0).strftime("%a, %d %b %Y %H:%M:%S GMT")
+
+ stringToSign = req.method + '\n'
+ stringToSign += req.headers.get('content-md5', '') + '\n'
+ stringToSign += req.headers.get('content-type', '') + '\n'
+ stringToSign += req.headers.get('date', '') + '\n'
+ stringToSign += self._canonicalize_headers(req)
+ stringToSign += self._canonicalize_resource(req)
+
+ signed = base64.encodestring(hmac.new(self._secret_key.encode('utf-8'), stringToSign.encode('utf-8'), hashlib.sha1).digest()).strip()
+
+ req.headers['Authorization'] = 'AWS %s:%s' % (self._access_key, signed)
+
+ return req
+
+ def _canonicalize_headers(self, req):
+ headers = [(hdr.lower(), val) for hdr, val in req.headers.iteritems() if hdr.lower().startswith('x-amz')]
+ return '\n'.join([hdr + ':' + val for hdr, val in sorted(headers)]) + '\n' if headers else ''
+
+ def _canonicalize_resource(self, req):
+ url = urlparse.urlparse(req.full_url)
+ match = re.match(r'^([^\.]+)\.s3(-[\w\d-]+)?.amazonaws.com$', url.netloc)
+ if match:
+ return '/' + match.group(1) + url.path
+ return url.path
+
+class S3DefaultAuth(AuthBase):
+
+ def __init__(self):
+ self._bucketToSigner = {}
+
+ def add_creds_for_bucket(self, bucket, access_key, secret_key):
+ self._bucketToSigner[bucket] = S3Signer(access_key, secret_key)
+
+ def __call__(self, req):
+ bucket = self._extract_bucket(req)
+ if bucket and bucket in self._bucketToSigner:
+ return self._bucketToSigner[bucket].sign(req)
+ return req
+
+ def _extract_bucket(self, req):
+ url = urlparse.urlparse(req.full_url)
+ match = re.match(r'^([^\.]+\.)?s3(-[\w\d-]+)?.amazonaws.com$', url.netloc)
+ if not match:
+ # Not an S3 URL, skip
+ return None
+ elif match.group(1):
+ # Subdomain-style S3 URL
+ return match.group(1).rstrip('.')
+ else:
+ # This means that we're using path-style buckets
+ # lop off the first / and return everything up to the next /
+ return url.path[1:].partition('/')[0]
+
+class S3Auth(AuthBase):
+
+ def __init__(self, access_key, secret_key):
+ self._signer = S3Signer(access_key, secret_key)
+
+ def __call__(self, req):
+ return self._signer.sign(req)
+
+class BasicDefaultAuth(AuthBase):
+
+ def __init__(self):
+ self._auths = {}
+
+ def __call__(self, req):
+ base_uri = urlparse.urlparse(req.full_url).netloc
+ if base_uri in self._auths:
+ return self._auths[base_uri](req)
+ return req
+
+ def add_password(self, uri, username, password):
+ self._auths[uri] = HTTPBasicAuth(username, password)
+
+class DefaultAuth(AuthBase):
+
+ def __init__(self, s3, basic):
+ self._s3 = s3
+ self._basic = basic
+
+ def __call__(self, req):
+ return self._s3(self._basic(req))
+
+class AuthenticationConfig(object):
+
+ def __init__(self, model):
+
+ self._auths = {}
+
+ s3Auth = S3DefaultAuth()
+ basicAuth = BasicDefaultAuth()
+
+ for key, config in model.iteritems():
+ configType = config.get('type', '')
+ if 's3' == configType.lower():
+ self._auths[key] = S3Auth(config.get('accessKeyId'), config.get('secretKey'))
+ if 'buckets' in config:
+ buckets = [config['buckets']] if isinstance(config['buckets'], basestring) else config['buckets']
+ for bucket in buckets:
+ s3Auth.add_creds_for_bucket(bucket, config.get('accessKeyId'), config.get('secretKey'))
+ elif 'basic' == configType.lower():
+ self._auths[key] = HTTPBasicAuth(config.get('username'), config.get('password'))
+ if 'uris' in config:
+ if isinstance(config['uris'], basestring):
+ basicAuth.add_password(config['uris'], config.get('username'), config.get('password'))
+ else:
+ for u in config['uris']:
+ basicAuth.add_password(u, config.get('username'), config.get('password'))
+ else:
+ log.warn("Unrecognized authentication type: %s", configType)
+
+ self._defaultAuth = DefaultAuth(s3Auth, basicAuth)
+
+ def get_auth(self, key):
+ if not key or not key in self._auths:
+ return self._defaultAuth
+
+ return self._auths[key]
297 cfnbootstrap/aws_client.py
@@ -0,0 +1,297 @@
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+"""
+Base classes for AWS/QUERY clients
+
+Classes:
+AWSClient - an HTTP client that makes signed requests
+
+"""
+from cfnbootstrap import util
+from requests import api
+from xml.etree import ElementTree
+import StringIO
+import base64
+import datetime
+import hashlib
+import hmac
+import logging
+import operator
+import re
+import urllib
+import urlparse
+
+log = logging.getLogger('cfn.client')
+
+class Signer(object):
+
+ def sign(self, verb, base_url, params, creds, in_headers={}, timestamp=None):
+ pass
+
+ def _construct_query(self, sign_data):
+ ret_str = ''
+ for k, vs in sorted(sign_data.iteritems(), key=operator.itemgetter(0)):
+ if isinstance(vs, list):
+ for v in sorted(vs):
+ ret_str += '&'.join(urllib.quote(k, safe='~') + '=' + urllib.quote(v, safe='~'))
+ else:
+ if ret_str:
+ ret_str += '&'
+ ret_str += urllib.quote(k, safe='~') + '=' + urllib.quote(vs, safe='~')
+
+ return ret_str
+
+ def _normalize_url(self, base_url):
+ return base_url if base_url.endswith('/') else base_url + '/'
+
+class CFNSigner(Signer):
+
+ def sign(self, verb, base_url, params, creds, in_headers={}, timestamp=None):
+ base_url = self._normalize_url(base_url)
+
+ if not util.is_ec2():
+ raise ValueError("Cannot use CFN signature outside of EC2")
+
+ document = util.get_instance_identity_document()
+ signature = util.get_instance_identity_signature()
+
+ new_headers = dict(in_headers)
+ new_headers['Authorization'] = 'CFN_V1 %s:%s' % (base64.b64encode(document), signature.replace('\n', ''))
+
+ return (verb, base_url, params, new_headers)
+
+class V2Signer(Signer):
+
+ def sign(self, verb, base_url, in_params, creds, in_headers={}, timestamp=None):
+ base_url = self._normalize_url(base_url)
+
+ if not timestamp:
+ timestamp = datetime.datetime.utcnow()
+
+ if not in_params:
+ 'Signature V2 requires at least 1 Query String parameter (Action)'
+
+ params = dict(in_params)
+ params['SignatureVersion'] = '2'
+ params['SignatureMethod'] = 'HmacSHA256'
+ params['AWSAccessKeyId'] = creds.access_key
+ params['Timestamp'] = timestamp.replace(microsecond=0).isoformat()
+ if creds.security_token:
+ params['SecurityToken'] = creds.security_token
+
+ split_url = urlparse.urlsplit(base_url)
+
+ new_headers = dict(in_headers)
+ new_headers['Host'] = split_url.netloc
+ if verb == 'POST':
+ new_headers['Content-type'] = 'application/x-www-form-urlencoded'
+
+ stringToSign = verb + '\n' + split_url.netloc + '\n' + (split_url.path if split_url.path else '/') + '\n'
+
+ stringToSign += self._construct_query(params)
+
+ params['Signature'] = base64.b64encode(hmac.new(creds.secret_key.encode('utf-8'), stringToSign.encode('utf-8'), hashlib.sha256).digest())
+
+ return (verb, base_url, params, new_headers)
+
+class V4Signer(Signer):
+
+ def __init__(self, region, service, terminator='aws4_request'):
+ self._region = region
+ self._service = service
+ self._terminator = terminator
+
+ def sign(self, verb, base_url, params, creds, in_headers={}, timestamp=None):
+ base_url = self._normalize_url(base_url)
+
+ if not timestamp:
+ timestamp = datetime.datetime.utcnow()
+
+ new_headers = dict(in_headers)
+
+ timestamp_formatted = timestamp.strftime('%Y%m%dT%H%M%SZ')
+ timestamp_short = timestamp.strftime('%Y%m%d')
+
+ scope = timestamp_short + '/' + self._region + '/' + self._service + '/' + self._terminator
+
+ if 'Date' in new_headers:
+ del new_headers['Date']
+ new_headers['X-Amz-Date'] = timestamp_formatted
+ if creds.security_token:
+ new_headers['X-Amz-Security-Token'] = creds.security_token
+ new_headers['Host'] = urlparse.urlsplit(base_url).netloc
+ if verb == 'POST':
+ new_headers['Content-type'] = 'application/x-www-form-urlencoded'
+
+ canonical_request = verb + '\n'
+ canonical_request += self._canonicalize_uri(base_url) + '\n'
+ canonical_request += (self._canonicalize_query(params) if verb == 'GET' else '') + '\n'
+
+ (canonical_headers, signed_headers) = self._canonicalize_headers(new_headers)
+ canonical_request += canonical_headers + '\n' + signed_headers + '\n'
+ canonical_request += hashlib.sha256(self._construct_query(params).encode('utf-8') if verb == 'POST' else '').hexdigest()
+
+ string_to_sign = 'AWS4-HMAC-SHA256\n' + timestamp_formatted + '\n' + scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
+
+ derived_key = hmac.new(("AWS4" + creds.secret_key).encode('utf-8'), timestamp_short.encode('utf-8'), hashlib.sha256).digest()
+ derived_key = hmac.new(derived_key, self._region.encode('utf-8'), hashlib.sha256).digest()
+ derived_key = hmac.new(derived_key, self._service.encode('utf-8'), hashlib.sha256).digest()
+ derived_key = hmac.new(derived_key, "aws4_request".encode('utf-8'), hashlib.sha256).digest()
+
+ signature = hmac.new(derived_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
+
+ credential = creds.access_key + '/' + scope
+ new_headers['Authorization'] = 'AWS4-HMAC-SHA256 Credential=%s,SignedHeaders=%s, Signature=%s' % (credential, signed_headers, signature)
+
+ return (verb, base_url, params, new_headers)
+
+ def _canonicalize_uri(self, uri):
+ split = urlparse.urlsplit(uri)
+ if not split.path:
+ return '/'
+ path = urlparse.urlsplit(urlparse.urljoin('http://foo.com', split.path.lstrip('/'))).path.rstrip('/')
+ return urllib.quote(path, '/~') if path else '/'
+
+ def _canonicalize_query(self, params):
+ if not params:
+ return ''
+
+ encoded_pairs = ((urllib.quote(entry[0], '~'), urllib.quote(entry[1], '~') if len(entry) > 1 else '') for entry in params.iteritems())
+ sorted_pairs = sorted(encoded_pairs, key=operator.itemgetter(0, 1))
+
+ return '&'.join(('='.join(pair) for pair in sorted_pairs))
+
+ def _canonicalize_headers(self, headers):
+ canon_headers = {}
+ for key, value in ((key.lower(), re.sub(r'(?su)[\s]+', ' ', value).strip()) for key, value in headers.iteritems()):
+ if key in canon_headers:
+ canon_headers[key] = canon_headers[key] + ',' + value
+ else:
+ canon_headers[key] = value
+
+ sorted_entries = sorted(canon_headers.iteritems(), key=operator.itemgetter(0))
+
+ return ('\n'.join((':'.join(entry) for entry in sorted_entries)) + '\n', ';'.join((entry[0] for entry in sorted_entries)))
+
+
+class Credentials(object):
+ '''
+ AWS Credentials
+ '''
+
+ def __init__(self, access_key, secret_key, security_token=None, expiration=None):
+ self._access_key = access_key
+ self._secret_key = secret_key
+ self._security_token = security_token
+ self._expiration = expiration
+
+ @property
+ def access_key(self):
+ return self._access_key
+
+ @property
+ def secret_key(self):
+ return self._secret_key
+
+ @property
+ def security_token(self):
+ return self._security_token
+
+ @property
+ def expiration(self):
+ return self._expiration
+
+ @classmethod
+ def from_response(cls, resp):
+ body = util.json_from_response(resp)['GetListenerCredentialsResponse']['GetListenerCredentialsResult']['Credentials']
+ return Credentials(body['AccessKeyId'],
+ body['SecretAccessKey'],
+ body['SessionToken'],
+ datetime.datetime.utcfromtimestamp(body['Expiration']))
+
+
+
+class AwsQueryError(util.RemoteError):
+
+ def __init__(self, status_code, error_code, error_type, msg):
+ # Retry for Throttling or InvalidAccessKeyId (IAM propagation delay)
+ if status_code == 503 or error_code in ('Throttling', 'InvalidAccessKeyId', 'InvalidClientTokenId'):
+ retry_mode = 'RETRIABLE_FOREVER'
+ elif error_type == 'Sender':
+ retry_mode = 'TERMINAL'
+ else:
+ retry_mode = 'RETRIABLE'
+
+ super(AwsQueryError, self).__init__(status_code, "%s: %s" % (error_code, msg), retry_mode)
+
+ self.error_code = error_code
+ self.error_type = error_type
+
+class Client(object):
+ '''
+ A base AWS/QUERY client
+ '''
+
+ def __init__(self, credentials, is_json, endpoint=None, signer=V2Signer(), xmlns=None):
+ self._credentials = credentials
+ self._endpoint = endpoint
+ self._is_json = is_json
+ self._xmlns = xmlns
+ self._signer = signer
+
+ @staticmethod
+ def _extract_json_message(resp):
+ try:
+ eDoc = util.json_from_response(resp)['Error']
+ code = eDoc['Code']
+ message = eDoc['Message']
+ error_type = eDoc['Type']
+
+ return AwsQueryError(resp.status_code, code, error_type, message)
+ except (TypeError, AttributeError, KeyError, ValueError):
+ return AwsQueryError(resp.status_code, 'Unknown', 'Receiver', resp.text)
+
+ @staticmethod
+ def _get_xml_extractor(xmlns):
+ def _extract_xml_message(resp):
+ try:
+ eDoc = ElementTree.ElementTree(file=StringIO.StringIO(resp.content))
+ code = eDoc.findtext('{%s}Error/{%s}Code' % (xmlns, xmlns))
+ error_type = eDoc.findtext('{%s}Error/{%s}Type' % (xmlns, xmlns))
+ message = eDoc.findtext('{%s}Error/{%s}Message' % (xmlns, xmlns))
+
+ return AwsQueryError(resp.status_code, code, error_type, message)
+ except (TypeError, AttributeError, KeyError, ValueError):
+ return AwsQueryError(resp.status_code, 'Unknown', 'Receiver', resp.text)
+
+ return _extract_xml_message
+
+ def _call(self, params, endpoint=None, request_credentials=None, verb='GET'):
+ base = endpoint if endpoint else self._endpoint
+ creds = request_credentials if request_credentials else self._credentials
+ accept_type = "application/json" if self._is_json else "application/xml"
+ req = self._signer.sign(verb, base, params, creds, {"Accept" : accept_type})
+
+ return self._make_request(*req)
+
+ def _make_request(self, verb, base_url, params, headers):
+ return api.request(verb, base_url,
+ data=params if verb=='POST' else dict(),
+ params=params if verb!='POST' else dict(),
+ headers=headers,
+ verify=util.get_cert(),
+ prefetch=False,
+ config={'danger_mode' : True})
238 cfnbootstrap/cfn_client.py
@@ -0,0 +1,238 @@
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+"""
+CloudFormation client-related classes
+
+Classes:
+CloudFormationClient - an HTTP client that makes API calls against CloudFormation
+StackResourceDetail - detailed information about a StackResource
+
+"""
+from cfnbootstrap import aws_client, util
+from cfnbootstrap.aws_client import CFNSigner, V4Signer
+from cfnbootstrap.util import retry_on_failure
+import datetime
+import logging
+import re
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+log = logging.getLogger("cfn.client")
+
+class CloudFormationClient(aws_client.Client):
+ """
+ Makes API calls against CloudFormation
+
+ Notes:
+ - Public methods of this class have a 1-to-1 equivalence to published CloudFormation APIs.
+ - Calls are retried internally when appropriate; callers should not retry.
+
+ """
+
+ _apiVersion = "2010-05-15"
+
+ def __init__(self, credentials, url=None, region='us-east-1'):
+
+ if not url:
+ endpoint = CloudFormationClient.endpointForRegion(region)
+ else:
+ endpoint = url
+
+ self._using_instance_identity = (not credentials or not credentials.access_key) and util.is_ec2()
+
+ if not self._using_instance_identity:
+ if not region:
+ region = CloudFormationClient.regionForEndpoint(endpoint)
+
+ if not region:
+ raise ValueError('Region is required for AWS V4 Signatures')
+
+ signer = CFNSigner() if self._using_instance_identity else V4Signer(region, 'cloudformation')
+
+ super(CloudFormationClient, self).__init__(credentials, True, endpoint, signer)
+
+ log.debug("CloudFormation client initialized with endpoint %s", endpoint)
+
+ @classmethod
+ def endpointForRegion(cls, region):
+ return 'https://cloudformation.%s.amazonaws.com' % region
+
+ @classmethod
+ def regionForEndpoint(cls, endpoint):
+ match = re.match(r'https://cloudformation.([\w\d-]+).amazonaws.com', endpoint)
+ if match:
+ return match.group(1)
+ log.warn("Non-standard CloudFormation endpoint: %s", endpoint)
+ return None
+
+ @retry_on_failure(http_error_extractor=aws_client.Client._extract_json_message)
+ def describe_stack_resource(self, logicalResourceId, stackName, request_credentials=None):
+ """
+ Calls DescribeStackResource and returns a StackResourceDetail object.
+
+ Throws an IOError on failure.
+ """
+ log.debug("Describing resource %s in stack %s", logicalResourceId, stackName)
+
+ return StackResourceDetail(self._call({"Action" : "DescribeStackResource",
+ "LogicalResourceId" : logicalResourceId,
+ "ContentType" : "JSON",
+ "StackName": stackName,
+ "Version": CloudFormationClient._apiVersion },
+ request_credentials=request_credentials))
+
+ @retry_on_failure(http_error_extractor=aws_client.Client._extract_json_message)
+ def register_listener(self, stack_name, listener_id=None, request_credentials=None):
+ """
+ Calls RegisterListener and returns a Listener object
+
+ Throws an IOError on failure.
+ """
+ log.debug("Registering listener %s for stack %s", listener_id, stack_name)
+
+ params = {"Action" : "RegisterListener",
+ "StackName" : stack_name,
+ "ContentType" : "JSON"}
+
+ if not self._using_instance_identity:
+ params["ListenerId"] = listener_id
+
+ return Listener(self._call(params, request_credentials = request_credentials))
+
+ @retry_on_failure(http_error_extractor=aws_client.Client._extract_json_message)
+ def elect_command_leader(self, stack_name, command_name, invocation_id, listener_id=None, request_credentials=None):
+ """
+ Calls ElectCommandLeader and returns the listener id of the leader
+
+ Throws an IOError on failure.
+ """
+ log.debug("Attempting to elect '%s' as leader for stack: %s, command: %s, invocation: %s",
+ listener_id, stack_name, command_name, invocation_id)
+
+ params = {"Action" : "ElectCommandLeader",
+ "CommandName" : command_name,
+ "InvocationId" : invocation_id,
+ "StackName" : stack_name,
+ "ContentType" : "JSON"}
+
+ if not self._using_instance_identity:
+ params["ListenerId"] = listener_id
+
+ result_data = util.json_from_response(self._call(params, request_credentials = request_credentials))
+
+ return result_data['ElectCommandLeaderResponse']['ElectCommandLeaderResult']['ListenerId']
+
+ @retry_on_failure(http_error_extractor=aws_client.Client._extract_json_message)
+ def get_listener_credentials(self, stack_name, listener_id=None, request_credentials=None):
+ """
+ Calls GetListenerCredentials and returns a Credentials object
+
+ Throws an IOError on failure.
+ """
+ log.debug("Get listener credentials for listener %s in stack %s", listener_id, stack_name)
+
+ params = {"Action" : "GetListenerCredentials",
+ "StackName" : stack_name,
+ "ContentType" : "JSON"}
+
+ if not self._using_instance_identity:
+ params["ListenerId"] = listener_id
+
+ return aws_client.Credentials.from_response(self._call(params, request_credentials = request_credentials))
+
+
+class Listener(object):
+ """Result of RegisterListener"""
+
+ def __init__(self, resp):
+ result = util.json_from_response(resp)['RegisterListenerResponse']['RegisterListenerResult']
+ self._queue_url = result['QueueUrl']
+
+ @property
+ def queue_url(self):
+ return self._queue_url
+
+class StackResourceDetail(object):
+ """Detailed information about a stack resource"""
+
+ def __init__(self, resp):
+ detail = util.json_from_response(resp)['DescribeStackResourceResponse']['DescribeStackResourceResult']['StackResourceDetail']
+
+ self._description = detail.get('Description')
+ self._lastUpdated = datetime.datetime.utcfromtimestamp(detail['LastUpdatedTimestamp'])
+ self._logicalResourceId = detail['LogicalResourceId']
+
+ _rawMetadata = detail.get('Metadata')
+ self._metadata = json.loads(_rawMetadata) if _rawMetadata else None
+
+ self._physicalResourceId = detail.get('PhysicalResourceId')
+ self._resourceType = detail['ResourceType']
+ self._resourceStatus = detail['ResourceStatus']
+ self._resourceStatusReason = detail.get('ResourceStatusReason')
+ self._stackId = detail.get('StackId')
+ self._stackName = detail.get('StackName')
+
+ @property
+ def logicalResourceId(self):
+ """The resource's logical resource ID"""
+ return self._logicalResourceId
+
+ @property
+ def description(self):
+ """The resource's description"""
+ return self._description
+
+ @property
+ def lastUpdated(self):
+ """The timestamp of this resource's last status change as a datetime object"""
+ return self._lastUpdated
+
+ @property
+ def metadata(self):
+ """The resource's metadata as python object (not as a JSON string)"""
+ return self._metadata
+
+ @property
+ def physicalResourceId(self):
+ """The resource's physical resource ID"""
+ return self._physicalResourceId
+
+ @property
+ def resourceType(self):
+ """The resource's type"""
+ return self._resourceType
+
+ @property
+ def resourceStatus(self):
+ """The resource's status"""
+ return self._resourceStatus
+
+ @property
+ def resourceStatusReason(self):
+ """The reason for this resource's status"""
+ return self._resourceStatusReason
+
+ @property
+ def stackId(self):
+ """The ID of the stack this resource belongs to"""
+ return self._stackId
+
+ @property
+ def stackName(self):
+ """The name of the stack this resource belongs to"""
+ return self._stackName
126 cfnbootstrap/command_tool.py
@@ -0,0 +1,126 @@
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+from cfnbootstrap.construction_errors import ToolError
+from cfnbootstrap.util import ProcessHelper, interpret_boolean
+import logging
+import os.path
+import subprocess
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+log = logging.getLogger("cfn.init")
+
+class CommandTool(object):
+ """
+ Executes arbitrary commands
+ """
+
+ def apply(self, action):
+ """
+ Execute a set of commands, returning a list of commands that were executed.
+
+ Arguments:
+ action -- a dict of command to attributes, where attributes has keys of:
+ command: the command to run (a string or list)
+ cwd: working directory (a string)
+ env: a dictionary of environment variables
+ test: a commmand to run; if it returns zero, the command will run
+ ignoreErrors: if true, ignore errors
+ waitAfterCompletion: # of seconds to wait after completion (or "forever")
+ defaults: a command to run; the stdout will be used to provide defaults
+
+ Exceptions:
+ ToolError -- on expected failures
+ """
+
+ commands_run = []
+
+ if not action:
+ log.debug("No commands specified")
+ return commands_run
+
+ for name in sorted(action.keys()):
+ log.debug("Running command %s", name)
+
+ attributes = action[name]
+
+ if "defaults" in attributes:
+ log.debug("Generating defaults for command %s", name)
+ defaultsResult = ProcessHelper(attributes['defaults'], stderr=subprocess.PIPE).call()
+ log.debug("Defaults script for %s output: %s", name, defaultsResult.stdout)
+ if defaultsResult.returncode:
+ log.error("Defaults script failed for %s: %s", name, defaultsResult.stderr)
+ raise ToolError("Defaults script for command %s failed" % name)
+
+ old_attrs = attributes
+ attributes = json.loads(defaultsResult.stdout)
+ attributes.update(old_attrs)
+
+ if not "command" in attributes:
+ log.error("No command specified for %s", name)
+ raise ToolError("%s does not specify the 'command' attribute, which is required" % name)
+
+ cwd = os.path.expanduser(attributes["cwd"]) if "cwd" in attributes else None
+ env = attributes.get("env", None)
+
+ if "test" in attributes:
+ log.debug("Running test for command %s", name)
+ test = attributes["test"]
+ testResult = ProcessHelper(test, env=env, cwd=cwd).call()
+ log.debug("Test command output: %s", testResult.stdout)
+ if testResult.returncode:
+ log.info("Test failed with code %s", testResult.returncode)
+ continue
+ else:
+ log.debug("Test for command %s passed", name)
+ else:
+ log.debug("No test for command %s", name)
+
+ cmd_to_run = attributes["command"]
+ if "runas" in attributes:
+ if os.name == 'nt':
+ raise ToolError('Command %s specified "runas", which is not supported on Windows' % name)
+
+ if isinstance(cmd_to_run, basestring):
+ cmd_to_run = 'su %s -c %s' % (attributes['runas'], cmd_to_run)
+ else:
+ cmd_to_run = ['su', attributes['runas'], '-c'] + cmd_to_run
+
+ commandResult = ProcessHelper(cmd_to_run, env=env, cwd=cwd).call()
+
+ if commandResult.returncode:
+ log.error("Command %s (%s) failed", name, attributes["command"])
+ log.debug("Command %s output: %s", name, commandResult.stdout)
+ if interpret_boolean(attributes.get("ignoreErrors")):
+ log.info("ignoreErrors set to true, continuing build")
+ commands_run.append(name)
+ else:
+ raise ToolError("Command %s failed" % name)
+ else:
+ log.info("Command %s succeeded", name)
+ log.debug("Command %s output: %s", name, commandResult.stdout)
+ commands_run.append(name)
+
+ return commands_run
+
+ @classmethod
+ def get_wait(cls, cmd_options):
+ wait = cmd_options.get('waitAfterCompletion', 60 if os.name == 'nt' else 0)
+ if isinstance(wait, basestring) and 'forever' == wait.lower():
+ return -1
+ return int(wait)
524 cfnbootstrap/construction.py
@@ -0,0 +1,524 @@
+#==============================================================================
+# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# 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.
+#==============================================================================
+"""
+A library for building an installation from metadata
+
+Classes:
+Contractor - orchestrates the build process
+Carpenter - does the concrete work of applying metadata to the installation
+Tool - performs a specific task on an installation
+ToolError - a base exception type for all tools
+
+CloudFormationCarpenter - Orchestrates a non-delegated installation
+YumTool - installs packages via yum
+
+"""
+from cfnbootstrap import platform_utils
+from cfnbootstrap.apt_tool import AptTool
+from cfnbootstrap.auth import AuthenticationConfig
+from cfnbootstrap.command_tool import CommandTool
+from cfnbootstrap.construction_errors import BuildError, NoSuchConfigSetError, \
+ NoSuchConfigurationError, CircularConfigSetDependencyError
+from cfnbootstrap.file_tool import FileTool
+from cfnbootstrap.lang_package_tools import PythonTool, GemTool
+from cfnbootstrap.msi_tool import MsiTool
+from cfnbootstrap.rpm_tools import RpmTool, YumTool
+from cfnbootstrap.service_tools import SysVInitTool, WindowsServiceTool
+from cfnbootstrap.sources_tool import SourcesTool
+from cfnbootstrap.user_group_tools import GroupTool, UserTool
+import collections
+import contextlib
+import logging
+import operator
+import os.path
+import shelve
+import sys
+import time
+
+log = logging.getLogger("cfn.init")
+
+class WorkLog(object):
+ """
+ Keeps track of pending work, and can resume from the last known point
+ Useful for commands that cause restarts
+ """
+
+ def __init__(self, dbname='resume_db'):
+ if os.name == 'nt':
+ self._shelf_dir = os.path.expandvars(r'${SystemDrive}\cfn\cfn-init')
+ else:
+ self._shelf_dir = '/var/lib/cfn-init'
+
+ if not os.path.isdir(self._shelf_dir) and not os.path.exists(self._shelf_dir):
+ os.makedirs(self._shelf_dir)
+
+ if not os.path.isdir(self._shelf_dir):
+ print >> sys.stderr, "Could not create %s to store the work log" % self._shelf_dir
+ logging.error("Could not create %s to store the work log", self._shelf_dir)
+
+ self._dbname = dbname
+
+ def clear(self):
+ with contextlib.closing(shelve.open(os.path.join(self._shelf_dir, self._dbname))) as shelf:
+ shelf.clear()
+
+ def clear_except_metadata(self):
+ with contextlib.closing(shelve.open(os.path.join(self._shelf_dir, self._dbname))) as shelf:
+ metadata = shelf.get('metadata')
+ shelf.clear()
+ if metadata:
+ shelf['metadata'] = metadata
+
+ def put(self, key, data):
+ with contextlib.closing(shelve.open(os.path.join(self._shelf_dir, self._dbname))) as shelf:
+ if data:
+ shelf[key] = data
+ elif key in shelf:
+ del shelf[key]
+
+ def has_key(self, key):
+ with contextlib.closing(shelve.open(os.path.join(self._shelf_dir, self._dbname))) as shelf:
+ return key in shelf
+
+ def get(self, key, default=None):
+ with contextlib.closing(shelve.open(os.path.join(self._shelf_dir, self._dbname))) as shelf:
+ return shelf.get(key, default)
+
+ def delete(self, key):
+ with contextlib.closing(shelve.open(os.path.join(self._shelf_dir, self._dbname))) as shelf:
+ del shelf[key]
+
+ def pop(self, key):
+ with contextlib.closing(shelve.open(os.path.join(self._shelf_dir, self._dbname))) as shelf:
+ value = shelf[key]
+ ret_val = value.popleft()
+ if not value:
+ del shelf[key]
+ else:
+ shelf[key] = value
+ return ret_val
+
+ def build(self, metadata, configSets):
+ self.put('metadata', metadata)
+ platform_utils.set_reboot_trigger()
+ Contractor(metadata).build(configSets, self)
+
+ def run_commands(self):
+ cmd_tool = CommandTool()
+ while self.has_key('commands'):
+ next_cmd = self.pop('commands')
+ changes = self.get('changes', collections.defaultdict(list))
+ cmd_options = next_cmd[1]
+ command_changes = cmd_tool.apply({next_cmd[0]:cmd_options})
+ changes['commands'].extend(command_changes)
+ self.put('changes', changes)
+ if not command_changes:
+ log.info("Not waiting as command did not execute")
+ else:
+ wait = CommandTool.get_wait(cmd_options)
+ if wait < 0:
+ log.info("Waiting indefinitely for command to reboot")
+ sys.exit(0)
+ elif wait > 0:
+ log.info("Waiting %s seconds for reboot", wait)
+ time.sleep(wait)
+
+ for manager, services in self.get('services', {}).iteritems():
+ if manager in CloudFormationCarpenter._serviceTools:
+ CloudFormationCarpenter._serviceTools[manager]().apply(services, self.get('changes', collections.defaultdict(list)))
+ else:
+ log.warn("Unsupported service manager: %s", manager)
+
+ if self.has_key('changes'):
+ self.delete('changes')
+
+ if self.has_key('services'):
+ self.delete('services')
+
+ def resume(self):
+ log.debug("Starting resume")
+ platform_utils.set_reboot_trigger()
+
+ self.run_commands()
+
+ contractor = Contractor(self.get('metadata'))
+
+ #TODO: apply services when supported by Windows
+
+ while self.has_key('configs'):
+ next_config = self.pop('configs')
+ log.debug("Resuming config: %s", next_config.name)
+ contractor.run_config(next_config, self)
+
+ if self.has_key('configSets'):
+ remaining_sets = self.get('configSets')
+ log.debug("Resuming configSets: %s", remaining_sets)
+ contractor.build(remaining_sets, self)
+ else:
+ self.clear()
+ platform_utils.clear_reboot_trigger()
+
+ log.debug("Resume completed")
+
+
+class CloudFormationCarpenter(object):
+ """
+ Takes a model and uses tools to make it reality
+ """
+
+ _packageTools = { "yum" : YumTool,
+ "rubygems" : GemTool,
+ "python" : PythonTool,
+ "rpm" : RpmTool,
+ "apt" : AptTool,
+ "msi" : MsiTool }
+
+ _pkgOrder = ["msi", "dpkg", "rpm", "apt", "yum"]
+
+ _serviceTools = { "sysvinit" : SysVInitTool, "windows" : WindowsServiceTool }
+
+ @staticmethod
+ def _pkgsort(x, y):
+ order = CloudFormationCarpenter._pkgOrder
+ if x[0] in order and y[0] in order:
+ return cmp(order.index(x[0]), order.index(y[0]))
+ elif x[0] in order:
+ return -1
+ elif y[0] in order:
+ return 1
+ else:
+ return cmp(x[0].lower(), y[0].lower())
+
+ def __init__(self, config, auth_config):
+ self._config = config
+ self._auth_config = auth_config
+
+ def build(self, worklog):
+ changes = collections.defaultdict(list)
+
+ changes['packages'] = collections.defaultdict(list)
+ if self._config.packages:
+ for manager, packages in sorted(self._config.packages.iteritems(), cmp=CloudFormationCarpenter._pkgsort):
+ if manager in CloudFormationCarpenter._packageTools:
+ changes['packages'][manager] = CloudFormationCarpenter._packageTools[manager]().apply(packages, self._auth_config)
+ else:
+ log.warn('Unsupported package manager: %s', manager)
+ else:
+ log.debug("No packages specified")
+
+ if self._config.groups:
+ changes['groups'] = GroupTool().apply(self._config.groups)
+ else:
+ log.debug("No groups specified")
+
+ if self._config.users:
+ changes['users'] = UserTool().apply(self._config.users)
+ else:
+ log.debug("No users specified")
+
+ if self._config.sources:
+ changes['sources'] = SourcesTool().apply(self._config.sources, self._auth_config)
+ else:
+ log.debug("No sources specified")
+
+ if self._config.files:
+ changes['files'] = FileTool().apply(self._config.files, self._auth_config)
+ else:
+ log.debug("No files specified")
+
+ if self._config.commands:
+ if os.name=='nt':
+ worklog.put('changes', changes)
+ worklog.put('commands', collections.deque(sorted(self._config.commands.iteritems(), key=operator.itemgetter(0))))
+ else:
+ changes['commands'] = CommandTool().apply(self._config.commands)
+ else:
+ log.debug("No commands specified")
+
+ if self._config.services:
+ if os.name=='nt':
+ worklog.put('services', self._config.services)
+ else:
+ for manager, services in self._config.services.iteritems():
+ if manager in CloudFormationCarpenter._serviceTools:
+ CloudFormationCarpenter._serviceTools[manager]().apply(services, changes)
+ else:
+ log.warn("Unsupported service manager: %s", manager)
+ else:
+ log.debug("No services specified")
+
+class ConfigDefinition(object):
+ """
+ Encapsulates one config definition
+ """
+
+ def __init__(self, name, model):
+ self._name = name
+ self._files = model.get("files")
+ self._packages = model.get("packages")
+ self._services = model.get("services")
+ self._sources = model.get("sources")
+ self._commands = model.get("commands")
+ self._users = model.get("users")
+ self._groups = model.get("groups")
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def files(self):
+ return self._files
+
+ @property
+ def packages(self):
+ return self._packages
+
+ @property
+ def services(self):
+ return self._services
+
+ @property
+ def sources(self):
+ return self._sources
+
+ @property
+ def commands(self):
+ return self._commands
+
+ @property
+ def users(self):
+ return self._users
+
+ @property
+ def groups(self):
+ return self._groups
+
+ def __str__(self):
+ return 'Config(%s)' % self._name
+
+
+class ConfigSetRef(object):
+ """
+ Encapsulates a ref to a ConfigSet
+ """
+
+ def __init__(self, name):
+ self._name = name
+
+ @property
+ def name(self):
+ return self._name
+
+ def __str__(self):
+ return 'ConfigSet(%s)' % self._name
+
+class ConfigSet(object):
+ """
+ A list of ConfigDefinition or ConfigSetRef objects with their dependencies
+ """
+
+ def __init__(self, configDef=None):
+ """
+ Arguments:
+ configDef - optional ConfigDefinition|ConfigSetRef to initialize this list with (handy for 1-member lists)
+ """
+ self._defs = [] if not configDef else [configDef]
+ self._dependencies = set() if (not configDef or isinstance(configDef, ConfigDefinition)) else set([configDef.name])
+
+ def addConfigDef(self, configDef):
+ if isinstance(configDef, ConfigSetRef):
+ self._dependencies.add(configDef.name)
+ self._defs.append(configDef)
+
+ def extend(self, configDefList):
+ for cd in configDefList.configDefs:
+ self.addConfigDef(cd)
+
+ @property
+ def dependencies(self):
+ return self._dependencies
+
+ @property
+ def configDefs(self):
+ return self._defs
+
+ def __str__(self):
+ return 'ConfigSet of: %s' % ','.join(self._defs)
+
+class Contractor(object):
+ """
+ Take in a metadata model and force the environment to match it, returning nothing.
+
+ Processes configSets if they exist; otherwise, invents a virtual configSet named
+ "default" with one config of "config"
+
+ """
+
+ _configKey = "AWS::CloudFormation::Init"
+ _authKey = "AWS::CloudFormation::Authentication"
+ _configSetsKey = "configSets"
+
+ def __init__(self, model):
+ initModel = model.get(Contractor._configKey)
+ if not initModel:
+ raise ValueError("Metadata does not contain '%s'" % Contractor._configKey)
+
+ if not Contractor._configSetsKey in initModel:
+ self._configSets = { 'default' : [ConfigDefinition("config", initModel.get("config", dict()))]}
+ else:
+ configSetsDef = initModel[Contractor._configSetsKey]
+ if not isinstance(configSetsDef, dict):
+ raise ValueError("%s should be a mapping of name to list" % Contractor._configSetsKey)
+
+ self._processConfigSetsDefinition(configSetsDef, initModel)
+
+ self._auth_config = AuthenticationConfig(model.get(Contractor._authKey, {}))
+
+ def _processConfigSetsDefinition(self, configSetsDef, model):
+ """
+ Parse a set of configSets from the model and collapse them, validating there are no cycles
+ and that all references are valid.
+ """
+
+ # This builds both a map of the uncollapsed config sets
+ # as well as a lookup and reverse lookup table
+ # so we can traverse the graph and detect cycles
+ # in a not-terrible time
+
+ rawConfigSets = {}
+ dependencyTree = {} # maps configSets to the configSets they depend on
+ reverseDependencyTree = collections.defaultdict(set) # maps configSets to the configSets that depend on them
+ roots = set() # the roots of the configSets graph -- configSets without dependencies
+ for configSetName, configList in configSetsDef.iteritems():
+ processedList = self._processConfigList(configList, model)
+ if processedList.dependencies:
+ dependencyTree[configSetName] = set(processedList.dependencies)
+ for dependency in processedList.dependencies:
+ reverseDependencyTree[dependency].add(configSetName)
+ else:
+ roots.add(configSetName)
+
+ rawConfigSets[configSetName] = list(processedList.configDefs)
+
+ if not roots:
+ raise CircularConfigSetDependencyError("No configSets exist without references; this creates a circular dependency and is not allowed")
+
+ self._configSets = {}
+ # use a traditional (Kahn) topological sort to traverse the configSets in dependency order
+ # http://en.wikipedia.org/wiki/Topological_sort#Algorithms has a nice description
+ while roots:
+ configSet = roots.pop()
+ self._configSets[configSet] = self._collapse(configSet, rawConfigSets[configSet])
+ for dependent in reverseDependencyTree.pop(configSet, []):
+ dependencyTree[dependent].remove(configSet)
+ if not dependencyTree[dependent]:
+ roots.add(dependent)
+ del dependencyTree[dependent]
+
+ if dependencyTree:
+ raise CircularConfigSetDependencyError("At least one circular dependency detected; this is not allowed. Culprits: " + ', '.join(dependencyTree.keys()))
+
+
+ def _collapse(self, configSetName, configList):
+ """
+ Transform ConfigSetRefs into the contents of the ConfigSets they reference, returning a list of only ConfigDefinition objects
+ """
+ returnList = []
+
+ for config in configList:
+ if isinstance(config, ConfigDefinition):
+ returnList.append(config)
+ else:
+ if not config.name in self._configSets:
+ raise ValueError("ConfigSet %s referenced ConfigSet %s but it is not defined" % (configSetName, config.name))
+ returnList.extend(self._configSets[config.name])
+
+ return returnList
+
+ def _processConfigList(self, configList, model):
+ """
+ Processes a parsed-JSON list of config definitions, returning a ConfigSet
+
+ Handles both references ({"ConfigSet" : "name"}) and plain config names
+ so users can define simple ConfigSets without using lists, and so we can recurse simply
+ """
+
+ if isinstance(configList, basestring):
+ if not configList in model:
+ raise NoSuchConfigurationError("No configuration found with name: %s" % configList)
+ return ConfigSet(ConfigDefinition(configList, model[configList]))
+
+ if isinstance(configList, dict):
+ if not 'ConfigSet' in configList:
+ raise ValueError("Config definitions must be either a config name or a reference in the format {'ConfigSet':<config set name>}")
+ setName = configList['ConfigSet']
+ if not setName in model[Contractor._configSetsKey]:
+ raise ValueError("Configuration set %s was referenced but not defined" % setName)
+ return ConfigSet(ConfigSetRef(setName))
+
+ returnSet = ConfigSet()
+ for configDef in configList:
+ returnSet.extend(self._processConfigList(configDef, model))
+
+ return returnSet
+
+
+ def build(self, configSets, worklog):