#!/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 shutil import stat import subprocess import six import sys import requests HOOKS_DIR_PATHS = ( os.environ.get('HEAT_CONFIG_HOOKS'), '/usr/libexec/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/lib/heat-config/deployed') OLD_DEPLOYED_DIR = os.environ.get('HEAT_CONFIG_DEPLOYED_OLD', '/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 conf_mode = stat.S_IMODE(os.lstat(CONF_FILE).st_mode) if conf_mode != 0o600: os.chmod(CONF_FILE, 0o600) if not os.path.isdir(DEPLOYED_DIR): if DEPLOYED_DIR != OLD_DEPLOYED_DIR and os.path.isdir(OLD_DEPLOYED_DIR): log.debug('Migrating deployed state from %s to %s' % (OLD_DEPLOYED_DIR, DEPLOYED_DIR)) shutil.move(OLD_DEPLOYED_DIR, DEPLOYED_DIR) else: 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 find_hook_path(group): # sanitise the group to get an alphanumeric hook file name hook = "".join( x for x in group if x == '-' or x == '_' or x.isalnum()) for h in HOOKS_DIR_PATHS: if not h or not os.path.exists(h): continue hook_path = os.path.join(h, hook) if os.path.exists(hook_path): return hook_path 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'], six.string_types): 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 signal_data = {} hook_path = find_hook_path(c['group']) if not 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).encode('utf-8', 'replace')) 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.decode('utf-8', 'replace')) except ValueError: signal_data = { 'deploy_stdout': stdout, 'deploy_stderr': stderr, 'deploy_status_code': subproc.returncode, } for i in signal_data.items(): log.info('%s\n%s' % i) log.debug(stderr.decode('utf-8', 'replace')) 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).encode('utf-8', 'replace')) 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))