0235a734e4
The current shebang requires /usr/bin/python which is not available in Ubuntu Jammy by default. Change-Id: I142472eb20591fc752db9ca06c954d362cd3e405
218 lines
7.4 KiB
Python
Executable File
218 lines
7.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# 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 sys
|
|
|
|
import yaml
|
|
|
|
# legacy groups that have never had a hook script
|
|
WHITELISTED_MISSING_HOOK_SCRIPTS = ['os-apply-config']
|
|
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 humanize(data):
|
|
# reformat a json string with multi-line values into a human readable yaml
|
|
# dump. if conversion fails, it will fallback to original string.
|
|
try:
|
|
return yaml.safe_dump(data,
|
|
allow_unicode=True,
|
|
default_flow_style=False,
|
|
canonical=False,
|
|
default_style="|")
|
|
except Exception:
|
|
return data
|
|
|
|
|
|
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'], str):
|
|
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:
|
|
if not c['group'] in WHITELISTED_MISSING_HOOK_SCRIPTS:
|
|
log.error('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, separators=(',', ': '))
|
|
|
|
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'))
|
|
|
|
log.info(humanize(stdout))
|
|
log.debug(stderr)
|
|
|
|
if subproc.returncode:
|
|
log.error("Error running %s. [%s]\n" % (
|
|
hook_path, subproc.returncode))
|
|
signal_data = {
|
|
'deploy_stdout': stdout.decode("utf-8", "replace"),
|
|
'deploy_stderr': stderr.decode("utf-8", "replace"),
|
|
'deploy_status_code': 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.decode("utf-8", "replace"),
|
|
'deploy_stderr': stderr.decode("utf-8", "replace"),
|
|
'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, separators=(',', ': '))
|
|
|
|
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))
|