python utility to manage a tripleo based cloud
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1227 lines
50 KiB

# Copyright 2015 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.
#
import argparse
import os
import os.path
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from prettytable import PrettyTable
from pwd import getpwuid
import shutil
import six
import time
import yaml
from heatclient.common import template_utils
from keystoneauth1.exceptions.catalog import EndpointNotFound
from osc_lib import exceptions as oscexc
from osc_lib.i18n import _
from tripleo_common import update
from tripleo_common.utils import plan as plan_utils
from tripleoclient import command
from tripleoclient import constants
from tripleoclient import exceptions
from tripleoclient import utils
from tripleoclient.workflows import deployment
from tripleoclient.workflows import parameters as workflow_params
CONF = cfg.CONF
class DeployOvercloud(command.Command):
"""Deploy Overcloud"""
log = logging.getLogger(__name__ + ".DeployOvercloud")
def _setup_clients(self, parsed_args):
self.clients = self.app.client_manager
self.orchestration_client = self.clients.orchestration
if not parsed_args.deployed_server:
try:
self.compute_client = self.clients.compute
self.baremetal_client = self.clients.baremetal
except EndpointNotFound:
self.log.warning('WARNING: Nova endpoint not available. '
'Assuming --deployed-server')
parsed_args.deployed_server = True
def _update_args_from_answers_file(self, args):
if args.answers_file is not None:
with open(args.answers_file, 'r') as answers_file:
answers = yaml.safe_load(answers_file)
if args.templates is None:
args.templates = answers['templates']
if 'environments' in answers:
if args.environment_files is not None:
answers['environments'].extend(args.environment_files)
args.environment_files = answers['environments']
def _update_parameters(self, args, parameters,
tht_root, user_tht_root):
parameters['RootStackName'] = args.stack
if not args.skip_deploy_identifier:
parameters['DeployIdentifier'] = int(time.time())
else:
parameters['DeployIdentifier'] = ''
if args.heat_type != 'installed':
heat = None
else:
heat = self.orchestration_client
# Check for existing passwords file
password_params_path = os.path.join(
self.working_dir,
constants.PASSWORDS_ENV_FORMAT.format(args.stack))
if os.path.exists(password_params_path):
with open(password_params_path, 'r') as f:
passwords_env = yaml.safe_load(f.read())
else:
passwords_env = None
password_params = plan_utils.generate_passwords(
None, heat, args.stack, passwords_env=passwords_env)
# Save generated passwords file
with open(password_params_path, 'w') as f:
f.write(yaml.safe_dump(dict(parameter_defaults=password_params)))
os.chmod(password_params_path, 0o600)
parameters.update(password_params)
param_args = (
('NtpServer', 'ntp_server'),
('NovaComputeLibvirtType', 'libvirt_type'),
)
# Update parameters from commandline
for param, arg in param_args:
if getattr(args, arg, None) is not None:
parameters[param] = getattr(args, arg)
parameters[
'UndercloudHostsEntries'] = [utils.get_undercloud_host_entry()]
parameters['CtlplaneNetworkAttributes'] = utils.get_ctlplane_attrs()
return parameters
def _check_limit_skiplist_warning(self, env):
if env.get('parameter_defaults').get('DeploymentServerBlacklist'):
msg = _('[WARNING] DeploymentServerBlacklist is defined and will '
'be ignored because --limit has been specified.')
self.log.warning(msg)
def _heat_deploy(self, stack, stack_name, template_path,
env_files, timeout, tht_root, env,
run_validations,
roles_file,
env_files_tracker=None,
deployment_options=None):
"""Verify the Baremetal nodes are available and do a stack update"""
if stack:
self.log.debug(
"Checking compatibilities of neutron drivers for {0}".format(
stack_name))
msg = update.check_neutron_mechanism_drivers(
env, stack, None, stack_name)
if msg:
raise oscexc.CommandError(msg)
self.log.debug("Getting template contents from plan %s" % stack_name)
template_files, template = template_utils.get_template_contents(
template_file=template_path)
files = dict(list(template_files.items()) + list(env_files.items()))
workflow_params.check_deprecated_parameters(
self.clients, stack_name, tht_root, template,
roles_file, files, env_files_tracker)
self.log.info("Deploying templates in the directory {0}".format(
os.path.abspath(tht_root)))
deployment.deploy_without_plan(
self.clients, stack, stack_name,
template, files, env_files_tracker,
self.log, self.working_dir)
def create_template_dirs(self, parsed_args):
tht_root = os.path.abspath(parsed_args.templates)
new_tht_root = "%s/tripleo-heat-templates" % self.working_dir
self.log.debug("Creating working templates tree in %s"
% new_tht_root)
shutil.rmtree(new_tht_root, ignore_errors=True)
shutil.copytree(tht_root, new_tht_root, symlinks=True)
utils.jinja_render_files(self.log, parsed_args.templates,
new_tht_root,
parsed_args.roles_file,
parsed_args.networks_file,
new_tht_root)
return new_tht_root, tht_root
def create_env_files(self, stack, parsed_args,
new_tht_root, user_tht_root):
self.log.debug("Creating Environment files")
created_env_files = []
created_env_files.append(
os.path.join(new_tht_root, constants.DEFAULT_RESOURCE_REGISTRY))
parameters = utils.build_image_params(
created_env_files, parsed_args, new_tht_root, user_tht_root)
self._update_parameters(
parsed_args, parameters, new_tht_root, user_tht_root)
stack_is_new = stack is None
parameters['StackAction'] = 'CREATE' if stack_is_new else 'UPDATE'
param_env = utils.create_parameters_env(
parameters, new_tht_root, parsed_args.stack)
created_env_files.extend(param_env)
if stack:
env_path = utils.create_breakpoint_cleanup_env(
new_tht_root, parsed_args.stack)
created_env_files.extend(env_path)
if parsed_args.deployed_server:
created_env_files.append(
os.path.join(
new_tht_root,
constants.DEPLOYED_SERVER_ENVIRONMENT))
created_env_files.extend(
self._provision_networks(parsed_args, new_tht_root))
created_env_files.extend(
self._provision_baremetal(parsed_args, new_tht_root))
if parsed_args.environment_directories:
created_env_files.extend(utils.load_environment_directories(
parsed_args.environment_directories))
if parsed_args.environment_files:
created_env_files.extend(parsed_args.environment_files)
return created_env_files
def deploy_tripleo_heat_templates(self, stack, parsed_args,
new_tht_root, user_tht_root,
created_env_files):
"""Deploy the fixed templates in TripleO Heat Templates"""
self.log.info("Processing templates in the directory {0}".format(
os.path.abspath(new_tht_root)))
deployment_options = {}
if parsed_args.deployment_python_interpreter:
deployment_options['ansible_python_interpreter'] = \
parsed_args.deployment_python_interpreter
self.log.debug("Processing environment files %s" % created_env_files)
env_files_tracker = []
env_files, env = utils.process_multiple_environments(
created_env_files, new_tht_root, user_tht_root,
env_files_tracker=env_files_tracker,
cleanup=(not parsed_args.no_cleanup))
# Invokes the workflows specified in plan environment file
if parsed_args.plan_environment_file:
output_path = utils.build_user_env_path(
'derived_parameters.yaml', new_tht_root)
workflow_params.build_derived_params_environment(
self.clients, parsed_args.stack, new_tht_root, env_files,
env_files_tracker, parsed_args.roles_file,
parsed_args.plan_environment_file,
output_path, utils.playbook_verbosity(self=self))
created_env_files.append(output_path)
env_files_tracker = []
env_files, env = utils.process_multiple_environments(
created_env_files, new_tht_root, user_tht_root,
env_files_tracker=env_files_tracker,
cleanup=(not parsed_args.no_cleanup))
# Copy the env_files to tmp folder for archiving
utils.copy_env_files(env_files, new_tht_root)
if parsed_args.limit:
# check if skip list is defined while using --limit and throw a
# warning if necessary
self._check_limit_skiplist_warning(env)
if stack:
if not parsed_args.disable_validations:
# note(aschultz): network validation goes here before we deploy
utils.check_stack_network_matches_env_files(stack, env)
ceph_deployed = env.get('resource_registry', {}).get(
'OS::TripleO::Services::CephMon', 'OS::Heat::None')
ceph_external = env.get('resource_registry', {}).get(
'OS::TripleO::Services::CephExternal', 'OS::Heat::None')
# note (fpantano) if ceph is not TripleO deployed and no
# external ceph cluster are present, there's no reason to
# make this check and we can simply ignore it
if (ceph_deployed != "OS::Heat::None"
or ceph_external != "OS::Heat::None"):
utils.check_ceph_fsid_matches_env_files(stack, env)
# upgrades: check if swift is deployed
utils.check_swift_and_rgw(stack, env,
self.__class__.__name__)
# check migration to new nic config with ansible
utils.check_nic_config_with_ansible(stack, env)
# check migration to service vips managed by servce
utils.check_service_vips_migrated_to_service(stack, env)
if parsed_args.heat_type != 'installed':
self.setup_ephemeral_heat(
parsed_args, env.get('parameter_defaults'))
self._try_overcloud_deploy_with_compat_yaml(
new_tht_root, stack,
parsed_args.stack, env_files,
parsed_args.timeout, env,
parsed_args.run_validations,
parsed_args.roles_file,
env_files_tracker=env_files_tracker,
deployment_options=deployment_options)
self._unprovision_baremetal(parsed_args)
def _try_overcloud_deploy_with_compat_yaml(self, tht_root, stack,
stack_name,
env_files, timeout,
env, run_validations,
roles_file,
env_files_tracker=None,
deployment_options=None):
overcloud_yaml = os.path.join(tht_root, constants.OVERCLOUD_YAML_NAME)
try:
self._heat_deploy(stack, stack_name, overcloud_yaml,
env_files, timeout,
tht_root, env,
run_validations,
roles_file,
env_files_tracker=env_files_tracker,
deployment_options=deployment_options)
except oscexc.CommandError as e:
messages = 'Failed to deploy: %s' % str(e)
raise ValueError(messages)
def _deploy_postconfig(self, stack, parsed_args):
self.log.debug("_deploy_postconfig(%s)" % parsed_args)
overcloud_endpoint = utils.get_overcloud_endpoint(stack)
# NOTE(jaosorior): The overcloud endpoint can contain an IP address or
# an FQDN depending on how what it's configured to output in the
# tripleo-heat-templates. Such a configuration can be done by
# overriding the EndpointMap through parameter_defaults.
overcloud_ip_or_fqdn = six.moves.urllib.parse.urlparse(
overcloud_endpoint).hostname
keystone_admin_ip = utils.get_endpoint('KeystoneAdmin', stack)
no_proxy = os.environ.get('no_proxy', overcloud_ip_or_fqdn)
no_proxy_list = map(utils.bracket_ipv6,
[no_proxy, overcloud_ip_or_fqdn,
keystone_admin_ip])
os.environ['no_proxy'] = ','.join(
[x for x in no_proxy_list if x is not None])
utils.remove_known_hosts(overcloud_ip_or_fqdn)
def _validate_args(self, parsed_args):
if parsed_args.templates is None and parsed_args.answers_file is None:
raise oscexc.CommandError(
"You must specify either --templates or --answers-file")
if parsed_args.environment_files:
nonexisting_envs = []
jinja2_envs = []
for env_file in parsed_args.environment_files:
if env_file.endswith(".j2.yaml"):
jinja2_envs.append(env_file)
elif not os.path.isfile(env_file):
# Tolerate missing file if there's a j2.yaml file that will
# be rendered in the plan but not available locally (yet)
if not os.path.isfile(env_file.replace(".yaml",
".j2.yaml")):
nonexisting_envs.append(env_file)
# Check networks_file existence
if parsed_args.networks_file:
if not os.path.isfile(parsed_args.networks_file):
nonexisting_envs.append(parsed_args.networks_file)
# check plan_environment_file existence
if parsed_args.plan_environment_file:
if not os.path.isfile(parsed_args.plan_environment_file):
nonexisting_envs.append(parsed_args.plan_environment_file)
# check answers_file existence
if parsed_args.answers_file:
if not os.path.isfile(parsed_args.answers_file):
nonexisting_envs.append(parsed_args.answers_file)
if jinja2_envs:
rewritten_paths = [e.replace(".j2.yaml", ".yaml")
for e in jinja2_envs]
raise oscexc.CommandError(
"Error: The following jinja2 files were provided: -e "
"{}. Did you mean -e {}?".format(
' -e '.join(jinja2_envs),
' -e '.join(rewritten_paths)))
if nonexisting_envs:
raise oscexc.CommandError(
"Error: The following files were not found: {0}".format(
", ".join(nonexisting_envs)))
if (parsed_args.baremetal_deployment
and not parsed_args.deployed_server):
raise oscexc.CommandError(
"Error: --deployed-server must be used when using "
"--baremetal-deployment")
if (parsed_args.baremetal_deployment
and (parsed_args.config_download_only
or parsed_args.setup_only)):
raise oscexc.CommandError(
"Error: --config-download-only/--setup-only must not be "
"used when using --baremetal-deployment")
if parsed_args.environment_directories:
self._validate_args_environment_directory(
parsed_args.environment_directories)
def _validate_args_environment_directory(self, directories):
default = os.path.expanduser(constants.DEFAULT_ENV_DIRECTORY)
nonexisting_dirs = []
for d in directories:
if not os.path.isdir(d) and d != default:
nonexisting_dirs.append(d)
if nonexisting_dirs:
raise oscexc.CommandError(
"Error: The following environment directories were not found"
": {0}".format(", ".join(nonexisting_dirs)))
def _provision_baremetal(self, parsed_args, tht_root):
if not parsed_args.baremetal_deployment:
return []
with open(parsed_args.baremetal_deployment, 'r') as fp:
roles = yaml.safe_load(fp)
key = self.get_key_pair(parsed_args)
with open('{}.pub'.format(key), 'rt') as fp:
ssh_key = fp.read()
output_path = utils.build_user_env_path(
'baremetal-deployed.yaml',
tht_root
)
extra_vars = {
"stack_name": parsed_args.stack,
"baremetal_deployment": roles,
"baremetal_deployed_path": output_path,
"ssh_public_keys": ssh_key,
"ssh_user_name": parsed_args.overcloud_ssh_user
}
with utils.TempDirs() as tmp:
utils.run_ansible_playbook(
playbook='cli-overcloud-node-provision.yaml',
inventory='localhost,',
workdir=tmp,
playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS,
verbosity=utils.playbook_verbosity(self=self),
extra_vars=extra_vars,
)
with open(output_path, 'r') as fp:
parameter_defaults = yaml.safe_load(fp)
utils.write_user_environment(
parameter_defaults,
'baremetal-deployed.yaml',
tht_root,
parsed_args.stack)
return [output_path]
def _unprovision_baremetal(self, parsed_args):
if not parsed_args.baremetal_deployment:
return
with open(parsed_args.baremetal_deployment, 'r') as fp:
roles = yaml.safe_load(fp)
with utils.TempDirs() as tmp:
utils.run_ansible_playbook(
playbook='cli-overcloud-node-unprovision.yaml',
inventory='localhost,',
workdir=tmp,
playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS,
verbosity=utils.playbook_verbosity(self=self),
extra_vars={
"stack_name": parsed_args.stack,
"baremetal_deployment": roles,
"prompt": False,
}
)
@staticmethod
def _is_network_data_v2(networks_file_path):
with open(networks_file_path, 'r') as f:
network_data = yaml.safe_load(f.read())
for network in network_data:
if 'ip_subnet' in network or 'ipv6_subnet' in network:
return False
return True
def _provision_networks(self, parsed_args, tht_root):
# Parse the network data, if any network have 'ip_subnet' or
# 'ipv6_subnet' keys this is not a network-v2 format file. In this
# case do nothing.
networks_file_path = utils.get_networks_file_path(
parsed_args.networks_file, parsed_args.templates)
if not self._is_network_data_v2(networks_file_path):
return []
output_path = utils.build_user_env_path(
'networks-deployed.yaml',
tht_root)
extra_vars = {
"network_data_path": networks_file_path,
"network_deployed_path": output_path,
"overwrite": True,
}
with utils.TempDirs() as tmp:
utils.run_ansible_playbook(
playbook='cli-overcloud-network-provision.yaml',
inventory='localhost,',
workdir=tmp,
playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS,
verbosity=utils.playbook_verbosity(self=self),
extra_vars=extra_vars,
)
return [output_path]
def setup_ephemeral_heat(self, parsed_args, parameters):
self.log.info("Using ephemeral heat for stack operation")
api_container_image = parameters['ContainerHeatApiImage']
engine_container_image = \
parameters['ContainerHeatEngineImage']
restore_db = (parsed_args.setup_only or
parsed_args.config_download_only)
self.heat_launcher = utils.get_heat_launcher(
parsed_args.heat_type,
api_container_image=api_container_image,
engine_container_image=engine_container_image,
heat_dir=os.path.join(self.working_dir,
'heat-launcher'),
use_tmp_dir=False,
rm_heat=parsed_args.rm_heat,
skip_heat_pull=parsed_args.skip_heat_pull)
self.orchestration_client = \
utils.launch_heat(self.heat_launcher, restore_db=restore_db)
self.clients.orchestration = self.orchestration_client
def get_parser(self, prog_name):
# add_help doesn't work properly, set it to False:
parser = argparse.ArgumentParser(
description=self.get_description(),
prog=prog_name,
add_help=False
)
parser.add_argument(
'--templates', nargs='?', const=constants.TRIPLEO_HEAT_TEMPLATES,
help=_("The directory containing the Heat templates to deploy"),
)
parser.add_argument('--stack',
help=_("Stack name to create or update"),
default='overcloud')
parser.add_argument('--timeout', '-t', metavar='<TIMEOUT>',
type=int, default=240,
help=_('Deployment timeout in minutes.'))
parser.add_argument('--libvirt-type',
choices=['kvm', 'qemu'],
help=_('Libvirt domain type.'))
parser.add_argument('--ntp-server',
help=_('The NTP for overcloud nodes. '))
parser.add_argument(
'--no-proxy',
default=os.environ.get('no_proxy', ''),
help=_('A comma separated list of hosts that should not be '
'proxied.')
)
parser.add_argument(
'--overcloud-ssh-user',
default='heat-admin',
help=_('User for ssh access to overcloud nodes')
)
parser.add_argument(
'--overcloud-ssh-key',
default=None,
help=_('Key path for ssh access to overcloud nodes. When'
'undefined the key will be autodetected.')
)
parser.add_argument(
'--overcloud-ssh-network',
help=_('Network name to use for ssh access to overcloud nodes.'),
default='ctlplane'
)
parser.add_argument(
'--overcloud-ssh-enable-timeout',
help=_('This option no longer has any effect.'),
type=int,
default=constants.ENABLE_SSH_ADMIN_TIMEOUT
)
parser.add_argument(
'--overcloud-ssh-port-timeout',
help=_('Timeout for the ssh port to become active.'),
type=int,
default=constants.ENABLE_SSH_ADMIN_SSH_PORT_TIMEOUT
)
parser.add_argument(
'--environment-file', '-e', metavar='<HEAT ENVIRONMENT FILE>',
action='append', dest='environment_files',
help=_('Environment files to be passed to the heat stack-create '
'or heat stack-update command. (Can be specified more than '
'once.)')
)
parser.add_argument(
'--environment-directory', metavar='<HEAT ENVIRONMENT DIRECTORY>',
action='append', dest='environment_directories',
default=[os.path.expanduser(constants.DEFAULT_ENV_DIRECTORY)],
help=_('Environment file directories that are automatically '
' added to the heat stack-create or heat stack-update'
' commands. Can be specified more than once. Files in'
' directories are loaded in ascending sort order.')
)
parser.add_argument(
'--roles-file', '-r', dest='roles_file',
help=_('Roles file, overrides the default %s in the --templates '
'directory. May be an absolute path or the path relative '
' to --templates') % constants.OVERCLOUD_ROLES_FILE
)
parser.add_argument(
'--networks-file', '-n', dest='networks_file',
help=_('Networks file, overrides the default %s in the '
'--templates directory') % constants.OVERCLOUD_NETWORKS_FILE
)
parser.add_argument(
'--plan-environment-file', '-p',
help=_('Plan Environment file for derived parameters.')
)
parser.add_argument(
'--no-cleanup', action='store_true',
help=_('Don\'t cleanup temporary files, just log their location')
)
parser.add_argument(
'--update-plan-only',
action='store_true',
help=_('DEPRECATED: Only update the plan. Do not perform the '
'actual deployment. NOTE: Will move to a discrete command '
'in a future release. Not supported anymore.')
)
parser.add_argument(
'--validation-errors-nonfatal',
dest='validation_errors_fatal',
action='store_false',
default=True,
help=_('Allow the deployment to continue in spite of validation '
'errors. Note that attempting deployment while errors '
'exist is likely to fail.')
)
parser.add_argument(
'--validation-warnings-fatal',
action='store_true',
default=False,
help=_('Exit if there are warnings from the configuration '
'pre-checks.')
)
parser.add_argument(
'--disable-validations',
action='store_true',
default=False,
help=_('DEPRECATED. Disable the pre-deployment validations '
'entirely. These validations are the built-in '
'pre-deployment validations. To enable external '
'validations from tripleo-validations, '
'use the --run-validations flag. These validations are '
'now run via the external validations in '
'tripleo-validations.'))
parser.add_argument(
'--inflight-validations',
action='store_true',
default=False,
dest='inflight',
help=_('Activate in-flight validations during the deploy. '
'In-flight validations provide a robust way to ensure '
'deployed services are running right after their '
'activation. Defaults to False.')
)
parser.add_argument(
'--dry-run',
action='store_true',
default=False,
help=_('Only run validations, but do not apply any changes.')
)
parser.add_argument(
'--run-validations',
action='store_true',
default=False,
help=_('Run external validations from the tripleo-validations '
'project.'))
parser.add_argument(
'--skip-postconfig',
action='store_true',
default=False,
help=_('Skip the overcloud post-deployment configuration.')
)
parser.add_argument(
'--force-postconfig',
action='store_true',
default=False,
help=_('Force the overcloud post-deployment configuration.')
)
parser.add_argument(
'--skip-deploy-identifier',
action='store_true',
default=False,
help=_('Skip generation of a unique identifier for the '
'DeployIdentifier parameter. The software configuration '
'deployment steps will only be triggered if there is an '
'actual change to the configuration. This option should '
'be used with Caution, and only if there is confidence '
'that the software configuration does not need to be '
'run, such as when scaling out certain roles.')
)
parser.add_argument(
'--answers-file',
help=_('Path to a YAML file with arguments and parameters.')
)
parser.add_argument(
'--disable-password-generation',
action='store_true',
default=False,
help=_('Disable password generation.')
)
parser.add_argument(
'--deployed-server',
action='store_true',
default=False,
help=_('Use pre-provisioned overcloud nodes. Removes baremetal,'
'compute and image services requirements from the'
'undercloud node. Must only be used with the'
'--disable-validations.')
)
parser.add_argument(
'--config-download',
action='store_true',
default=True,
help=_('Run deployment via config-download mechanism. This is '
'now the default, and this CLI options may be removed in '
'the future.')
)
parser.add_argument(
'--no-config-download',
'--stack-only',
action='store_false',
default=False,
dest='config_download',
help=_('Disable the config-download workflow and only create '
'the stack and associated OpenStack resources. No '
'software configuration will be applied.')
)
parser.add_argument(
'--config-download-only',
action='store_true',
default=False,
help=_('Disable the stack create/update, and only run the '
'config-download workflow to apply the software '
'configuration.')
)
parser.add_argument(
'--setup-only',
action='store_true',
default=False,
help=_('option will automate the setup and download steps '
'required to prepare the environment for manual '
'ansible execution.')
)
parser.add_argument(
'--config-dir',
dest='config_dir',
default=None,
help=_('The directory where the configuration files will be '
'pushed'),
)
parser.add_argument(
'--config-type',
dest='config_type',
type=list,
default=None,
help=_('Only used when "--setup-only" is invoked. '
'Type of object config to be extract from the deployment, '
'defaults to all keys available'),
)
parser.add_argument(
'--no-preserve-config',
dest='preserve_config_dir',
action='store_false',
default=True,
help=('Only used when "--setup-only" is invoked. '
'If specified, will delete and recreate the --config-dir '
'if it already exists. Default is to use the existing dir '
'location and overwrite files. Files in --config-dir not '
'from the stack will be preserved by default.')
)
parser.add_argument(
'--output-dir',
action='store',
default=None,
help=_('Directory to use for saved output when using '
'--config-download. When not '
'specified, <working-dir>/config-download will be used.')
)
parser.add_argument(
'--override-ansible-cfg',
action='store',
default=None,
help=_('Path to ansible configuration file. The configuration '
'in the file will override any configuration used by '
'config-download by default.')
)
parser.add_argument(
'--config-download-timeout',
action='store',
type=int,
default=None,
help=_('Timeout (in minutes) to use for config-download steps. If '
'unset, will default to however much time is leftover '
'from the --timeout parameter after the stack operation.')
)
parser.add_argument('--deployment-python-interpreter', default=None,
help=_('The path to python interpreter to use for '
'the deployment actions. This may need to '
'be used if deploying on a python2 host '
'from a python3 system or vice versa.'))
parser.add_argument('-b', '--baremetal-deployment',
metavar='<baremetal_deployment.yaml>',
help=_('Configuration file describing the '
'baremetal deployment'))
parser.add_argument(
'--limit',
action='store',
default=None,
help=_("A string that identifies a single node or comma-separated"
"list of nodes the config-download Ansible playbook "
"execution will be limited to. For example: --limit"
" \"compute-0,compute-1,compute-5\".")
)
parser.add_argument(
'--tags',
action='store',
default=None,
help=_('A list of tags to use when running the the config-download'
' ansible-playbook command.')
)
parser.add_argument(
'--skip-tags',
action='store',
default=None,
help=_('A list of tags to skip when running the the'
' config-download ansible-playbook command.')
)
parser.add_argument(
'--ansible-forks',
action='store',
default=None,
type=int,
help=_('The number of Ansible forks to use for the'
' config-download ansible-playbook command.')
)
parser.add_argument(
'--disable-container-prepare',
action='store_true',
default=False,
help=_('Disable the container preparation actions to prevent '
'container tags from being updated and new containers '
'from being fetched. If you skip this but do not have '
'the container parameters configured, the deployment '
'action may fail.')
)
parser.add_argument(
'--working-dir',
action='store',
help=_('The working directory for the deployment where all '
'input, output, and generated files will be stored.\n'
'Defaults to "$HOME/overcloud-deploy/<stack>"')
)
parser.add_argument(
'--heat-type',
action='store',
default='installed',
choices=['installed', 'pod', 'container', 'native'],
help=_('The type of Heat process to use to execute '
'the deployment.\n'
'installed (Default): Use the system installed '
'Heat.\n'
'pod: Use an ephemeral Heat pod.\n'
'container: Use an ephemeral Heat container.\n'
'native: Use an ephemeral Heat process.')
)
parser.add_argument(
'--rm-heat',
action='store_true',
default=False,
help=_('If specified and --heat-type is container or pod '
'any existing container or pod of a previous '
'ephemeral Heat process will be deleted first. '
'Ignored if --heat-type is native.')
)
parser.add_argument(
'--skip-heat-pull',
action='store_true',
default=False,
help=_('When --heat-type is pod or container, assume '
'the container image has already been pulled ')
)
return parser
def take_action(self, parsed_args):
logging.register_options(CONF)
logging.setup(CONF, '')
self.log.debug("take_action(%s)" % parsed_args)
if not parsed_args.working_dir:
self.working_dir = utils.get_default_working_dir(
parsed_args.stack)
else:
self.working_dir = parsed_args.working_dir
utils.makedirs(self.working_dir)
if parsed_args.update_plan_only:
raise exceptions.DeploymentError(
'Only plan update is not supported.')
deploy_status = 'DEPLOY_SUCCESS'
deploy_message = 'without error'
self._setup_clients(parsed_args)
# Swiftclient logs things like 404s at error level, which is a problem
# because we use EAFP to check for the existence of files. Turn off
# most swiftclient logging to avoid cluttering up our output with
# pointless tracebacks.
sc_logger = logging.getLogger("swiftclient")
sc_logger.setLevel(logging.CRITICAL)
self._validate_args(parsed_args)
# Throw warning if deprecated service is enabled and
# ask user if deployment should still be continued.
if parsed_args.environment_files:
utils.check_deprecated_service_is_enabled(
parsed_args.environment_files)
self._update_args_from_answers_file(parsed_args)
if parsed_args.dry_run:
self.log.info("Validation Finished")
return
self.heat_launcher = None
stack = None
stack_create = None
start = time.time()
if parsed_args.heat_type == 'installed':
stack = utils.get_stack(self.orchestration_client,
parsed_args.stack)
stack_create = stack is None
if stack_create:
self.log.info("No stack found, "
"will be doing a stack create")
else:
self.log.info("Stack found, "
"will be doing a stack update")
new_tht_root, user_tht_root = \
self.create_template_dirs(parsed_args)
created_env_files = self.create_env_files(
stack, parsed_args, new_tht_root, user_tht_root)
try:
if not (parsed_args.config_download_only or
parsed_args.setup_only):
self.deploy_tripleo_heat_templates(
stack, parsed_args, new_tht_root,
user_tht_root, created_env_files)
except Exception:
if parsed_args.heat_type != 'installed' and self.heat_launcher:
self.log.info("Stopping ephemeral heat.")
utils.kill_heat(self.heat_launcher)
utils.rm_heat(self.heat_launcher, backup_db=True)
raise
# Get a new copy of the stack after stack update/create. If it was
# a create then the previous stack object would be None.
stack = utils.get_stack(self.orchestration_client, parsed_args.stack)
overcloud_endpoint = None
old_rcpath = None
rcpath = None
horizon_url = None
try:
# Force fetching of attributes
stack.get()
overcloud_endpoint = utils.get_overcloud_endpoint(stack)
horizon_url = deployment.get_horizon_url(
stack=stack.stack_name,
heat_type=parsed_args.heat_type,
working_dir=self.working_dir)
rc_params = utils.get_rc_params(
self.orchestration_client,
parsed_args.stack)
# For backwards compatibility, we will also write overcloudrc to
# $HOME and then self.working_dir.
old_rcpath = deployment.create_overcloudrc(
stack, rc_params, parsed_args.no_proxy)
rcpath = deployment.create_overcloudrc(
stack, rc_params, parsed_args.no_proxy,
self.working_dir)
config_download_dir = parsed_args.output_dir or \
os.path.join(self.working_dir, "config-download")
if parsed_args.config_download:
self.log.info("Deploying overcloud configuration")
deployment.set_deployment_status(
stack.stack_name,
status='DEPLOYING',
working_dir=self.working_dir
)
if not parsed_args.config_download_only:
deployment.get_hosts_and_enable_ssh_admin(
stack,
parsed_args.overcloud_ssh_network,
parsed_args.overcloud_ssh_user,
self.get_key_pair(parsed_args),
parsed_args.overcloud_ssh_port_timeout,
verbosity=utils.playbook_verbosity(self=self),
heat_type=parsed_args.heat_type
)
if parsed_args.config_download_timeout:
timeout = parsed_args.config_download_timeout
else:
used = int((time.time() - start) // 60)
timeout = parsed_args.timeout - used
if timeout <= 0:
raise exceptions.DeploymentError(
'Deployment timed out after %sm' % used)
if not parsed_args.setup_only:
deployment_options = {}
if parsed_args.deployment_python_interpreter:
deployment_options['ansible_python_interpreter'] = \
parsed_args.deployment_python_interpreter
deployment.make_config_download_dir(config_download_dir,
parsed_args.stack)
deployment.config_download(
self.log,
self.clients,
stack,
parsed_args.overcloud_ssh_network,
config_download_dir,
parsed_args.override_ansible_cfg,
timeout=parsed_args.overcloud_ssh_port_timeout,
verbosity=utils.playbook_verbosity(self=self),
deployment_options=deployment_options,
in_flight_validations=parsed_args.inflight,
deployment_timeout=timeout,
tags=parsed_args.tags,
skip_tags=parsed_args.skip_tags,
limit_hosts=utils.playbook_limit_parse(
limit_nodes=parsed_args.limit
),
forks=parsed_args.ansible_forks)
else:
# Download config
config_dir = parsed_args.config_dir or config_download_dir
config_type = parsed_args.config_type
preserve_config_dir = parsed_args.preserve_config_dir
extra_vars = {
'plan': stack.stack_name,
'config_dir': config_dir,
'preserve_config': preserve_config_dir
}
if parsed_args.config_type:
extra_vars['config_type'] = config_type
with utils.TempDirs() as tmp:
utils.run_ansible_playbook(
playbook='cli-config-download-export.yaml',
inventory='localhost,',
workdir=tmp,
playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS,
verbosity=utils.playbook_verbosity(self=self),
extra_vars=extra_vars
)
deployment.set_deployment_status(
stack.stack_name,
status=deploy_status,
working_dir=self.working_dir)
except (BaseException, Exception):
with excutils.save_and_reraise_exception():
deploy_status = 'DEPLOY_FAILED'
deploy_message = 'with error'
deployment.set_deployment_status(
stack.stack_name,
status=deploy_status,
working_dir=self.working_dir)
finally:
try:
# Run postconfig on create or force
if (stack_create or parsed_args.force_postconfig
and not parsed_args.skip_postconfig):
self._deploy_postconfig(stack, parsed_args)
except Exception as e:
self.log.error('Exception during postconfig')
self.log.error(e)
try:
# Copy clouds.yaml to the cloud user directory
user = \
getpwuid(os.stat(constants.CLOUD_HOME_DIR).st_uid).pw_name
utils.copy_clouds_yaml(user)
except Exception as e:
self.log.error('Exception creating clouds.yaml')
self.log.error(e)
try:
utils.create_tempest_deployer_input(
output_dir=self.working_dir)
except Exception as e:
self.log.error('Exception creating tempest configuration.')
self.log.error(e)
try:
if (parsed_args.heat_type != 'installed' and
parsed_args.config_download):
# Create overcloud export
data = utils.export_overcloud(
self.orchestration_client,
parsed_args.stack, True, False,
config_download_dir)
export_file = os.path.join(
self.working_dir, "%s-export.yaml" % parsed_args.stack)
# write the exported data
with open(export_file, 'w') as f:
yaml.safe_dump(data, f, default_flow_style=False)
os.chmod(export_file, 0o600)
except Exception as e:
self.log.error('Exception creating overcloud export.')
self.log.error(e)
print("Overcloud Endpoint: {0}".format(overcloud_endpoint))
print("Overcloud Horizon Dashboard URL: {0}".format(horizon_url))
print("Overcloud rc file: {} and {}".format(
rcpath, old_rcpath))
print("Overcloud Deployed {0}".format(deploy_message))
try:
if parsed_args.heat_type != 'installed':
self.log.info("Stopping ephemeral heat.")
utils.kill_heat(self.heat_launcher)
utils.rm_heat(self.heat_launcher, backup_db=True)
except Exception as e:
self.log.error('Exception stopping ephemeral Heat')
self.log.error(e)
try:
if parsed_args.output_dir:
ansible_dir = config_download_dir
else:
ansible_dir = None
utils.archive_deploy_artifacts(self.log, parsed_args.stack,
self.working_dir, ansible_dir)
except Exception as e:
self.log.error('Exception archiving deploy artifacts')
self.log.error(e)
class GetDeploymentStatus(command.Command):
"""Get deployment status"""
log = logging.getLogger(__name__ + ".GetDeploymentStatus")
def get_parser(self, prog_name):
parser = super(GetDeploymentStatus, self).get_parser(prog_name)
parser.add_argument('--plan', '--stack',
help=_('Name of the stack/plan. '
'(default: overcloud)'),
default='overcloud')
parser.add_argument(
'--working-dir',
action='store',
help=_('The working directory for the deployment where all '
'input, output, and generated files are stored.\n'
'Defaults to "$HOME/overcloud-deploy/<stack>"'))
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)" % parsed_args)
stack = parsed_args.plan
if not parsed_args.working_dir:
working_dir = utils.get_default_working_dir(stack)
else:
working_dir = parsed_args.working_dir
status = deployment.get_deployment_status(
self.app.client_manager,
stack,
working_dir
)
if not status:
self.log.info('No deployment was found for %s' % stack)
return
table = PrettyTable(
['Stack Name', 'Deployment Status'])
table.add_row([stack, status])
print(table, file=self.app.stdout)