diff --git a/.gitignore b/.gitignore index a033d7715..46a154be0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ lib64 # Installer logs pip-log.txt install-undercloud.log +install-minion.log # Unit test / coverage reports .coverage diff --git a/config-generator/minion.conf b/config-generator/minion.conf new file mode 100644 index 000000000..6cc50a2dd --- /dev/null +++ b/config-generator/minion.conf @@ -0,0 +1,3 @@ +[DEFAULT] +output_file = minion.conf.sample +namespace = minion_config diff --git a/releasenotes/notes/undercloud-minion-install-6b369d8f5f3d6a89.yaml b/releasenotes/notes/undercloud-minion-install-6b369d8f5f3d6a89.yaml new file mode 100644 index 000000000..482b5f386 --- /dev/null +++ b/releasenotes/notes/undercloud-minion-install-6b369d8f5f3d6a89.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds `openstack undercloud minion install` and `openstack undercloud + minion upgrade` to install or upgrade an undercloud minion that can be used + to scale heat-engine and ironic-conductor horizontally. diff --git a/setup.cfg b/setup.cfg index f760cc2cb..08eec4f1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -109,9 +109,12 @@ openstack.tripleoclient.v1 = undercloud_deploy = tripleoclient.v1.undercloud_deploy:DeployUndercloud undercloud_install = tripleoclient.v1.undercloud:InstallUndercloud undercloud_upgrade = tripleoclient.v1.undercloud:UpgradeUndercloud + undercloud_minion_install = tripleoclient.v1.undercloud_minion:InstallUndercloudMinion + undercloud_minion_upgrade = tripleoclient.v1.undercloud_minion:UpgradeUndercloudMinion undercloud_backup = tripleoclient.v1.undercloud_backup:BackupUndercloud tripleo_validator_list = tripleoclient.v1.tripleo_validator:TripleOValidatorList tripleo_validator_run = tripleoclient.v1.tripleo_validator:TripleOValidatorRun oslo.config.opts = undercloud_config = tripleoclient.config.undercloud:list_opts standalone_config = tripleoclient.config.standalone:list_opts + minion_config = tripleoclient.config.minion:list_opts diff --git a/tox.ini b/tox.ini index 25134aaa3..84e34c672 100644 --- a/tox.ini +++ b/tox.ini @@ -79,6 +79,7 @@ setenv = commands = oslo-config-generator --config-file config-generator/undercloud.conf oslo-config-generator --config-file config-generator/standalone.conf + oslo-config-generator --config-file config-generator/minion.conf [testenv:releasenotes] basepython = python3 diff --git a/tripleoclient/config/minion.py b/tripleoclient/config/minion.py new file mode 100644 index 000000000..03c9993d2 --- /dev/null +++ b/tripleoclient/config/minion.py @@ -0,0 +1,159 @@ +# 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. +# + +import copy + +from osc_lib.i18n import _ +from oslo_config import cfg + +from tripleoclient import constants + +from tripleoclient.config.standalone import StandaloneConfig + +CONF = cfg.CONF + + +class MinionConfig(StandaloneConfig): + def get_minion_service_opts(self, heat_engine=True, + ironic_conductor=False): + _opts = [ + cfg.BoolOpt('enable_heat_engine', + default=heat_engine, + help=_( + 'Whether to install the Heat Engine service.')), + cfg.BoolOpt('enable_ironic_conductor', + default=ironic_conductor, + help=_( + 'Whether to install the Ironic Conductor service. ' + 'This is currently disabled by default.')), + ] + return self.sort_opts(_opts) + + def get_base_opts(self): + _base_opts = super(MinionConfig, self).get_base_opts() + _opts = [ + cfg.StrOpt('minion_log_file', + default=constants.MINION_LOG_FILE, + help=_( + 'The path to a log file to store the ' + 'install/upgrade logs.'), + ), + cfg.StrOpt('minion_hostname', + help=_( + 'Fully qualified hostname (including domain) to ' + 'set on the Undercloud Minion. If left unset, the ' + 'current hostname will be used, but the user is ' + 'responsible for configuring all system hostname ' + 'settings appropriately. If set, the Undercloud ' + 'Minion install will configure all system hostname ' + 'settings.'), + ), + cfg.StrOpt('minion_local_ip', + default='192.168.24.50/24', + help=_( + 'IP information for the interface on the ' + 'Undercloud Minion. 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.ListOpt('minion_nameservers', + default=[], + help=_( + 'DNS nameserver(s) to configure on the Undercloud ' + 'Minion.') + ), + cfg.ListOpt('minion_ntp_servers', + default=['0.pool.ntp.org', '1.pool.ntp.org', + '2.pool.ntp.org', '3.pool.ntp.org'], + help=_('List of ntp servers to use.')), + cfg.StrOpt('minion_timezone', default=None, + help=_('Host timezone to be used. If no timezone is ' + 'specified, the existing timezone configuration ' + 'is used.')), + cfg.StrOpt('minion_service_certificate', + default='', + help=_( + 'TODO: ' + '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('minion_password_file', + default='tripleo-undercloud-passwords.yaml', + help=_( + 'The name of the file to look for the passwords ' + 'used to connect to the Undercloud. We assume ' + 'this file is in the folder where the command ' + 'is executed if a fully qualified path is not ' + 'provided.') + ), + cfg.StrOpt('minion_undercloud_output_file', + default='tripleo-undercloud-outputs.yaml', + help=_( + 'The name of the file to look for the Undercloud ' + 'output file that contains configuration ' + 'information. We assume this file is in the folder ' + 'where the command is executed if a fully ' + 'qualified path is not provided.') + ), + + cfg.StrOpt('minion_local_interface', + default='eth1', + help=_('Network interface on the Undercloud Minion ' + 'that will be used for the services.') + ), + cfg.IntOpt('minion_local_mtu', + default=1500, + help=_('MTU to use for the local_interface.') + ), + cfg.BoolOpt('minion_debug', + default=True, + help=_( + 'Whether to enable the debug log level for ' + 'OpenStack services and Container Image Prepare ' + 'step.') + ), + cfg.BoolOpt('minion_enable_selinux', + default=True, + help=_('Enable or disable SELinux during the ' + 'deployment.')), + cfg.BoolOpt('minion_enable_validations', + default=True, + help=_( + 'Run pre-flight checks when installing or ' + 'upgrading.') + ), + + ] + return self.sort_opts(_base_opts + _opts) + + def get_opts(self): + _base_opts = self.get_base_opts() + _service_opts = self.get_minion_service_opts() + return self.sort_opts(_base_opts + _service_opts) + + +def list_opts(): + """List config opts for oslo config generator""" + return [(None, copy.deepcopy(MinionConfig().get_opts()))] + + +def load_global_config(): + """Register MinionConfig options into global config""" + _opts = MinionConfig().get_opts() + CONF.register_opts(_opts) diff --git a/tripleoclient/config/standalone.py b/tripleoclient/config/standalone.py index ca7baa43b..5585267a0 100644 --- a/tripleoclient/config/standalone.py +++ b/tripleoclient/config/standalone.py @@ -134,9 +134,9 @@ class StandaloneConfig(BaseConfig): 'Relative paths get computed inside of $HOME. ' 'Must be in the json format.' 'Its content overrides anything in t-h-t ' - 'UndercloudNetConfigOverride. The processed ' + 'NetConfigOverride. The processed ' 'template is then passed in Heat via the ' - 'undercloud_parameters.yaml file created in ' + 'generated parameters file created in ' 'output_dir and used to configure the networking ' 'via run-os-net-config. If you wish to disable ' 'you can set this location to an empty file.' @@ -204,6 +204,11 @@ class StandaloneConfig(BaseConfig): default='podman', help=_('Container CLI used for deployment; ' 'Can be docker or podman.')), + cfg.BoolOpt('container_healthcheck_disabled', + default=False, + help=_( + 'Whether or not we disable the container ' + 'healthchecks.')), ] return self.sort_opts(_base_opts + _opts) diff --git a/tripleoclient/config/undercloud.py b/tripleoclient/config/undercloud.py index 16f532d14..817c6c866 100644 --- a/tripleoclient/config/undercloud.py +++ b/tripleoclient/config/undercloud.py @@ -320,11 +320,6 @@ class UndercloudConfig(StandaloneConfig): '(Experimental) Whether to clean undercloud rpms ' 'after an upgrade to a containerized ' 'undercloud.')), - cfg.BoolOpt('container_healthcheck_disabled', - default=False, - help=_( - 'Whether or not we disable the container ' - 'healthchecks.')), cfg.ListOpt('enabled_hardware_types', default=['ipmi', 'redfish', 'ilo', 'idrac'], help=_('List of enabled bare metal hardware types ' diff --git a/tripleoclient/constants.py b/tripleoclient/constants.py index efb7fcfe3..aa28a8c96 100644 --- a/tripleoclient/constants.py +++ b/tripleoclient/constants.py @@ -18,6 +18,10 @@ import os TRIPLEO_HEAT_TEMPLATES = "/usr/share/openstack-tripleo-heat-templates/" OVERCLOUD_YAML_NAME = "overcloud.yaml" OVERCLOUD_ROLES_FILE = "roles_data.yaml" +MINION_ROLES_FILE = "roles/UndercloudMinion.yaml" +MINION_OUTPUT_DIR = os.path.join(os.environ.get('HOME', '~/')) +MINION_CONF_PATH = os.path.join(MINION_OUTPUT_DIR, "minion.conf") +MINION_LOG_FILE = "install-minion.log" UNDERCLOUD_ROLES_FILE = "roles_data_undercloud.yaml" UNDERCLOUD_OUTPUT_DIR = os.path.join(os.environ.get('HOME', '~/')) STANDALONE_EPHEMERAL_STACK_VSTATE = '/var/lib/tripleo-heat-installer' diff --git a/tripleoclient/tests/config/test_config_minion.py b/tripleoclient/tests/config/test_config_minion.py new file mode 100644 index 000000000..62dc76cb8 --- /dev/null +++ b/tripleoclient/tests/config/test_config_minion.py @@ -0,0 +1,103 @@ +# Copyright 2018 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. +# + +from tripleoclient.config.minion import MinionConfig +from tripleoclient.tests import base + + +class TestMinionConfig(base.TestCase): + def setUp(self): + super(TestMinionConfig, self).setUp() + # Get the class object to test + self.config = MinionConfig() + + def test_get_base_opts(self): + ret = self.config.get_base_opts() + expected = ['cleanup', + 'container_cli', + 'container_healthcheck_disabled', + 'container_images_file', + 'container_insecure_registries', + 'container_registry_mirror', + 'custom_env_files', + 'deployment_user', + 'heat_container_image', + 'heat_native', + 'hieradata_override', + 'minion_debug', + 'minion_enable_selinux', + 'minion_enable_validations', + 'minion_hostname', + 'minion_local_interface', + 'minion_local_ip', + 'minion_local_mtu', + 'minion_log_file', + 'minion_nameservers', + 'minion_ntp_servers', + 'minion_password_file', + 'minion_service_certificate', + 'minion_timezone', + 'minion_undercloud_output_file', + 'net_config_override', + 'networks_file', + 'output_dir', + 'roles_file', + 'templates'] + self.assertEqual(expected, [x.name for x in ret]) + + def test_get_opts(self): + ret = self.config.get_opts() + expected = ['cleanup', + 'container_cli', + 'container_healthcheck_disabled', + 'container_images_file', + 'container_insecure_registries', + 'container_registry_mirror', + 'custom_env_files', + 'deployment_user', + 'enable_heat_engine', + 'enable_ironic_conductor', + 'heat_container_image', + 'heat_native', + 'hieradata_override', + 'minion_debug', + 'minion_enable_selinux', + 'minion_enable_validations', + 'minion_hostname', + 'minion_local_interface', + 'minion_local_ip', + 'minion_local_mtu', + 'minion_log_file', + 'minion_nameservers', + 'minion_ntp_servers', + 'minion_password_file', + 'minion_service_certificate', + 'minion_timezone', + 'minion_undercloud_output_file', + 'net_config_override', + 'networks_file', + 'output_dir', + 'roles_file', + 'templates'] + self.assertEqual(expected, [x.name for x in ret]) + + def test_get_minion_service_opts(self): + ret = self.config.get_minion_service_opts() + expected = {'enable_heat_engine': True, + 'enable_ironic_conductor': False} + self.assertEqual(sorted(expected.keys()), [x.name for x in ret]) + for x in ret: + self.assertEqual(expected[x.name], x.default, "%s config not %s" % + (x.name, expected[x.name])) diff --git a/tripleoclient/tests/config/test_config_standalone.py b/tripleoclient/tests/config/test_config_standalone.py index 08a14d97d..dc14cb7af 100644 --- a/tripleoclient/tests/config/test_config_standalone.py +++ b/tripleoclient/tests/config/test_config_standalone.py @@ -27,6 +27,7 @@ class TestStandaloneConfig(base.TestCase): ret = self.config.get_base_opts() expected = ['cleanup', 'container_cli', + 'container_healthcheck_disabled', 'container_images_file', 'container_insecure_registries', 'container_registry_mirror', @@ -88,6 +89,7 @@ class TestStandaloneConfig(base.TestCase): ret = self.config.get_opts() expected = ['cleanup', 'container_cli', + 'container_healthcheck_disabled', 'container_images_file', 'container_insecure_registries', 'container_registry_mirror', diff --git a/tripleoclient/tests/v1/tripleo/test_tripleo_deploy.py b/tripleoclient/tests/v1/tripleo/test_tripleo_deploy.py index fe3eef4ec..e77145bba 100644 --- a/tripleoclient/tests/v1/tripleo/test_tripleo_deploy.py +++ b/tripleoclient/tests/v1/tripleo/test_tripleo_deploy.py @@ -887,6 +887,8 @@ class TestDeployUndercloud(TestPluginV1): env ) + @mock.patch('tripleoclient.v1.tripleo_deploy.Deploy.' + '_download_stack_outputs') @mock.patch('tripleo_common.actions.ansible.' 'write_default_ansible_cfg') # TODO(cjeanner) drop once we have proper oslo.privsep @@ -937,7 +939,7 @@ class TestDeployUndercloud(TestPluginV1): mock_cleanupdirs, mock_launchansible, mock_tarball, mock_templates_dir, mock_open, mock_os, mock_user, mock_cc, - mock_chmod, mock_ac): + mock_chmod, mock_ac, mock_outputs): mock_slink.side_effect = 'fake-cmd' parsed_args = self.check_parser(self.cmd, ['--local-ip', '127.0.0.1', diff --git a/tripleoclient/tests/v1/undercloud/minion/__init__.py b/tripleoclient/tests/v1/undercloud/minion/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleoclient/tests/v1/undercloud/minion/test_config.py b/tripleoclient/tests/v1/undercloud/minion/test_config.py new file mode 100644 index 000000000..751647bf5 --- /dev/null +++ b/tripleoclient/tests/v1/undercloud/minion/test_config.py @@ -0,0 +1,200 @@ +# 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. +# +import mock +import tempfile +import yaml + +from oslo_config import cfg +from oslo_config import fixture as oslo_fixture + +from tripleo_common.image import kolla_builder + +from tripleoclient.tests import base +from tripleoclient.v1 import minion_config + + +class TestMinionDeploy(base.TestCase): + def setUp(self): + super(TestMinionDeploy, self).setUp() + self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + self.conf.set_default('output_dir', '/home/stack') + # set timezone so we don't have to mock it everywhere + self.conf.set_default('minion_timezone', 'UTC') + + @mock.patch('os.path.isdir', return_value=True) + @mock.patch('tripleoclient.v1.minion_config._process_undercloud_output', + return_value='output.yaml') + @mock.patch('tripleoclient.v1.minion_config._container_images_config') + @mock.patch('tripleoclient.utils.write_env_file') + @mock.patch('tripleoclient.utils.get_deployment_user') + @mock.patch('tripleoclient.utils.load_config') + def test_basic_deploy(self, mock_load_config, mock_get_user, + mock_write_env, mock_undercloud_output, + mock_images_config, mock_isdir): + mock_get_user.return_value = 'foo' + cmd = minion_config.prepare_minion_deploy() + expected_cmd = ['sudo', '--preserve-env', + 'openstack', 'tripleo', 'deploy', + '--standalone', '--standalone-role', + 'UndercloudMinion', '--stack', 'minion', + '-r', + '/usr/share/openstack-tripleo-heat-templates/roles/' + 'UndercloudMinion.yaml', + '--local-ip=192.168.24.50/24', + '--templates=' + '/usr/share/openstack-tripleo-heat-templates/', + '--networks-file=network_data_undercloud.yaml', + '-e', 'output.yaml', + '--heat-native', + '-e', '/usr/share/openstack-tripleo-heat-templates/' + 'environments/undercloud/undercloud-minion.yaml', + '-e', '/usr/share/openstack-tripleo-heat-templates/' + 'environments/use-dns-for-vips.yaml', + '-e', '/usr/share/openstack-tripleo-heat-templates/' + 'environments/podman.yaml', + '-e', '/usr/share/openstack-tripleo-heat-templates/' + 'environments/services/heat-engine.yaml', + '--deployment-user', 'foo', + '--output-dir=/home/stack', + '-e', '/home/stack/tripleo-config-generated-env-files/' + 'minion_parameters.yaml', + '--log-file=install-minion.log', + '-e', '/usr/share/openstack-tripleo-heat-templates/' + 'minion-stack-vstate-dropin.yaml'] + self.assertEqual(expected_cmd, cmd) + + @mock.patch('os.path.exists', return_value=True) + @mock.patch('os.path.isdir', return_value=True) + @mock.patch('tripleoclient.v1.minion_config._process_undercloud_output', + return_value='output.yaml') + @mock.patch('tripleoclient.v1.minion_config._container_images_config') + @mock.patch('tripleoclient.utils.write_env_file') + @mock.patch('tripleoclient.utils.load_config') + def test_configured_deploy(self, mock_load_config, + mock_write_env, mock_undercloud_output, + mock_images_config, mock_isdir, mock_exists): + self.conf.set_default('deployment_user', 'bar') + self.conf.set_default('enable_heat_engine', False) + self.conf.set_default('enable_ironic_conductor', True) + self.conf.set_default('hieradata_override', '/data.yaml') + self.conf.set_default('minion_local_ip', '1.1.1.1/24') + self.conf.set_default('networks_file', 'network.yaml') + self.conf.set_default('output_dir', '/bar') + self.conf.set_default('templates', '/foo') + cmd = minion_config.prepare_minion_deploy() + expected_cmd = ['sudo', '--preserve-env', + 'openstack', 'tripleo', 'deploy', + '--standalone', '--standalone-role', + 'UndercloudMinion', '--stack', 'minion', + '-r', '/foo/roles/UndercloudMinion.yaml', + '--local-ip=1.1.1.1/24', + '--templates=/foo', + '--networks-file=network.yaml', + '-e', 'output.yaml', + '--heat-native', + '-e', '/foo/environments/undercloud/' + 'undercloud-minion.yaml', + '-e', '/foo/environments/use-dns-for-vips.yaml', + '-e', '/foo/environments/podman.yaml', + '-e', '/foo/environments/services/' + 'ironic-conductor.yaml', + '--deployment-user', 'bar', + '--output-dir=/bar', + '-e', '/bar/tripleo-config-generated-env-files/' + 'minion_parameters.yaml', + '--hieradata-override=/data.yaml', + '--log-file=install-minion.log', + '-e', '/foo/minion-stack-vstate-dropin.yaml'] + self.assertEqual(expected_cmd, cmd) + + +class TestMinionContainerImageConfig(base.TestCase): + def setUp(self): + super(TestMinionContainerImageConfig, self).setUp() + conf_keys = ( + 'container_images_file', + ) + self.conf = mock.Mock(**{key: getattr(minion_config.CONF, key) + for key in conf_keys}) + + @mock.patch('shutil.copy') + def test_defaults(self, mock_copy): + env = {} + deploy_args = [] + cip_default = getattr(kolla_builder, + 'CONTAINER_IMAGE_PREPARE_PARAM', None) + self.addCleanup(setattr, kolla_builder, + 'CONTAINER_IMAGE_PREPARE_PARAM', cip_default) + + setattr(kolla_builder, 'CONTAINER_IMAGE_PREPARE_PARAM', [{ + 'set': { + 'namespace': 'one', + 'name_prefix': 'two', + 'name_suffix': 'three', + 'tag': 'four', + }, + 'tag_from_label': 'five', + }]) + + minion_config._container_images_config(self.conf, deploy_args, + env, None) + self.assertEqual([], deploy_args) + cip = env['ContainerImagePrepare'][0] + set = cip['set'] + + self.assertEqual( + 'one', set['namespace']) + self.assertEqual( + 'two', set['name_prefix']) + self.assertEqual( + 'three', set['name_suffix']) + self.assertEqual( + 'four', set['tag']) + self.assertEqual( + 'five', cip['tag_from_label']) + + @mock.patch('shutil.copy') + def test_container_images_file(self, mock_copy): + env = {} + deploy_args = [] + self.conf.container_images_file = '/tmp/container_images_file.yaml' + minion_config._container_images_config(self.conf, deploy_args, + env, None) + self.assertEqual(['-e', '/tmp/container_images_file.yaml'], + deploy_args) + self.assertEqual({}, env) + + @mock.patch('shutil.copy') + def test_custom(self, mock_copy): + env = {} + deploy_args = [] + with tempfile.NamedTemporaryFile(mode='w') as f: + yaml.dump({ + 'parameter_defaults': {'ContainerImagePrepare': [{ + 'set': { + 'namespace': 'one', + 'name_prefix': 'two', + 'name_suffix': 'three', + 'tag': 'four', + }, + 'tag_from_label': 'five', + }]} + }, f) + self.conf.container_images_file = f.name + cif_name = f.name + + minion_config._container_images_config( + self.conf, deploy_args, env, None) + self.assertEqual(['-e', cif_name], deploy_args) diff --git a/tripleoclient/tests/v1/undercloud/minion/test_install.py b/tripleoclient/tests/v1/undercloud/minion/test_install.py new file mode 100644 index 000000000..16ff01402 --- /dev/null +++ b/tripleoclient/tests/v1/undercloud/minion/test_install.py @@ -0,0 +1,130 @@ +# 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 fixtures +import mock + +from oslo_config import cfg +from oslo_config import fixture as oslo_fixture + +from tripleoclient.tests.v1.test_plugin import TestPluginV1 + +# Load the plugin init module for the plugin list and show commands +from tripleoclient.v1 import undercloud_minion + + +class FakePluginV1Client(object): + def __init__(self, **kwargs): + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + + +class TestMinionInstall(TestPluginV1): + + def setUp(self): + super(TestMinionInstall, self).setUp() + + self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + # don't actually load config from ~/minion.conf + self.mock_config_load = self.useFixture( + fixtures.MockPatch('tripleoclient.utils.load_config')) + # Get the command object to test + app_args = mock.Mock() + app_args.verbose_level = 1 + self.cmd = undercloud_minion.InstallUndercloudMinion(self.app, + app_args) + + @mock.patch('tripleoclient.v1.minion_config.prepare_minion_deploy') + @mock.patch('tripleoclient.utils.ensure_run_as_normal_user') + @mock.patch('tripleoclient.utils.configure_logging') + @mock.patch('subprocess.check_call', autospec=True) + def test_take_action(self, mock_subprocess, mock_logging, mock_usercheck, + mock_prepare_deploy): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + mock_prepare_deploy.return_value = ['foo'] + self.cmd.take_action(parsed_args) + mock_prepare_deploy.assert_called_once_with( + dry_run=False, force_stack_update=False, no_validations=False, + verbose_level=1) + mock_usercheck.assert_called_once() + mock_subprocess.assert_called_with(['foo']) + + @mock.patch('tripleoclient.v1.minion_config.prepare_minion_deploy') + @mock.patch('tripleoclient.utils.ensure_run_as_normal_user') + @mock.patch('tripleoclient.utils.configure_logging') + @mock.patch('subprocess.check_call', autospec=True) + def test_take_action_dry_run(self, mock_subprocess, mock_logging, + mock_usercheck, mock_prepare_deploy): + arglist = ['--dry-run'] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + mock_prepare_deploy.assert_called_once_with( + dry_run=True, force_stack_update=False, no_validations=True, + verbose_level=1) + mock_usercheck.assert_called_once() + self.assertItemsEqual(mock_subprocess.call_args_list, []) + + +class TestMinionUpgrade(TestPluginV1): + + def setUp(self): + super(TestMinionUpgrade, self).setUp() + + self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + # don't actually load config from ~/minion.conf + self.mock_config_load = self.useFixture( + fixtures.MockPatch('tripleoclient.utils.load_config')) + # Get the command object to test + app_args = mock.Mock() + app_args.verbose_level = 1 + self.cmd = undercloud_minion.UpgradeUndercloudMinion(self.app, + app_args) + + @mock.patch('tripleoclient.v1.minion_config.prepare_minion_deploy') + @mock.patch('tripleoclient.utils.ensure_run_as_normal_user') + @mock.patch('tripleoclient.utils.configure_logging') + @mock.patch('subprocess.check_call', autospec=True) + def test_take_action(self, mock_subprocess, mock_logging, mock_usercheck, + mock_prepare_deploy): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + mock_prepare_deploy.return_value = ['foo'] + self.cmd.take_action(parsed_args) + mock_prepare_deploy.assert_called_once_with( + force_stack_update=False, no_validations=False, upgrade=True, + verbose_level=1, yes=False) + mock_usercheck.assert_called_once() + mock_subprocess.assert_called_with(['foo']) + + @mock.patch('tripleoclient.v1.minion_config.prepare_minion_deploy') + @mock.patch('tripleoclient.utils.ensure_run_as_normal_user') + @mock.patch('tripleoclient.utils.configure_logging') + @mock.patch('subprocess.check_call', autospec=True) + def test_take_action_yes(self, mock_subprocess, mock_logging, + mock_usercheck, mock_prepare_deploy): + arglist = ['--yes'] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + mock_prepare_deploy.return_value = ['foo'] + self.cmd.take_action(parsed_args) + mock_prepare_deploy.assert_called_once_with( + force_stack_update=False, no_validations=False, upgrade=True, + verbose_level=1, yes=True) + mock_usercheck.assert_called_once() + mock_subprocess.assert_called_with(['foo']) diff --git a/tripleoclient/v1/minion_config.py b/tripleoclient/v1/minion_config.py new file mode 100644 index 000000000..b674dcd25 --- /dev/null +++ b/tripleoclient/v1/minion_config.py @@ -0,0 +1,405 @@ +# 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 + + +# 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', + '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 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. + tempdir = os.path.join(os.path.abspath(CONF['output_dir']), + 'tripleo-config-generated-env-files') + if not os.path.isdir(tempdir): + os.mkdir(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' % + 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] + + 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') + ] + + # TODO(aschultz): remove when podman is actual default + deploy_args += [ + '-e', os.path.join(tht_templates, 'environments/podman.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' % CONF['output_dir']] + if not os.path.isdir(CONF['output_dir']): + os.mkdir(CONF['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: + utils.ansible_symlink() + # TODO(aschultz): write this + # minion_preflight.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 diff --git a/tripleoclient/v1/tripleo_deploy.py b/tripleoclient/v1/tripleo_deploy.py index 594e4bdfa..b53cad0d5 100644 --- a/tripleoclient/v1/tripleo_deploy.py +++ b/tripleoclient/v1/tripleo_deploy.py @@ -108,8 +108,10 @@ class Deploy(command.Command): python_cmd = "python{}".format(python_version) def _is_undercloud_deploy(self, parsed_args): - return parsed_args.standalone_role == 'Undercloud' and \ - parsed_args.stack == 'undercloud' + role = parsed_args.standalone_role + stack = parsed_args.stack + return (role in ['Undercloud', 'UndercloudMinion'] and + stack in ['undercloud', 'minion']) def _run_preflight_checks(self, parsed_args): """Run preflight deployment checks @@ -855,6 +857,34 @@ class Deploy(command.Command): sys.stdout.flush() return self.tmp_ansible_dir + def _download_stack_outputs(self, client, stack_name): + stack = utils.get_stack(client, stack_name) + output_file = 'tripleo-{}-outputs.yaml'.format(stack_name) + endpointmap_file = os.path.join(self.output_dir, output_file) + + outputs = {} + endpointmap = utils.get_endpoint_map(stack) + if endpointmap: + outputs['EndpointMapOverride'] = endpointmap + + allnodescfg = utils.get_stack_output_item(stack, 'AllNodesConfig') + if allnodescfg: + outputs['AllNodesExtraMapData'] = allnodescfg + + hosts = utils.get_stack_output_item(stack, 'HostsEntry') + if hosts: + outputs['ExtraHostFileEntries'] = hosts + + globalcfg = utils.get_stack_output_item(stack, 'GlobalConfig') + if globalcfg: + outputs['GlobalConfigExtraMapData'] = globalcfg + + self._create_working_dirs() + output = {'parameter_defaults': outputs} + with open(endpointmap_file, 'w') as f: + yaml.safe_dump(output, f, default_flow_style=False) + return output + # Never returns, calls exec() def _launch_ansible(self, ansible_dir, list_args=None, operation="deploy"): @@ -1219,6 +1249,10 @@ class Deploy(command.Command): parsed_args.standalone_role, depl_python) + # output an file with EndpointMapOverride for use with other stacks + self._download_stack_outputs(orchestration_client, + parsed_args.stack) + # Do not override user's custom ansible configuraition file, # it may have been pre-created with the tripleo CLI, or the like ansible_config = os.path.join(self.output_dir, 'ansible.cfg') diff --git a/tripleoclient/v1/undercloud_minion.py b/tripleoclient/v1/undercloud_minion.py new file mode 100644 index 000000000..cc244d79f --- /dev/null +++ b/tripleoclient/v1/undercloud_minion.py @@ -0,0 +1,156 @@ +# 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 argparse +import logging +import subprocess + +from openstackclient.i18n import _ + +from oslo_config import cfg + +from tripleoclient import command +from tripleoclient import constants +from tripleoclient import exceptions +from tripleoclient import utils +from tripleoclient.v1 import minion_config + +MINION_FAILURE_MESSAGE = """ +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +An error has occured while deploying the Undercloud Minion + +See the previous output for details about what went wrong. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +""" + +MINION_COMPLETION_MESSAGE = """ +########################################################## + +The Undercloud Minion has been successfully installed. + +########################################################## +""" +MINION_UPGRADE_COMPLETION_MESSAGE = """ +########################################################## + +The Undercloud Minion has been successfully upgraded. + +########################################################## +""" + + +class InstallUndercloudMinion(command.Command): + """Install and setup the undercloud minion""" + + auth_required = False + log = logging.getLogger(__name__ + ".InstallUndercloudMinion") + osloconfig = cfg.CONF + + def get_parser(self, prog_name): + parser = argparse.ArgumentParser( + description=self.get_description(), + prog=prog_name, + add_help=False + ) + parser.add_argument('--force-stack-update', + dest='force_stack_update', + action='store_true', + default=False, + help=_("Do a virtual update of the ephemeral " + "heat stack. New or failed deployments " + "always have the stack_action=CREATE. This " + "option enforces stack_action=UPDATE."), + ) + parser.add_argument( + '--no-validations', + dest='no_validations', + action='store_true', + default=False, + help=_("Do not perform minion configuration validations"), + ) + parser.add_argument( + '--dry-run', + dest='dry_run', + action='store_true', + default=False, + help=_("Print the install command instead of running it"), + ) + parser.add_argument('-y', '--yes', default=False, + action='store_true', + help=_("Skip yes/no prompt (assume yes).")) + return parser + + def take_action(self, parsed_args): + # Fetch configuration used to add logging to a file + utils.load_config(self.osloconfig, constants.MINION_CONF_PATH) + utils.configure_logging(self.log, self.app_args.verbose_level, + self.osloconfig['minion_log_file']) + self.log.debug("take_action(%s)" % parsed_args) + + utils.ensure_run_as_normal_user() + no_validations = parsed_args.dry_run or parsed_args.no_validations + cmd = minion_config.prepare_minion_deploy( + no_validations=no_validations, + verbose_level=self.app_args.verbose_level, + force_stack_update=parsed_args.force_stack_update, + dry_run=parsed_args.dry_run) + + self.log.warning("Running: %s" % ' '.join(cmd)) + if not parsed_args.dry_run: + try: + subprocess.check_call(cmd) + self.log.warning(MINION_COMPLETION_MESSAGE) + except Exception as e: + self.log.error(MINION_FAILURE_MESSAGE) + self.log.error(e) + raise exceptions.DeploymentError(e) + + +class UpgradeUndercloudMinion(InstallUndercloudMinion): + """Upgrade undercloud minion""" + + auth_required = False + log = logging.getLogger(__name__ + ".UpgradeUndercloudMinion") + osloconfig = cfg.CONF + + def take_action(self, parsed_args): + # Fetch configuration used to add logging to a file + utils.load_config(self.osloconfig, constants.MINION_CONF_PATH) + utils.configure_logging(self.log, self.app_args.verbose_level, + self.osloconfig['minion_log_file']) + self.log.debug("take action(%s)" % parsed_args) + + utils.ensure_run_as_normal_user() + cmd = minion_config.\ + prepare_minion_deploy( + upgrade=True, + yes=parsed_args.yes, + no_validations=parsed_args. + no_validations, + verbose_level=self.app_args.verbose_level, + force_stack_update=parsed_args.force_stack_update) + self.log.warning("Running: %s" % ' '.join(cmd)) + if not parsed_args.dry_run: + try: + subprocess.check_call(cmd) + self.log.warning(MINION_UPGRADE_COMPLETION_MESSAGE) + except Exception as e: + self.log.error(MINION_FAILURE_MESSAGE) + self.log.error(e) + raise exceptions.DeploymentError(e)