heat_template_version: 2014-10-16 resources: install_base: type: "OS::Heat::CloudConfig" properties: cloud_config: package_update: true apt_upgrade: true packages: - python-pip - git - gcc - python-dev - libyaml-dev - libssl-dev - libffi-dev - libxml2-dev - libxslt1-dev install_agents: type: "OS::Heat::SoftwareConfig" properties: group: ungrouped config: | #!/bin/bash set -eux pip install os-collect-config os-apply-config os-refresh-config dib-utils heat-cfntools wrapt monotonic pytz funcsigs positional cfn-create-aws-symlinks configure: type: "OS::Heat::CloudConfig" properties: cloud_config: write_files: - path: /etc/os-collect-config.conf content: | [DEFAULT] command = os-refresh-config - path: /usr/libexec/os-apply-config/templates/etc/os-collect-config.conf content: | [DEFAULT] {{^os-collect-config.command}} command = os-refresh-config {{/os-collect-config.command}} {{#os-collect-config}} {{#command}} command = {{command}} {{/command}} {{#polling_interval}} polling_interval = {{polling_interval}} {{/polling_interval}} {{#cachedir}} cachedir = {{cachedir}} {{/cachedir}} {{#collectors}} collectors = {{.}} {{/collectors}} {{#cfn}} [cfn] {{#metadata_url}} metadata_url = {{metadata_url}} {{/metadata_url}} stack_name = {{stack_name}} secret_access_key = {{secret_access_key}} access_key_id = {{access_key_id}} path = {{path}} {{/cfn}} {{#heat}} [heat] auth_url = {{auth_url}} user_id = {{user_id}} password = {{password}} project_id = {{project_id}} stack_id = {{stack_id}} resource_name = {{resource_name}} {{/heat}} {{#request}} [request] {{#metadata_url}} metadata_url = {{metadata_url}} {{/metadata_url}} {{/request}} {{/os-collect-config}} - path: /usr/libexec/os-apply-config/templates/var/run/heat-config/heat-config content: | {{deployments}} - path: /opt/stack/os-config-refresh/configure.d/20-os-apply-config permissions: "0700" content: | #!/bin/bash set -ue exec os-apply-config - path: /opt/stack/os-config-refresh/configure.d/55-heat-config permissions: "0700" content: | #!/usr/bin/env python # # 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 json import logging import os import subprocess import sys import requests HOOKS_DIR = os.environ.get('HEAT_CONFIG_HOOKS', '/var/lib/heat-config/hooks') CONF_FILE = os.environ.get('HEAT_SHELL_CONFIG', '/var/run/heat-config/heat-config') DEPLOYED_DIR = os.environ.get('HEAT_CONFIG_DEPLOYED', '/var/run/heat-config/deployed') HEAT_CONFIG_NOTIFY = os.environ.get('HEAT_CONFIG_NOTIFY', 'heat-config-notify') def main(argv=sys.argv): log = logging.getLogger('heat-config') handler = logging.StreamHandler(sys.stderr) handler.setFormatter( logging.Formatter( '[%(asctime)s] (%(name)s) [%(levelname)s] %(message)s')) log.addHandler(handler) log.setLevel('DEBUG') if not os.path.exists(CONF_FILE): log.error('No config file %s' % CONF_FILE) return 1 if not os.path.isdir(DEPLOYED_DIR): os.makedirs(DEPLOYED_DIR, 0o700) try: configs = json.load(open(CONF_FILE)) except ValueError: pass else: for c in configs: try: invoke_hook(c, log) except Exception as e: log.exception(e) def invoke_hook(c, log): # Sanitize input values (bug 1333992). Convert all String # inputs to strings if they're not already hot_inputs = c.get('inputs', []) for hot_input in hot_inputs: if hot_input.get('type', None) == 'String' and \ not isinstance(hot_input['value'], basestring): hot_input['value'] = str(hot_input['value']) iv = dict((i['name'], i['value']) for i in c['inputs']) # The group property indicates whether it is softwarecomponent or # plain softwareconfig # If it is softwarecomponent, pick up a property config to invoke # according to deploy_action group = c.get('group') if group == 'component': found = False action = iv.get('deploy_action') config = c.get('config') configs = config.get('configs') if configs: for cfg in configs: if action in cfg['actions']: c['config'] = cfg['config'] c['group'] = cfg['tool'] found = True break if not found: log.warn('Skipping group %s, no valid script is defined' ' for deploy action %s' % (group, action)) return # check to see if this config is already deployed deployed_path = os.path.join(DEPLOYED_DIR, '%s.json' % c['id']) if os.path.exists(deployed_path): log.warn('Skipping config %s, already deployed' % c['id']) log.warn('To force-deploy, rm %s' % deployed_path) return # sanitise the group to get an alphanumeric hook file name hook = "".join( x for x in c['group'] if x == '-' or x == '_' or x.isalnum()) hook_path = os.path.join(HOOKS_DIR, hook) signal_data = {} if not os.path.exists(hook_path): log.warn('Skipping group %s with no hook script %s' % ( c['group'], hook_path)) return # write out config, which indicates it is deployed regardless of # subsequent hook success with os.fdopen(os.open( deployed_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f: json.dump(c, f, indent=2) log.debug('Running %s < %s' % (hook_path, deployed_path)) subproc = subprocess.Popen([hook_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = subproc.communicate(input=json.dumps(c)) log.info(stdout) log.debug(stderr) if subproc.returncode: log.error("Error running %s. [%s]\n" % ( hook_path, subproc.returncode)) else: log.info('Completed %s' % hook_path) try: if stdout: signal_data = json.loads(stdout) except ValueError: signal_data = { 'deploy_stdout': stdout, 'deploy_stderr': stderr, 'deploy_status_code': subproc.returncode, } signal_data_path = os.path.join(DEPLOYED_DIR, '%s.notify.json' % c['id']) # write out notify data for debugging with os.fdopen(os.open( signal_data_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f: json.dump(signal_data, f, indent=2) log.debug('Running %s %s < %s' % ( HEAT_CONFIG_NOTIFY, deployed_path, signal_data_path)) subproc = subprocess.Popen([HEAT_CONFIG_NOTIFY, deployed_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = subproc.communicate(input=json.dumps(signal_data)) log.info(stdout) if subproc.returncode: log.error( "Error running heat-config-notify. [%s]\n" % subproc.returncode) log.error(stderr) else: log.debug(stderr) if __name__ == '__main__': sys.exit(main(sys.argv)) - path: /var/lib/heat-config/hooks/script permissions: "0755" content: | #!/usr/bin/env python # # 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 json import logging import os import subprocess import sys WORKING_DIR = os.environ.get('HEAT_SCRIPT_WORKING', '/var/lib/heat-config/heat-config-script') OUTPUTS_DIR = os.environ.get('HEAT_SCRIPT_OUTPUTS', '/var/run/heat-config/heat-config-script') def prepare_dir(path): if not os.path.isdir(path): os.makedirs(path, 0o700) def main(argv=sys.argv): log = logging.getLogger('heat-config') handler = logging.StreamHandler(sys.stderr) handler.setFormatter( logging.Formatter( '[%(asctime)s] (%(name)s) [%(levelname)s] %(message)s')) log.addHandler(handler) log.setLevel('DEBUG') prepare_dir(OUTPUTS_DIR) prepare_dir(WORKING_DIR) os.chdir(WORKING_DIR) c = json.load(sys.stdin) env = os.environ.copy() for input in c['inputs']: input_name = input['name'] value = input.get('value', '') if isinstance(value, dict) or isinstance(value, list): env[input_name] = json.dumps(value) else: env[input_name] = value log.info('%s=%s' % (input_name, env[input_name])) fn = os.path.join(WORKING_DIR, c['id']) heat_outputs_path = os.path.join(OUTPUTS_DIR, c['id']) env['heat_outputs_path'] = heat_outputs_path with os.fdopen(os.open(fn, os.O_CREAT | os.O_WRONLY, 0o700), 'w') as f: f.write(c.get('config', '').encode('utf-8')) log.debug('Running %s' % fn) subproc = subprocess.Popen([fn], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) stdout, stderr = subproc.communicate() log.info(stdout) log.debug(stderr) if subproc.returncode: log.error("Error running %s. [%s]\n" % (fn, subproc.returncode)) else: log.info('Completed %s' % fn) response = {} for output in c.get('outputs') or []: output_name = output['name'] try: with open('%s.%s' % (heat_outputs_path, output_name)) as out: response[output_name] = out.read() except IOError: pass response.update({ 'deploy_stdout': stdout, 'deploy_stderr': stderr, 'deploy_status_code': subproc.returncode, }) json.dump(response, sys.stdout) if __name__ == '__main__': sys.exit(main(sys.argv)) - path: /usr/bin/heat-config-notify permissions: "0700" content: | #!/usr/bin/env python # # 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 json import logging import os import sys import requests try: from heatclient import client as heatclient except ImportError: heatclient = None try: from keystoneclient.v3 import client as ksclient except ImportError: ksclient = None def init_logging(): log = logging.getLogger('heat-config-notify') handler = logging.StreamHandler(sys.stderr) handler.setFormatter( logging.Formatter( '[%(asctime)s] (%(name)s) [%(levelname)s] %(message)s')) log.addHandler(handler) log.setLevel('DEBUG') return log def main(argv=sys.argv, stdin=sys.stdin): log = init_logging() usage = ('Usage:\n heat-config-notify /path/to/config.json ' '< /path/to/signal_data.json') if len(argv) < 2: log.error(usage) return 1 try: signal_data = json.load(stdin) except ValueError: log.warn('No valid json found on stdin') signal_data = {} conf_file = argv[1] if not os.path.exists(conf_file): log.error('No config file %s' % conf_file) log.error(usage) return 1 c = json.load(open(conf_file)) iv = dict((i['name'], i['value']) for i in c['inputs']) if 'deploy_signal_id' in iv: sigurl = iv.get('deploy_signal_id') sigverb = iv.get('deploy_signal_verb', 'POST') signal_data = json.dumps(signal_data) log.debug('Signaling to %s via %s' % (sigurl, sigverb)) if sigverb == 'PUT': r = requests.put(sigurl, data=signal_data, headers={'content-type': None}) else: r = requests.post(sigurl, data=signal_data, headers={'content-type': None}) log.debug('Response %s ' % r) if 'deploy_auth_url' in iv: ks = ksclient.Client( auth_url=iv['deploy_auth_url'], user_id=iv['deploy_user_id'], password=iv['deploy_password'], project_id=iv['deploy_project_id']) endpoint = ks.service_catalog.url_for( service_type='orchestration', endpoint_type='publicURL') log.debug('Signalling to %s' % endpoint) heat = heatclient.Client( '1', endpoint, token=ks.auth_token) r = heat.resources.signal( iv.get('deploy_stack_id'), iv.get('deploy_resource_name'), data=signal_data) log.debug('Response %s ' % r) return 0 if __name__ == '__main__': sys.exit(main(sys.argv, sys.stdin)) runcmd: - os-collect-config --one-time --debug - cat /etc/os-collect-config.conf - os-collect-config --one-time --debug start: type: "OS::Heat::SoftwareConfig" properties: group: ungrouped config: | #!/bin/bash set -eux if [[ `systemctl` =~ -\.mount ]]; then # if there is no system unit file, install a local unit if [ ! -f /usr/lib/systemd/system/os-collect-config.service ]; then cat </etc/systemd/system/os-collect-config.service [Unit] Description=Collect metadata and run hook commands. [Service] ExecStart=/usr/bin/os-collect-config Restart=on-failure [Install] WantedBy=multi-user.target EOF cat </etc/os-collect-config.conf [DEFAULT] command=os-refresh-config EOF fi # enable and start service to poll for deployment changes systemctl enable os-collect-config systemctl start --no-block os-collect-config elif [[ `/sbin/init --version` =~ upstart ]]; then if [ ! -f /etc/init/os-collect-config.conf ]; then cat </etc/init/os-collect-config.conf start on runlevel [2345] stop on runlevel [016] respawn # We're logging to syslog console none exec os-collect-config 2>&1 | logger -t os-collect-config EOF fi initctl reload-configuration service os-collect-config start else echo "ERROR: only systemd or upstart supported" 1>&2 exit 1 fi install_config_agent: type: "OS::Heat::MultipartMime" properties: parts: - config: { get_resource: install_base } - config: { get_resource: install_agents } - config: { get_resource: configure } - config: { get_resource: start } outputs: config: value: { get_resource: install_config_agent }