# 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 copy import errno import getpass import glob import hashlib import io import logging import os import platform import socket import subprocess import uuid from novaclient import client as novaclient from novaclient import exceptions from oslo_config import cfg import psutil import six from six.moves import configparser # Making these values properties on a class allows us to delay their lookup, # which makes testing code that interacts with these files much easier. # NOTE(bnemec): The unit tests rely on these paths being in ~. If they are # ever moved the tests may need to be updated to avoid overwriting real files. class Paths(object): @property def CONF_PATH(self): return os.path.expanduser('~/undercloud.conf') # NOTE(bnemec): Deprecated @property def ANSWERS_PATH(self): return os.path.expanduser('~/instack.answers') @property def PASSWORD_PATH(self): return os.path.expanduser('~/undercloud-passwords.conf') @property def LOG_FILE(self): return os.path.expanduser('~/.instack/install-undercloud.log') PATHS = Paths() DEFAULT_LOG_LEVEL = logging.DEBUG DEFAULT_LOG_FORMAT = '%(asctime)s %(levelname)s: %(message)s' LOG = None CONF = cfg.CONF COMPLETION_MESSAGE = """ ############################################################################# Undercloud install complete. The file containing this installation's passwords is at %(password_path)s. There is also a stackrc file at %(stackrc_path)s. These files are needed to interact with the OpenStack services, and should be secured. ############################################################################# """ # We need 4 GB, leave a little room for variation in what 4 GB means on # different platforms. REQUIRED_MB = 3750 # When adding new options to the lists below, make sure to regenerate the # sample config by running "tox -e genconfig" in the project root. _opts = [ cfg.StrOpt('image_path', default='.', help=('Local file path to the necessary images. The path ' 'should be a directory readable by the current user ' 'that contains the full set of images.'), ), cfg.StrOpt('local_ip', default='192.0.2.1/24', help=('IP information for the interface on the Undercloud ' 'that will be handling the PXE boots and DHCP for ' 'Overcloud instances. The IP portion of the value will ' 'be assigned to the network interface defined by ' 'local_interface, with the netmask defined by the ' 'prefix portion of the value.') ), cfg.StrOpt('network_gateway', default='192.0.2.1', help=('Network gateway for the Neutron-managed network for ' 'Overcloud instances. This should match the local_ip ' 'above when using masquerading.') ), cfg.StrOpt('undercloud_public_vip', default='192.0.2.2', help=('Virtual IP address to use for the public endpoints of ' 'Undercloud services. Only used if ' 'undercloud_service_certficate is set.') ), cfg.StrOpt('undercloud_admin_vip', default='192.0.2.3', help=('Virtual IP address to use for the admin endpoints of ' 'Undercloud services. Only used if ' 'undercloud_service_certficate is set.') ), cfg.StrOpt('undercloud_service_certificate', default='', help=('Certificate file to use for OpenStack service SSL ' 'connections. Setting this enables SSL for the ' 'OpenStack API endpoints, leaving it unset disables SSL.') ), cfg.StrOpt('local_interface', default='eth1', help=('Network interface on the Undercloud that will be ' 'handling the PXE boots and DHCP for Overcloud ' 'instances.') ), cfg.StrOpt('network_cidr', default='192.0.2.0/24', help=('Network CIDR for the Neutron-managed network for ' 'Overcloud instances. This should be the subnet used ' 'for PXE booting.') ), cfg.StrOpt('masquerade_network', default='192.0.2.0/24', help=('Network that will be masqueraded for external access, ' 'if required. This should be the subnet used for PXE ' 'booting.') ), cfg.StrOpt('dhcp_start', default='192.0.2.5', help=('Start of DHCP allocation range for PXE and DHCP of ' 'Overcloud instances.') ), cfg.StrOpt('dhcp_end', default='192.0.2.24', help=('End of DHCP allocation range for PXE and DHCP of ' 'Overcloud instances.') ), cfg.StrOpt('inspection_interface', default='br-ctlplane', deprecated_name='discovery_interface', help=('Network interface on which inspection dnsmasq will ' 'listen. If in doubt, use the default value.') ), cfg.StrOpt('inspection_iprange', default='192.0.2.100,192.0.2.120', deprecated_name='discovery_iprange', help=('Temporary IP range that will be given to nodes during ' 'the inspection process. Should not overlap with the ' 'range defined by dhcp_start and dhcp_end, but should ' 'be in the same network.') ), cfg.BoolOpt('inspection_runbench', default=False, deprecated_name='discovery_runbench', help='Whether to run benchmarks when inspecting nodes.' ), cfg.BoolOpt('undercloud_debug', default=True, help=('Whether to enable the debug log level for Undercloud ' 'OpenStack services.') ), cfg.BoolOpt('enable_tuskar', default=False, help=('Whether to install Tuskar services in the Undercloud.') ), cfg.BoolOpt('enable_tempest', default=False, help=('Whether to install Tempest in the Undercloud.') ), cfg.BoolOpt('ipxe_deploy', default=True, help=('Whether to use iPXE for deploy by default.') ), ] # Passwords, tokens, hashes _auth_opts = [ cfg.StrOpt('undercloud_db_password', help=('Password used for MySQL databases. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_admin_token', help=('Keystone admin token. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_admin_password', help=('Keystone admin password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_glance_password', help=('Glance service password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_heat_encryption_key', help=('Heat db encryption key(must be 16, 24, or 32 characters.' ' If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_heat_password', help=('Heat service password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_neutron_password', help=('Neutron service password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_nova_password', help=('Nova service password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_ironic_password', help=('Ironic service password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_tuskar_password', help=('Tuskar service password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_ceilometer_password', help=('Ceilometer service password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_ceilometer_metering_secret', help=('Ceilometer metering secret. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_ceilometer_snmpd_user', default='ro_snmp_user', help=('Ceilometer snmpd read-only user. If this value is ' 'changed from the default, the new value must be passed ' 'in the overcloud environment as the parameter ' 'SnmpdReadonlyUserName. This value must be between ' '1 and 32 characters long.') ), cfg.StrOpt('undercloud_ceilometer_snmpd_password', help=('Ceilometer snmpd password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_swift_password', help=('Swift service password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_rabbit_cookie', help=('Rabbitmq cookie. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_rabbit_password', help=('Rabbitmq password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_rabbit_username', help=('Rabbitmq username. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_heat_stack_domain_admin_password', help=('Heat stack domain admin password. ' 'If left unset, one will be automatically generated.') ), cfg.StrOpt('undercloud_swift_hash_suffix', help=('Swift hash suffix. ' 'If left unset, one will be automatically generated.') ), ] CONF.register_opts(_opts) CONF.register_opts(_auth_opts, group='auth') def list_opts(): return [(None, copy.deepcopy(_opts)), ('auth', copy.deepcopy(_auth_opts)), ] def _configure_logging(level, filename): """Does the initial logging configuration This should only ever be called once. If further changes to the logging config are needed they should be made directly on the LOG object. :param level: The desired logging level :param filename: The log file. Set to None to disable file logging. """ try: os.makedirs(os.path.dirname(PATHS.LOG_FILE)) except OSError as e: if e.errno != errno.EEXIST: raise logging.basicConfig(filename=filename, format=DEFAULT_LOG_FORMAT, level=level) global LOG LOG = logging.getLogger(__name__) LOG.addHandler(logging.StreamHandler()) def _load_config(): conf_params = [] if os.path.isfile(PATHS.PASSWORD_PATH): conf_params += ['--config-file', PATHS.PASSWORD_PATH] if os.path.isfile(PATHS.CONF_PATH): conf_params += ['--config-file', PATHS.CONF_PATH] CONF(conf_params) def _run_command(args, env=None, name=None): """Run the command defined by args and return its output :param args: List of arguments for the command to be run. :param env: Dict defining the environment variables. Pass None to use the current environment. :param name: User-friendly name for the command being run. A value of None will cause args[0] to be used. """ if name is None: name = args[0] try: return subprocess.check_output(args, stderr=subprocess.STDOUT, env=env).decode() except subprocess.CalledProcessError as e: LOG.error('%s failed: %s', name, e.output) raise def _run_live_command(args, env=None, name=None): """Run the command defined by args and log its output Takes the same arguments as _run_command, but runs the process asynchronously so the output can be logged while the process is still running. """ if name is None: name = args[0] process = subprocess.Popen(args, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: line = process.stdout.readline().decode() if line: LOG.info(line.rstrip()) if line == '' and process.poll() is not None: break if process.returncode != 0: raise RuntimeError('%s failed. See log for details.' % name) def _check_hostname(): """Check system hostname configuration Rabbit requires a pretty specific hostname configuration. This attempts to verify the configuration is correct before continuing with installation. """ LOG.info('Checking for a FQDN hostname...') args = ['sudo', 'hostnamectl', '--static'] detected_static_hostname = _run_command(args, name='hostnamectl').rstrip() LOG.info('Static hostname detected as %s', detected_static_hostname) args = ['sudo', 'hostnamectl', '--transient'] detected_transient_hostname = _run_command(args, name='hostnamectl').rstrip() LOG.info('Transient hostname detected as %s', detected_transient_hostname) if detected_static_hostname != detected_transient_hostname: LOG.error('Static hostname "%s" does not match transient hostname ' '"%s".', detected_static_hostname, detected_transient_hostname) LOG.error('Use hostnamectl to set matching hostnames.') raise RuntimeError('Static and transient hostnames do not match') with open('/etc/hosts') as hosts_file: for line in hosts_file: if (not line.lstrip().startswith('#') and detected_static_hostname in line.split()): break else: LOG.error('Static hostname not set in /etc/hosts.') LOG.error('Please add a line to /etc/hosts for the static ' 'hostname.') raise RuntimeError('Static hostname not set in /etc/hosts') def _check_memory(): """Check system memory The undercloud will not run properly in less than 4 GB of memory. This function verifies that at least that much is available before proceeding with install. """ mem = psutil.virtual_memory() total_mb = mem.total / 1024 / 1024 if total_mb < REQUIRED_MB: LOG.error('At least 4 GB of memory is required for undercloud ' 'installation. A minimum of 6 GB is recommended. ' 'Only detected %d MB' % total_mb) raise RuntimeError('Insufficient memory available') def _generate_password(length=40): """Create a random password Copied from rdomanager-oscplugin. This should eventually live in tripleo-common. """ uuid_str = six.text_type(uuid.uuid4()).encode("UTF-8") return hashlib.sha1(uuid_str).hexdigest()[:length] def _generate_endpoints(instack_env): local_host = instack_env['LOCAL_IP'] public_host = local_host proto = 'http' heat_public_port = 8004 neutron_public_port = 9696 glance_public_port = 9292 nova_public_port = 8774 ceilo_public_port = 8777 keystone_public_port = 5000 swift_public_port = 8080 ironic_public_port = 6385 if CONF.undercloud_service_certificate: public_host = CONF.undercloud_public_vip proto = 'https' heat_public_port = 13004 neutron_public_port = 13696 glance_public_port = 13292 nova_public_port = 13774 ceilo_public_port = 13777 keystone_public_port = 13000 swift_public_port = 13808 ironic_public_port = 13385 heat_public_params = (proto, public_host, heat_public_port) heat_internal_params = ('http', local_host, 8004) heat_admin_params = heat_internal_params neutron_public_params = (proto, public_host, neutron_public_port) neutron_internal_params = ('http', local_host, 9696) neutron_admin_params = neutron_internal_params glance_public_params = (proto, public_host, glance_public_port) glance_internal_params = ('http', local_host, 9292) glance_admin_params = glance_internal_params nova_public_params = (proto, public_host, nova_public_port) nova_internal_params = ('http', local_host, 8774) nova_admin_params = nova_internal_params ceilo_public_params = (proto, public_host, ceilo_public_port) ceilo_internal_params = ('http', local_host, 8777) ceilo_admin_params = ceilo_internal_params keystone_public_params = (proto, public_host, keystone_public_port) keystone_internal_params = ('http', local_host, 5000) keystone_admin_params = ('http', local_host, 35357) swift_public_params = (proto, public_host, swift_public_port) swift_internal_params = ('http', local_host, 8080) swift_admin_params = swift_internal_params ironic_public_params = (proto, public_host, ironic_public_port) ironic_internal_params = ('http', local_host, 6385) ironic_admin_params = ironic_internal_params endpoints = {} def add_endpoint(name, format_str, public, internal, admin): upper_name = name.upper() endpoints['UNDERCLOUD_ENDPOINT_%s_PUBLIC' % upper_name] = (format_str % public) endpoints['UNDERCLOUD_ENDPOINT_%s_INTERNAL' % upper_name] = ( format_str % internal) endpoints['UNDERCLOUD_ENDPOINT_%s_ADMIN' % upper_name] = (format_str % admin) add_endpoint('heat', '%s://%s:%d/v1/%%(tenant_id)s', heat_public_params, heat_internal_params, heat_admin_params, ) add_endpoint('neutron', '%s://%s:%d', neutron_public_params, neutron_internal_params, neutron_admin_params, ) add_endpoint('glance', '%s://%s:%d', glance_public_params, glance_internal_params, glance_admin_params, ) add_endpoint('nova', '%s://%s:%d/v2/%%(tenant_id)s', nova_public_params, nova_internal_params, nova_admin_params, ) add_endpoint('novav3', '%s://%s:%d/v3', nova_public_params, nova_internal_params, nova_admin_params, ) add_endpoint('ceilometer', '%s://%s:%d', ceilo_public_params, ceilo_internal_params, ceilo_admin_params, ) add_endpoint('keystone', '%s://%s:%d', keystone_public_params, keystone_internal_params, keystone_admin_params, ) add_endpoint('swift', '%s://%s:%d/v1/AUTH_%%(tenant_id)s', swift_public_params, swift_internal_params, swift_admin_params, ) # The swift admin endpoint has a different format from the others endpoints['UNDERCLOUD_ENDPOINT_SWIFT_ADMIN'] = ('%s://%s:%s' % swift_admin_params) add_endpoint('ironic', '%s://%s:%d', ironic_public_params, ironic_internal_params, ironic_admin_params, ) instack_env.update(endpoints) def _write_password_file(answers_parser, instack_env): with open(PATHS.PASSWORD_PATH, 'w') as password_file: password_file.write('[auth]\n') for opt in _auth_opts: env_name = opt.name.upper() if answers_parser.has_option('answers', env_name): LOG.warning('Using value for %s from instack.answers. This ' 'behavior is deprecated. undercloud.conf should ' 'now be used for configuration.', env_name) value = answers_parser.get('answers', env_name) else: value = CONF.auth[opt.name] if not value: # Heat requires this encryption key to be a specific length if env_name == 'UNDERCLOUD_HEAT_ENCRYPTION_KEY': value = _generate_password(32) else: value = _generate_password() LOG.info('Generated new password for %s', opt.name) instack_env[env_name] = value password_file.write('%s=%s\n' % (opt.name, value)) def _generate_environment(instack_root): """Generate an environment dict for instack The returned dict will have the necessary values for use as the env parameter when calling instack via the subprocess module. :param instack_root: The path containing the instack-undercloud elements and json files. """ instack_env = dict(os.environ) # Rabbit uses HOSTNAME, so we need to make sure it's right instack_env['HOSTNAME'] = socket.gethostname() # Find the paths we need json_file_dir = '/usr/share/instack-undercloud/json-files' if not os.path.isdir(json_file_dir): json_file_dir = os.path.join(instack_root, 'json-files') instack_undercloud_elements = '/usr/share/instack-undercloud' if not os.path.isdir(instack_undercloud_elements): instack_undercloud_elements = os.path.join(instack_root, 'elements') tripleo_puppet_elements = '/usr/share/tripleo-puppet-elements' if not os.path.isdir(tripleo_puppet_elements): tripleo_puppet_elements = os.path.join(os.getcwd(), 'tripleo-puppet-elements', 'elements') if 'ELEMENTS_PATH' in os.environ: instack_env['ELEMENTS_PATH'] = os.environ['ELEMENTS_PATH'] else: instack_env['ELEMENTS_PATH'] = ( '%s:%s:' '/usr/share/tripleo-image-elements:' '/usr/share/diskimage-builder/elements' ) % (tripleo_puppet_elements, instack_undercloud_elements) # Distro-specific values distro = platform.linux_distribution()[0] if distro.startswith('Red Hat Enterprise Linux'): instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'rhel7' instack_env['JSONFILE'] = ( os.environ.get('JSONFILE') or os.path.join(json_file_dir, 'rhel-7-undercloud-packages.json') ) instack_env['REG_METHOD'] = 'disable' instack_env['REG_HALT_UNREGISTER'] = '1' elif distro.startswith('CentOS'): instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'centos7' instack_env['JSONFILE'] = ( os.environ.get('JSONFILE') or os.path.join(json_file_dir, 'centos-7-undercloud-packages.json') ) elif distro.startswith('Fedora'): instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'fedora' raise RuntimeError('Fedora is not currently supported') else: raise RuntimeError('%s is not supported' % distro) # Do some fiddling to retain answers file support for now answers_parser = configparser.ConfigParser() if os.path.isfile(PATHS.ANSWERS_PATH): config_answers = io.StringIO() config_answers.write(u'[answers]\n') with open(PATHS.ANSWERS_PATH) as f: config_answers.write(six.text_type(f.read())) config_answers.seek(0) answers_parser.readfp(config_answers) # Convert conf opts to env values for opt in _opts: env_name = opt.name.upper() if answers_parser.has_option('answers', env_name): LOG.warning('Using value for %s from instack.answers. This ' 'behavior is deprecated. undercloud.conf should ' 'now be used for configuration.', env_name) instack_env[env_name] = answers_parser.get('answers', env_name) else: instack_env[env_name] = six.text_type(CONF[opt.name]) # Opts that needs extra processing if instack_env.get('INSPECTION_RUNBENCH') not in ['0', '1']: instack_env['INSPECTION_RUNBENCH'] = ('1' if CONF.inspection_runbench else '0') instack_env['PUBLIC_INTERFACE_IP'] = instack_env['LOCAL_IP'] instack_env['LOCAL_IP'] = instack_env['LOCAL_IP'].split('/')[0] if instack_env['UNDERCLOUD_SERVICE_CERTIFICATE']: instack_env['UNDERCLOUD_SERVICE_CERTIFICATE'] = os.path.abspath( instack_env['UNDERCLOUD_SERVICE_CERTIFICATE']) # We're not in a chroot so this doesn't make sense, and it causes weird # errors if it's set. if instack_env.get('DIB_YUM_REPO_CONF'): del instack_env['DIB_YUM_REPO_CONF'] _generate_endpoints(instack_env) _write_password_file(answers_parser, instack_env) return instack_env def _run_instack(instack_env): args = ['sudo', '-E', 'instack', '-p', instack_env['ELEMENTS_PATH'], '-j', instack_env['JSONFILE'], ] LOG.info('Running instack') _run_live_command(args, instack_env, 'instack') LOG.info('Instack completed successfully') def _run_orc(instack_env): args = ['sudo', 'os-refresh-config'] LOG.info('Running os-refresh-config') _run_live_command(args, instack_env, 'os-refresh-config') LOG.info('os-refresh-config completed successfully') def _extract_from_stackrc(name): """Extract authentication values from stackrc :param name: The value to be extracted. For example: OS_USERNAME or OS_AUTH_URL. """ with open(os.path.expanduser('~/stackrc')) as f: for line in f: if name in line: parts = line.split('=') return parts[1].rstrip() def _ensure_user_identity(id_path): if not os.path.isfile(id_path): args = ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', id_path] _run_command(args) LOG.info('Generated new ssh key in ~/.ssh/id_rsa') def _get_auth_values(): """Get auth values from stackrc Returns the user, password, tenant and auth_url as read from stackrc, in that order as a tuple. """ user = _extract_from_stackrc('OS_USERNAME') password = _run_command(['sudo', 'hiera', 'admin_password']).rstrip() tenant = _extract_from_stackrc('OS_TENANT') auth_url = _extract_from_stackrc('OS_AUTH_URL') return user, password, tenant, auth_url def _configure_ssh_keys(nova): """Configure default ssh keypair in Nova Generates a new ssh key for the current user if one does not already exist, then uploads that to Nova as the 'default' keypair. """ id_path = os.path.expanduser('~/.ssh/id_rsa') _ensure_user_identity(id_path) try: nova.keypairs.get('default') except exceptions.NotFound: with open(id_path + '.pub') as pubkey: nova.keypairs.create('default', pubkey.read().rstrip()) def _ensure_flavor(nova, name, profile=None): try: flavor = nova.flavors.create(name, 4096, 1, 40) except exceptions.Conflict: LOG.info('Not creating flavor "%s" because it already exists.', name) return keys = {'capabilities:boot_option': 'local'} if profile is not None: keys['capabilities:profile'] = profile flavor.set_keys(keys) message = 'Created flavor "%s" with profile "%s"' LOG.info(message, name, profile) def _copy_stackrc(): args = ['sudo', 'cp', '/root/stackrc', os.path.expanduser('~')] _run_command(args, name='Copy stackrc') args = ['sudo', 'chown', getpass.getuser() + ':', os.path.expanduser('~/stackrc')] _run_command(args, name='Chown stackrc') def _clean_os_refresh_config(): orc_dirs = glob.glob('/usr/libexec/os-refresh-config/*') args = ['sudo', 'rm', '-rf'] + orc_dirs _run_command(args, name='Clean os-refresh-config') def _post_config(): _copy_stackrc() user, password, tenant, auth_url = _get_auth_values() nova = novaclient.Client(2, user, password, tenant, auth_url) _configure_ssh_keys(nova) _ensure_flavor(nova, 'baremetal') _ensure_flavor(nova, 'control', 'control') _ensure_flavor(nova, 'compute', 'compute') _ensure_flavor(nova, 'ceph-storage', 'ceph-storage') _ensure_flavor(nova, 'block-storage', 'block-storage') _ensure_flavor(nova, 'swift-storage', 'swift-storage') def install(instack_root): """Install the undercloud :param instack_root: The path containing the instack-undercloud elements and json files. """ _configure_logging(DEFAULT_LOG_LEVEL, PATHS.LOG_FILE) LOG.info('Logging to %s', PATHS.LOG_FILE) _load_config() _check_hostname() _clean_os_refresh_config() _check_memory() instack_env = _generate_environment(instack_root) _run_instack(instack_env) # NOTE(bnemec): I removed the conditional running of os-refresh-config. # To my knowledge it wasn't really being used anymore, and if we do still # need it, it should be reimplemented as a client parameter instead of # an input env var. # TODO(bnemec): Do we still need INSTACK_ROOT? instack_env['INSTACK_ROOT'] = os.environ.get('INSTACK_ROOT') or '' _run_orc(instack_env) _post_config() _run_command(['sudo', 'rm', '-f', '/tmp/svc-map-services'], None, 'rm') LOG.info(COMPLETION_MESSAGE, {'password_path': PATHS.PASSWORD_PATH, 'stackrc_path': os.path.expanduser('~/stackrc')})