When https://review.opendev.org/c/openstack/python-tripleoclient/+/775302 merged, it changed the function signature of utils.archive_deploy_artifacts, but the tripleo deploy code was not updated to pass the right arguments. This caused the templates directory to be considered the working directory and the artifact tarball was generated in that directory, and then promptly deleted during the cleanup phase. This patch updates tripleo deploy to pass the right arguments to archive_deploy_artifacts, and also use a consistent working directory based on the stack name under ~/tripleo-deploy instead of defaulting to the home dir. This is a cleaner approach than just leaving all the files in the home dir, especially given that tripleo deploy can deploy multiple stacks, and if the same directory was used as the default, there would be file collisions. Closes-Bug: #1921975 Signed-off-by: James Slagle <jslagle@redhat.com> Change-Id: Idded7faba1ff6c811b94503c559029aeeaca6a06
422 lines
16 KiB
Python
422 lines
16 KiB
Python
# Copyright 2019 Red Hat, Inc.
|
|
#
|
|
# 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.
|
|
#
|
|
|
|
"""Plugin action implementation"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
from cryptography import x509
|
|
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import serialization
|
|
|
|
from jinja2 import Environment
|
|
from jinja2 import FileSystemLoader
|
|
|
|
from osc_lib.i18n import _
|
|
from oslo_config import cfg
|
|
from tripleo_common.image import kolla_builder
|
|
|
|
from tripleoclient.config.minion import load_global_config
|
|
from tripleoclient.config.minion import MinionConfig
|
|
from tripleoclient import constants
|
|
from tripleoclient import exceptions
|
|
from tripleoclient import utils
|
|
from tripleoclient.v1 import undercloud_preflight
|
|
|
|
|
|
# Provides mappings for some of the instack_env tags to minion heat
|
|
# params or minion.conf opts known here (as a fallback), needed to maintain
|
|
# feature parity with instack net config override templates.
|
|
# TODO(bogdando): all of the needed mappings should be wired-in, eventually
|
|
# NOTE(aschultz): this is used by the custom netconfig still, even though
|
|
# the minion config is new.
|
|
INSTACK_NETCONF_MAPPING = {
|
|
'LOCAL_INTERFACE': 'minion_local_interface',
|
|
'LOCAL_IP': 'minion_local_ip',
|
|
'LOCAL_MTU': 'UndercloudMinionLocalMtu',
|
|
'PUBLIC_INTERFACE_IP': 'minion_local_ip', # can't be 'CloudName'
|
|
'UNDERCLOUD_NAMESERVERS': 'minion_nameservers',
|
|
'SUBNETS_STATIC_ROUTES': 'ControlPlaneStaticRoutes',
|
|
}
|
|
|
|
|
|
PARAMETER_MAPPING = {
|
|
'minion_debug': 'Debug',
|
|
'minion_local_mtu': 'UndercloudMinionLocalMtu',
|
|
'container_healthcheck_disabled': 'ContainerHealthcheckDisabled',
|
|
'minion_local_interface': 'NeutronPublicInterface',
|
|
}
|
|
|
|
SUBNET_PARAMETER_MAPPING = {
|
|
'cidr': 'NetworkCidr',
|
|
'gateway': 'NetworkGateway',
|
|
'host_routes': 'HostRoutes'
|
|
}
|
|
|
|
THT_HOME = os.environ.get('THT_HOME',
|
|
"/usr/share/openstack-tripleo-heat-templates/")
|
|
|
|
USER_HOME = os.environ.get('HOME', '')
|
|
|
|
CONF = cfg.CONF
|
|
|
|
# When adding new options to the lists below, make sure to regenerate the
|
|
# sample config by running "tox -e genconfig" in the project root.
|
|
ci_defaults = kolla_builder.container_images_prepare_defaults()
|
|
|
|
config = MinionConfig()
|
|
|
|
# Routed subnets
|
|
_opts = config.get_opts()
|
|
load_global_config()
|
|
|
|
|
|
LOG = logging.getLogger(__name__ + ".minion_config")
|
|
|
|
|
|
def _get_jinja_env_source(f):
|
|
path, filename = os.path.split(f)
|
|
env = Environment(loader=FileSystemLoader(path))
|
|
src = env.loader.get_source(env, filename)[0]
|
|
return (env, src)
|
|
|
|
|
|
def _process_undercloud_output(templates_dir, output_file_path):
|
|
"""copy the undercloud output file to our work dir"""
|
|
output_file = os.path.join(constants.MINION_OUTPUT_DIR, output_file_path)
|
|
env_file = os.path.join(templates_dir, 'tripleo-undercloud-base.yaml')
|
|
if os.path.exists(output_file):
|
|
src_file = output_file
|
|
elif os.path.exists(output_file_path):
|
|
src_file = output_file_path
|
|
else:
|
|
raise exceptions.DeploymentError('Cannot locate undercloud output '
|
|
'file {}'.format(output_file_path))
|
|
|
|
try:
|
|
shutil.copy(os.path.abspath(src_file), env_file)
|
|
except Exception:
|
|
msg = _('Cannot copy undercloud output file %s into a '
|
|
'tempdir!') % src_file
|
|
LOG.error(msg)
|
|
raise exceptions.DeploymentError(msg)
|
|
return env_file
|
|
|
|
|
|
def _process_undercloud_passwords(src_file, dest_file):
|
|
try:
|
|
shutil.copy(os.path.abspath(src_file), dest_file)
|
|
except Exception:
|
|
msg = _('Cannot copy undercloud password file %(src)s to '
|
|
'%(dest)s') % {'src': src_file, 'dest': dest_file}
|
|
LOG.error(msg)
|
|
raise exceptions.DeploymentError(msg)
|
|
|
|
|
|
def prepare_minion_deploy(upgrade=False, no_validations=False,
|
|
verbose_level=1, yes=False,
|
|
force_stack_update=False, dry_run=False):
|
|
"""Prepare Minion deploy command based on minion.conf"""
|
|
|
|
env_data = {}
|
|
registry_overwrites = {}
|
|
deploy_args = []
|
|
# Fetch configuration and use its log file param to add logging to a file
|
|
utils.load_config(CONF, constants.MINION_CONF_PATH)
|
|
utils.configure_logging(LOG, verbose_level, CONF['minion_log_file'])
|
|
|
|
# NOTE(bogdando): the generated env files are stored another path then
|
|
# picked up later.
|
|
# NOTE(aschultz): We copy this into the tht root that we save because
|
|
# we move any user provided environment files into this root later.
|
|
if not CONF.get('output_dir'):
|
|
output_dir = os.path.join(constants.MINION_OUTPUT_DIR,
|
|
'tripleo-deploy',
|
|
'minion')
|
|
else:
|
|
output_dir = CONF['output_dir']
|
|
tempdir = os.path.join(os.path.abspath(output_dir),
|
|
'tripleo-config-generated-env-files')
|
|
utils.makedirs(tempdir)
|
|
|
|
env_data['PythonInterpreter'] = sys.executable
|
|
|
|
env_data['ContainerImagePrepareDebug'] = CONF['minion_debug']
|
|
|
|
for param_key, param_value in PARAMETER_MAPPING.items():
|
|
if param_key in CONF.keys():
|
|
env_data[param_value] = CONF[param_key]
|
|
|
|
# Parse the minion.conf options to include necessary args and
|
|
# yaml files for minion deploy command
|
|
|
|
if CONF.get('minion_enable_selinux'):
|
|
env_data['SELinuxMode'] = 'enforcing'
|
|
else:
|
|
env_data['SELinuxMode'] = 'permissive'
|
|
|
|
if CONF.get('minion_ntp_servers', None):
|
|
env_data['NtpServer'] = CONF['minion_ntp_servers']
|
|
|
|
if CONF.get('minion_timezone', None):
|
|
env_data['TimeZone'] = CONF['minion_timezone']
|
|
else:
|
|
env_data['TimeZone'] = utils.get_local_timezone()
|
|
|
|
# TODO(aschultz): fix this logic, look it up out of undercloud-outputs.yaml
|
|
env_data['DockerInsecureRegistryAddress'] = [
|
|
'%s:8787' % CONF['minion_local_ip'].split('/')[0]]
|
|
env_data['DockerInsecureRegistryAddress'].extend(
|
|
CONF['container_insecure_registries'])
|
|
|
|
env_data['ContainerCli'] = CONF['container_cli']
|
|
|
|
if CONF.get('container_registry_mirror', None):
|
|
env_data['DockerRegistryMirror'] = CONF['container_registry_mirror']
|
|
|
|
# This parameter the IP address used to bind the local container registry
|
|
env_data['LocalContainerRegistry'] = CONF['minion_local_ip'].split('/')[0]
|
|
|
|
if CONF.get('minion_local_ip', None):
|
|
deploy_args.append('--local-ip=%s' % CONF['minion_local_ip'])
|
|
|
|
if CONF.get('templates', None):
|
|
tht_templates = CONF['templates']
|
|
deploy_args.append('--templates=%s' % tht_templates)
|
|
else:
|
|
tht_templates = THT_HOME
|
|
deploy_args.append('--templates=%s' % THT_HOME)
|
|
|
|
if CONF.get('roles_file', constants.MINION_ROLES_FILE):
|
|
deploy_args.append('--roles-file=%s' % CONF['roles_file'])
|
|
|
|
if CONF.get('networks_file'):
|
|
deploy_args.append('--networks-file=%s' % CONF['networks_file'])
|
|
else:
|
|
deploy_args.append('--networks-file=%s' %
|
|
os.path.join(tht_templates,
|
|
constants.UNDERCLOUD_NETWORKS_FILE))
|
|
|
|
if yes:
|
|
deploy_args += ['-y']
|
|
|
|
# copy the undercloud output file into our working dir and include it
|
|
output_file = _process_undercloud_output(
|
|
tempdir, CONF['minion_undercloud_output_file'])
|
|
deploy_args += ['-e', output_file]
|
|
|
|
# copy undercloud password file (the configuration is minion_password_file
|
|
# to the place that triple deploy looks for it
|
|
# tripleo-<stack name>-passwords.yaml)
|
|
_process_undercloud_passwords(
|
|
CONF['minion_password_file'],
|
|
os.path.join(output_dir, 'tripleo-minion-passwords.yaml'))
|
|
|
|
if upgrade:
|
|
# TODO(aschultz): validate minion upgrade, should be the same as the
|
|
# undercloud one.
|
|
deploy_args += [
|
|
'--upgrade',
|
|
'-e', os.path.join(
|
|
tht_templates,
|
|
"environments/lifecycle/undercloud-upgrade-prepare.yaml")]
|
|
|
|
if not CONF.get('heat_native', False):
|
|
deploy_args.append('--heat-native=False')
|
|
else:
|
|
deploy_args.append('--heat-native')
|
|
|
|
if CONF.get('heat_container_image'):
|
|
deploy_args.append('--heat-container-image=%s'
|
|
% CONF['heat_container_image'])
|
|
|
|
# These should be loaded first so we can override all the bits later
|
|
deploy_args += [
|
|
"-e", os.path.join(tht_templates,
|
|
'environments/undercloud/undercloud-minion.yaml'),
|
|
'-e', os.path.join(tht_templates, 'environments/use-dns-for-vips.yaml')
|
|
]
|
|
|
|
# If a container images file is used, copy it into the tempdir to make it
|
|
# later into other deployment artifacts and user-provided files.
|
|
_container_images_config(CONF, deploy_args, env_data, tempdir)
|
|
|
|
if CONF.get('enable_heat_engine'):
|
|
deploy_args += ['-e', os.path.join(
|
|
tht_templates, "environments/services/heat-engine.yaml")]
|
|
if CONF.get('enable_ironic_conductor'):
|
|
deploy_args += ['-e', os.path.join(
|
|
tht_templates, "environments/services/ironic-conductor.yaml")]
|
|
|
|
if CONF.get('minion_service_certificate'):
|
|
# We assume that the certificate is trusted
|
|
env_data['InternalTLSCAFile'] = ''
|
|
env_data.update(
|
|
_get_public_tls_parameters(
|
|
CONF.get('minion_service_certificate')))
|
|
|
|
u = CONF.get('deployment_user') or utils.get_deployment_user()
|
|
env_data['DeploymentUser'] = u
|
|
# TODO(cjeanner) drop that once using oslo.privsep
|
|
deploy_args += ['--deployment-user', u]
|
|
|
|
deploy_args += ['--output-dir=%s' % output_dir]
|
|
utils.makedirs(output_dir)
|
|
|
|
# TODO(aschultz): move this to a central class
|
|
if CONF.get('net_config_override', None):
|
|
data_file = CONF['net_config_override']
|
|
if os.path.abspath(data_file) != data_file:
|
|
data_file = os.path.join(USER_HOME, data_file)
|
|
|
|
if not os.path.exists(data_file):
|
|
msg = _("Could not find net_config_override file '%s'") % data_file
|
|
LOG.error(msg)
|
|
raise RuntimeError(msg)
|
|
|
|
# NOTE(bogdando): Process templated net config override data:
|
|
# * get a list of used instack_env j2 tags (j2 vars, like {{foo}}),
|
|
# * fetch values for the tags from the known mappins,
|
|
# * raise, if there is unmatched tags left
|
|
# * render the template into a JSON dict
|
|
net_config_env, template_source = _get_jinja_env_source(data_file)
|
|
|
|
# Create rendering context from the known to be present mappings for
|
|
# identified instack_env tags to generated in env_data minion heat
|
|
# params. Fall back to config opts, when env_data misses a param.
|
|
context = {}
|
|
for tag in INSTACK_NETCONF_MAPPING.keys():
|
|
mapped_value = INSTACK_NETCONF_MAPPING[tag]
|
|
if mapped_value in env_data.keys() or mapped_value in CONF.keys():
|
|
try:
|
|
context[tag] = CONF[mapped_value]
|
|
except cfg.NoSuchOptError:
|
|
context[tag] = env_data.get(mapped_value, None)
|
|
|
|
# this returns a unicode string, convert it in into json
|
|
net_config_str = net_config_env.get_template(
|
|
os.path.split(data_file)[-1]).render(context).replace(
|
|
"'", '"').replace('"', '"')
|
|
try:
|
|
net_config_json = json.loads(net_config_str)
|
|
except ValueError:
|
|
net_config_json = json.loads("{%s}" % net_config_str)
|
|
|
|
if 'network_config' not in net_config_json:
|
|
msg = ('Unsupported data format in net_config_override '
|
|
'file %s: %s' % (data_file, net_config_str))
|
|
LOG.error(msg)
|
|
raise exceptions.DeploymentError(msg)
|
|
|
|
env_data['UndercloudNetConfigOverride'] = net_config_json
|
|
|
|
params_file = os.path.join(tempdir, 'minion_parameters.yaml')
|
|
utils.write_env_file(env_data, params_file, registry_overwrites)
|
|
deploy_args += ['-e', params_file]
|
|
|
|
if CONF.get('hieradata_override', None):
|
|
data_file = CONF['hieradata_override']
|
|
if os.path.abspath(data_file) != data_file:
|
|
data_file = os.path.join(USER_HOME, data_file)
|
|
|
|
if not os.path.exists(data_file):
|
|
msg = _("Could not find hieradata_override file '%s'") % data_file
|
|
LOG.error(msg)
|
|
raise RuntimeError(msg)
|
|
|
|
deploy_args += ['--hieradata-override=%s' % data_file]
|
|
|
|
if CONF.get('minion_hostname'):
|
|
utils.set_hostname(CONF.get('minion_hostname'))
|
|
|
|
if CONF.get('minion_enable_validations') and not no_validations:
|
|
undercloud_preflight.minion_check(verbose_level, upgrade)
|
|
|
|
if CONF.get('custom_env_files'):
|
|
for custom_file in CONF['custom_env_files']:
|
|
deploy_args += ['-e', custom_file]
|
|
|
|
if verbose_level > 1:
|
|
deploy_args.append('--debug')
|
|
|
|
deploy_args.append('--log-file=%s' % CONF['minion_log_file'])
|
|
|
|
# Always add a drop-in for the ephemeral minion heat stack
|
|
# virtual state tracking (the actual file will be created later)
|
|
stack_vstate_dropin = os.path.join(
|
|
tht_templates, 'minion-stack-vstate-dropin.yaml')
|
|
deploy_args += ["-e", stack_vstate_dropin]
|
|
if force_stack_update:
|
|
deploy_args += ["--force-stack-update"]
|
|
|
|
roles_file = os.path.join(tht_templates, constants.MINION_ROLES_FILE)
|
|
cmd = ["sudo", "--preserve-env", "openstack", "tripleo", "deploy",
|
|
"--standalone", "--standalone-role", "UndercloudMinion", "--stack",
|
|
"minion", "-r", roles_file]
|
|
cmd += deploy_args[:]
|
|
|
|
# In dry-run, also report the expected heat stack virtual state/action
|
|
if dry_run:
|
|
stack_update_mark = os.path.join(
|
|
constants.STANDALONE_EPHEMERAL_STACK_VSTATE,
|
|
'update_mark_minion')
|
|
if os.path.isfile(stack_update_mark) or force_stack_update:
|
|
LOG.warning(_('The heat stack minion virtual state/action '
|
|
' would be UPDATE'))
|
|
|
|
return cmd
|
|
|
|
|
|
def _get_public_tls_parameters(service_certificate_path):
|
|
with open(service_certificate_path, "rb") as pem_file:
|
|
pem_data = pem_file.read()
|
|
cert = x509.load_pem_x509_certificate(pem_data, default_backend())
|
|
private_key = serialization.load_pem_private_key(
|
|
pem_data,
|
|
password=None,
|
|
backend=default_backend())
|
|
|
|
key_pem = private_key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
encryption_algorithm=serialization.NoEncryption())
|
|
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
|
|
return {
|
|
'SSLCertificate': cert_pem,
|
|
'SSLKey': key_pem
|
|
}
|
|
|
|
|
|
def _container_images_config(conf, deploy_args, env_data, tempdir):
|
|
if conf.container_images_file:
|
|
deploy_args += ['-e', conf.container_images_file]
|
|
try:
|
|
shutil.copy(os.path.abspath(conf.container_images_file), tempdir)
|
|
except Exception:
|
|
msg = _('Cannot copy a container images'
|
|
'file %s into a tempdir!') % conf.container_images_file
|
|
LOG.error(msg)
|
|
raise exceptions.DeploymentError(msg)
|
|
else:
|
|
# no images file was provided. Set a default ContainerImagePrepare
|
|
# parameter to trigger the preparation of the required container list
|
|
cip = kolla_builder.CONTAINER_IMAGE_PREPARE_PARAM
|
|
env_data['ContainerImagePrepare'] = cip
|