Browse Source

Add minion installation

Add the ability to install an undercloud minion which is connected to an
original undercloud. This minion can have either heat-engine or
ironic-conductor deployed on it.

Adds two new openstack commands for the minion install and a new
minion.conf can be used to configure them.

openstack undercloud minion install
openstack undercloud minion upgrade

Depends-On: https://review.opendev.org/#/c/656984
Change-Id: I61832f5088be172eaf31b36a9cca8dc289580bb2
Related-Blueprint: undercloud-minion
changes/97/656997/24
Alex Schultz 2 years ago
parent
commit
8de77cbe70
  1. 1
      .gitignore
  2. 3
      config-generator/minion.conf
  3. 6
      releasenotes/notes/undercloud-minion-install-6b369d8f5f3d6a89.yaml
  4. 3
      setup.cfg
  5. 1
      tox.ini
  6. 159
      tripleoclient/config/minion.py
  7. 9
      tripleoclient/config/standalone.py
  8. 5
      tripleoclient/config/undercloud.py
  9. 4
      tripleoclient/constants.py
  10. 103
      tripleoclient/tests/config/test_config_minion.py
  11. 2
      tripleoclient/tests/config/test_config_standalone.py
  12. 4
      tripleoclient/tests/v1/tripleo/test_tripleo_deploy.py
  13. 0
      tripleoclient/tests/v1/undercloud/minion/__init__.py
  14. 200
      tripleoclient/tests/v1/undercloud/minion/test_config.py
  15. 130
      tripleoclient/tests/v1/undercloud/minion/test_install.py
  16. 405
      tripleoclient/v1/minion_config.py
  17. 38
      tripleoclient/v1/tripleo_deploy.py
  18. 156
      tripleoclient/v1/undercloud_minion.py

1
.gitignore

@ -34,6 +34,7 @@ lib64
# Installer logs
pip-log.txt
install-undercloud.log
install-minion.log
# Unit test / coverage reports
.coverage

3
config-generator/minion.conf

@ -0,0 +1,3 @@
[DEFAULT]
output_file = minion.conf.sample
namespace = minion_config

6
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.

3
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

1
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

159
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)

9
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 '
'<role>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)

5
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 '

4
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'

103
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]))

2
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',

4
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',

0
tripleoclient/tests/v1/undercloud/minion/__init__.py

200
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)

130
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'])

405
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('&quot;', '"')
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

38
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')

156
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)
Loading…
Cancel
Save