From 96ffa3a325544206b2fa542bf52bb76531f7d4d6 Mon Sep 17 00:00:00 2001 From: mandreou Date: Thu, 22 Feb 2018 18:21:09 +0200 Subject: [PATCH] Adds Overcloud Upgrade class and entry points for major upgrade This further refactors the update/upgrades cli and separates the update and upgrade code. This adds the overcloud_upgrade.py which defines the UpgradePrepare and UpgradeRun. The entry points are now: openstack overcloud upgrade prepare --container-registry-file ... For the no-op heat stack update to refresh stack outputs and openstack overcloud upgrade run --nodes foo --playbooks all For running all the upgrade ansible playbooks. A corresponding converge sub-command will be introduced in a subsequent patch. Change-Id: I1880e8f546df8d509871ba3b4f02877e95c611c8 --- setup.cfg | 4 +- tripleoclient/constants.py | 5 + .../tests/v1/overcloud_upgrade/__init__.py | 0 .../tests/v1/overcloud_upgrade/fakes.py | 61 ++++++ .../test_overcloud_upgrade.py | 188 ++++++++++++++++++ tripleoclient/utils.py | 20 ++ tripleoclient/v1/overcloud_update.py | 43 +--- tripleoclient/v1/overcloud_upgrade.py | 184 +++++++++++++++++ 8 files changed, 463 insertions(+), 42 deletions(-) create mode 100644 tripleoclient/tests/v1/overcloud_upgrade/__init__.py create mode 100644 tripleoclient/tests/v1/overcloud_upgrade/fakes.py create mode 100644 tripleoclient/tests/v1/overcloud_upgrade/test_overcloud_upgrade.py create mode 100644 tripleoclient/v1/overcloud_upgrade.py diff --git a/setup.cfg b/setup.cfg index 0636fef3d..ba78b3221 100644 --- a/setup.cfg +++ b/setup.cfg @@ -91,7 +91,9 @@ openstack.tripleoclient.v1 = overcloud_support_report_collect = tripleoclient.v1.overcloud_support:ReportExecute overcloud_update_prepare= tripleoclient.v1.overcloud_update:UpdatePrepare overcloud_update_run = tripleoclient.v1.overcloud_update:UpdateRun - overcloud_upgrade_converge = tripleoclient.v1.overcloud_update:UpgradeConvergeOvercloud + overcloud_upgrade_prepare = tripleoclient.v1.overcloud_upgrade:UpgradePrepare + overcloud_upgrade_run = tripleoclient.v1.overcloud_upgrade:UpgradeRun + overcloud_upgrade_converge = tripleoclient.v1.overcloud_upgrade:UpgradeConvergeOvercloud overcloud_execute = tripleoclient.v1.overcloud_execute:RemoteExecute overcloud_generate_fencing = tripleoclient.v1.overcloud_parameters:GenerateFencingParameters undercloud_deploy = tripleoclient.v1.undercloud_deploy:DeployUndercloud diff --git a/tripleoclient/constants.py b/tripleoclient/constants.py index 450f0102c..f632590af 100644 --- a/tripleoclient/constants.py +++ b/tripleoclient/constants.py @@ -35,8 +35,13 @@ PUPPET_MODULES = "/etc/puppet/modules/" PUPPET_BASE = "/etc/puppet/" # Update Queue UPDATE_QUEUE = 'update' +UPGRADE_QUEUE = 'upgrade' STACK_TIMEOUT = 240 # The default minor update ansible playbooks generated from heat stack output MINOR_UPDATE_PLAYBOOKS = ['update_steps_playbook.yaml', 'deploy_steps_playbook.yaml'] +# The default major upgrade ansible playbooks generated from heat stack output +MAJOR_UPGRADE_PLAYBOOKS = ["upgrade_steps_playbook.yaml", + "deploy_steps_playbook.yaml", + "post_upgrade_steps_playbook.yaml"] diff --git a/tripleoclient/tests/v1/overcloud_upgrade/__init__.py b/tripleoclient/tests/v1/overcloud_upgrade/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleoclient/tests/v1/overcloud_upgrade/fakes.py b/tripleoclient/tests/v1/overcloud_upgrade/fakes.py new file mode 100644 index 000000000..ffb0e2c4a --- /dev/null +++ b/tripleoclient/tests/v1/overcloud_upgrade/fakes.py @@ -0,0 +1,61 @@ +# 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 mock +from osc_lib.tests import utils + +from tripleoclient.tests import fakes + + +class FakeClientWrapper(object): + + def __init__(self): + self._instance = mock.Mock() + self.object_store = FakeObjectClient() + + def messaging_websocket(self): + return fakes.FakeWebSocket() + + +class FakeObjectClient(object): + + def __init__(self): + self._instance = mock.Mock() + self.put_object = mock.Mock() + + def get_object(self, *args): + return + + +class TestOvercloudUpgradePrepare(utils.TestCommand): + + def setUp(self): + super(TestOvercloudUpgradePrepare, self).setUp() + + self.app.client_manager.auth_ref = mock.Mock(auth_token="TOKEN") + self.app.client_manager.baremetal = mock.Mock() + self.app.client_manager.orchestration = mock.Mock() + self.app.client_manager.tripleoclient = FakeClientWrapper() + self.app.client_manager.workflow_engine = mock.Mock() + + +class TestOvercloudUpgradeRun(utils.TestCommand): + + def setUp(self): + super(TestOvercloudUpgradeRun, self).setUp() + + self.app.client_manager.auth_ref = mock.Mock(auth_token="TOKEN") + self.app.client_manager.tripleoclient = FakeClientWrapper() + self.app.client_manager.workflow_engine = mock.Mock() diff --git a/tripleoclient/tests/v1/overcloud_upgrade/test_overcloud_upgrade.py b/tripleoclient/tests/v1/overcloud_upgrade/test_overcloud_upgrade.py new file mode 100644 index 000000000..0d004baef --- /dev/null +++ b/tripleoclient/tests/v1/overcloud_upgrade/test_overcloud_upgrade.py @@ -0,0 +1,188 @@ +# 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. +# + +import mock + +from osc_lib.tests.utils import ParserException +from tripleoclient import constants +from tripleoclient import exceptions +from tripleoclient.tests.v1.overcloud_upgrade import fakes +from tripleoclient.v1 import overcloud_upgrade + + +class TestOvercloudUpgradePrepare(fakes.TestOvercloudUpgradePrepare): + + def setUp(self): + super(TestOvercloudUpgradePrepare, self).setUp() + + # Get the command object to test + app_args = mock.Mock() + app_args.verbose_level = 1 + self.cmd = overcloud_upgrade.UpgradePrepare(self.app, app_args) + + uuid4_patcher = mock.patch('uuid.uuid4', return_value="UUID4") + self.mock_uuid4 = uuid4_patcher.start() + self.addCleanup(self.mock_uuid4.stop) + + @mock.patch('tripleoclient.utils.get_stack', + autospec=True) + @mock.patch('tripleoclient.v1.overcloud_upgrade.UpgradePrepare.log', + autospec=True) + @mock.patch('tripleoclient.workflows.package_update.update', + autospec=True) + @mock.patch('os.path.abspath') + @mock.patch('yaml.load') + @mock.patch('shutil.copytree', autospec=True) + @mock.patch('six.moves.builtins.open') + @mock.patch('tripleoclient.v1.overcloud_deploy.DeployOvercloud.' + '_deploy_tripleo_heat_templates', autospec=True) + def test_upgrade_out(self, mock_deploy, mock_open, mock_copy, mock_yaml, + mock_abspath, mock_upgrade, mock_logger, + mock_get_stack): + mock_stack = mock.Mock() + mock_stack.stack_name = 'mystack' + mock_get_stack.return_value = mock_stack + mock_abspath.return_value = '/home/fake/my-fake-registry.yaml' + mock_yaml.return_value = {'fake_container': 'fake_value'} + + argslist = ['--stack', 'overcloud', '--templates', + '--container-registry-file', 'my-fake-registry.yaml'] + verifylist = [ + ('stack', 'overcloud'), + ('templates', constants.TRIPLEO_HEAT_TEMPLATES), + ('container_registry_file', 'my-fake-registry.yaml') + ] + + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + self.cmd.take_action(parsed_args) + mock_upgrade.assert_called_once_with( + self.app.client_manager, + container='mystack', + container_registry={'fake_container': 'fake_value'}, + ceph_ansible_playbook='/usr/share/ceph-ansible' + '/site-docker.yml.sample' + ) + + @mock.patch('tripleoclient.workflows.package_update.update', + autospec=True) + @mock.patch('six.moves.builtins.open') + @mock.patch('os.path.abspath') + @mock.patch('yaml.load') + @mock.patch('shutil.copytree', autospec=True) + @mock.patch('tripleoclient.v1.overcloud_deploy.DeployOvercloud.' + '_deploy_tripleo_heat_templates', autospec=True) + def test_upgrade_failed(self, mock_deploy, mock_copy, mock_yaml, + mock_abspath, mock_open, mock_upgrade): + mock_upgrade.side_effect = exceptions.DeploymentError() + mock_abspath.return_value = '/home/fake/my-fake-registry.yaml' + mock_yaml.return_value = {'fake_container': 'fake_value'} + argslist = ['--stack', 'overcloud', '--templates', + '--container-registry-file', 'my-fake-registry.yaml'] + verifylist = [ + ('stack', 'overcloud'), + ('templates', constants.TRIPLEO_HEAT_TEMPLATES), + ('container_registry_file', 'my-fake-registry.yaml') + ] + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + + self.assertRaises(exceptions.DeploymentError, + self.cmd.take_action, parsed_args) + + +class TestOvercloudUpgradeRun(fakes.TestOvercloudUpgradeRun): + + def setUp(self): + super(TestOvercloudUpgradeRun, self).setUp() + + # Get the command object to test + app_args = mock.Mock() + app_args.verbose_level = 1 + self.cmd = overcloud_upgrade.UpgradeRun(self.app, app_args) + + uuid4_patcher = mock.patch('uuid.uuid4', return_value="UUID4") + self.mock_uuid4 = uuid4_patcher.start() + self.addCleanup(self.mock_uuid4.stop) + + @mock.patch('tripleoclient.workflows.package_update.update_ansible', + autospec=True) + @mock.patch('os.path.expanduser') + @mock.patch('oslo_concurrency.processutils.execute') + @mock.patch('six.moves.builtins.open') + def test_upgrade_with_playbook(self, mock_open, mock_execute, + mock_expanduser, upgrade_ansible): + mock_expanduser.return_value = '/home/fake/' + argslist = ['--nodes', 'Compute', '--playbook', + 'fake-playbook.yaml'] + verifylist = [ + ('nodes', 'Compute'), + ('static_inventory', None), + ('playbook', 'fake-playbook.yaml') + ] + + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + with mock.patch('os.path.exists') as mock_exists: + mock_exists.return_value = True + self.cmd.take_action(parsed_args) + upgrade_ansible.assert_called_once_with( + self.app.client_manager, + nodes='Compute', + inventory_file=mock_open().read(), + playbook='fake-playbook.yaml', + ansible_queue_name=constants.UPGRADE_QUEUE + ) + + @mock.patch('tripleoclient.workflows.package_update.update_ansible', + autospec=True) + @mock.patch('os.path.expanduser') + @mock.patch('oslo_concurrency.processutils.execute') + @mock.patch('six.moves.builtins.open') + def test_upgrade_with_all_playbooks(self, mock_open, mock_execute, + mock_expanduser, upgrade_ansible): + mock_expanduser.return_value = '/home/fake/' + argslist = ['--nodes', 'Compute', '--playbook', 'all'] + verifylist = [ + ('nodes', 'Compute'), + ('static_inventory', None), + ('playbook', 'all') + ] + + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + with mock.patch('os.path.exists') as mock_exists: + mock_exists.return_value = True + self.cmd.take_action(parsed_args) + for book in constants.MAJOR_UPGRADE_PLAYBOOKS: + upgrade_ansible.assert_any_call( + self.app.client_manager, + nodes='Compute', + inventory_file=mock_open().read(), + playbook=book, + ansible_queue_name=constants.UPGRADE_QUEUE + ) + + @mock.patch('tripleoclient.workflows.package_update.update_ansible', + autospec=True) + @mock.patch('os.path.expanduser') + @mock.patch('oslo_concurrency.processutils.execute') + @mock.patch('six.moves.builtins.open') + def test_upgrade_with_no_nodes(self, mock_open, mock_execute, + mock_expanduser, upgrade_ansible): + mock_expanduser.return_value = '/home/fake/' + argslist = [] + verifylist = [ + ('static_inventory', None), + ('playbook', 'all') + ] + self.assertRaises(ParserException, lambda: self.check_parser( + self.cmd, argslist, verifylist)) diff --git a/tripleoclient/utils.py b/tripleoclient/utils.py index 6344ae25f..88b25b651 100644 --- a/tripleoclient/utils.py +++ b/tripleoclient/utils.py @@ -33,6 +33,7 @@ import yaml from heatclient.common import event_utils from heatclient.exc import HTTPNotFound from osc_lib.i18n import _ +from oslo_concurrency import processutils from six.moves import configparser from tripleoclient import exceptions @@ -810,3 +811,22 @@ def load_environment_directories(directories): if os.path.isfile(f): environments.append(f) return environments + + +def get_tripleo_ansible_inventory(inventory_file=''): + if not inventory_file: + inventory_file = '%s/%s' % (os.path.expanduser('~'), + 'tripleo-ansible-inventory.yaml') + try: + processutils.execute( + '/usr/bin/tripleo-ansible-inventory', + '--static-yaml-inventory', inventory_file) + except processutils.ProcessExecutionError as e: + message = "Failed to generate inventory: %s" % str(e) + raise exceptions.InvalidConfiguration(message) + if os.path.exists(inventory_file): + inventory = open(inventory_file, 'r').read() + return inventory + else: + raise exceptions.InvalidConfiguration( + "Inventory file %s can not be found." % inventory_file) diff --git a/tripleoclient/v1/overcloud_update.py b/tripleoclient/v1/overcloud_update.py index e35b236f8..f9e4d2d17 100644 --- a/tripleoclient/v1/overcloud_update.py +++ b/tripleoclient/v1/overcloud_update.py @@ -18,11 +18,9 @@ import os import yaml from osc_lib.i18n import _ -from oslo_concurrency import processutils from tripleoclient import command from tripleoclient import constants -from tripleoclient import exceptions from tripleoclient import utils as oooutils from tripleoclient.v1.overcloud_deploy import DeployOvercloud from tripleoclient.workflows import package_update @@ -155,22 +153,8 @@ class UpdateRun(command.Command): # unset this, the ansible action deals with unset 'limithosts' nodes = None playbook = parsed_args.playbook - inventory_file = parsed_args.static_inventory - if inventory_file is None: - inventory_file = '%s/%s' % (os.path.expanduser('~'), - 'tripleo-ansible-inventory.yaml') - try: - processutils.execute( - '/usr/bin/tripleo-ansible-inventory', - '--static-yaml-inventory', inventory_file) - except processutils.ProcessExecutionError as e: - message = "Failed to generate inventory: %s" % str(e) - raise exceptions.InvalidConfiguration(message) - if os.path.exists(inventory_file): - inventory = open(inventory_file, 'r').read() - else: - raise exceptions.InvalidConfiguration( - "Inventory file %s can not be found." % inventory_file) + inventory = oooutils.get_tripleo_ansible_inventory( + parsed_args.static_inventory) update_playbooks = [playbook] if playbook == "all": update_playbooks = constants.MINOR_UPDATE_PLAYBOOKS @@ -181,26 +165,3 @@ class UpdateRun(command.Command): inventory_file=inventory, playbook=book, ansible_queue_name=constants.UPDATE_QUEUE) - - -class UpgradeConvergeOvercloud(DeployOvercloud): - """Converge the upgrade on Overcloud Nodes""" - - log = logging.getLogger(__name__ + ".UpgradeConvergeOvercloud") - - def get_parser(self, prog_name): - parser = super(UpgradeConvergeOvercloud, self).get_parser(prog_name) - return parser - - def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) - clients = self.app.client_manager - - stack = oooutils.get_stack(clients.orchestration, - parsed_args.stack) - stack_name = stack.stack_name - - parsed_args.update_plan_only = True - super(UpgradeConvergeOvercloud, self).take_action(parsed_args) - # Run converge steps - package_update.converge_nodes(clients, container=stack_name) diff --git a/tripleoclient/v1/overcloud_upgrade.py b/tripleoclient/v1/overcloud_upgrade.py new file mode 100644 index 000000000..bb100cfb5 --- /dev/null +++ b/tripleoclient/v1/overcloud_upgrade.py @@ -0,0 +1,184 @@ +# 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. +# +import logging +import os +import yaml + +from osc_lib.i18n import _ + +from tripleoclient import command +from tripleoclient import constants +from tripleoclient import utils as oooutils +from tripleoclient.v1.overcloud_deploy import DeployOvercloud +from tripleoclient.workflows import package_update + + +class UpgradePrepare(DeployOvercloud): + """Run heat stack update for overcloud nodes to refresh heat stack outputs. + + The heat stack outputs are what we use later on to generate ansible + playbooks which deliver the major upgrade workflow. This is used as the + first step for a major upgrade of your overcloud. + """ + + log = logging.getLogger(__name__ + ".MajorUpgradePrepare") + + # enable preservation of all important files (plan env, user env, + # roles/network data, user files) so that we don't have to pass + # all env files on update command + _keep_env_on_update = True + + def get_parser(self, prog_name): + parser = super(UpgradePrepare, self).get_parser(prog_name) + parser.add_argument('--container-registry-file', + dest='container_registry_file', + default=None, + help=_("File which contains the container " + "registry data for the upgrade"), + ) + parser.add_argument('--ceph-ansible-playbook', + action="store", + default="/usr/share/ceph-ansible" + "/site-docker.yml.sample", + help=_('Path to switch the ceph-ansible playbook ' + 'used for upgrade. '), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + clients = self.app.client_manager + + stack = oooutils.get_stack(clients.orchestration, + parsed_args.stack) + + stack_name = stack.stack_name + container_registry = parsed_args.container_registry_file + + # Update the container registry: + if container_registry: + with open(os.path.abspath(container_registry)) as content: + registry = yaml.load(content.read()) + else: + self.log.warning( + "You have not provided a container registry file. Note " + "that none of the containers on your environement will be " + "updated. If you want to update your container you have " + "to re-run this command and provide the registry file " + "with: --container-registry-file option.") + registry = None + # Run update + ceph_ansible_playbook = parsed_args.ceph_ansible_playbook + # Run Overcloud deploy (stack update) + # In case of update and upgrade we need to force the + # update_plan_only. The heat stack update is done by the + # packag_update mistral action + parsed_args.update_plan_only = True + super(UpgradePrepare, self).take_action(parsed_args) + package_update.update(clients, container=stack_name, + container_registry=registry, + ceph_ansible_playbook=ceph_ansible_playbook) + package_update.get_config(clients, container=stack_name) + print("Update init on stack {0} complete.".format( + parsed_args.stack)) + + +class UpgradeRun(command.Command): + """Run major upgrade ansible playbooks on Overcloud nodes""" + + log = logging.getLogger(__name__ + ".MajorUpgradeRun") + + def get_parser(self, prog_name): + parser = super(UpgradeRun, self).get_parser(prog_name) + parser.add_argument('--nodes', + action="store", + required=True, + help=_("Required parameter. This specifies the " + "overcloud nodes to run the major upgrade " + "playbooks on. You can use the name of " + "a specific node, or the name of the role " + "(e.g. Compute).") + ) + parser.add_argument('--playbook', + action="store", + default="all", + help=_("Ansible playbook to use for the major " + "upgrade. Defaults to the special value " + "\'all\' which causes all the upgrade " + "playbooks to run. That is the " + "upgrade_steps_playbook.yaml " + "then deploy_steps_playbook.yaml and then " + "post_upgrade_steps_playbooks.yaml. Set " + "this to each of those playbooks in " + "consecutive invocations of this command " + "if you prefer to run them manually. Note: " + "you will have to run all of those " + "playbooks so that all services are " + "upgraded and running with the target " + "version configuration.") + ) + parser.add_argument('--static-inventory', + dest='static_inventory', + action="store", + default=None, + help=_('Path to an existing ansible inventory to ' + 'use. If not specified, one will be ' + 'generated in ' + '~/tripleo-ansible-inventory.yaml') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + clients = self.app.client_manager + + # Run ansible: + nodes = parsed_args.nodes + playbook = parsed_args.playbook + inventory = oooutils.get_tripleo_ansible_inventory( + parsed_args.static_inventory) + upgrade_playbooks = [playbook] + if playbook == "all": + upgrade_playbooks = constants.MAJOR_UPGRADE_PLAYBOOKS + for book in upgrade_playbooks: + self.log.debug("Running major upgrade ansible playbook %s " % book) + package_update.update_ansible( + clients, nodes=nodes, + inventory_file=inventory, + playbook=book, + ansible_queue_name=constants.UPGRADE_QUEUE) + + +class UpgradeConvergeOvercloud(DeployOvercloud): + """Converge the upgrade on Overcloud Nodes""" + + log = logging.getLogger(__name__ + ".UpgradeConvergeOvercloud") + + def get_parser(self, prog_name): + parser = super(UpgradeConvergeOvercloud, self).get_parser(prog_name) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + clients = self.app.client_manager + + stack = oooutils.get_stack(clients.orchestration, + parsed_args.stack) + stack_name = stack.stack_name + + parsed_args.update_plan_only = True + super(UpgradeConvergeOvercloud, self).take_action(parsed_args) + # Run converge steps + package_update.converge_nodes(clients, container=stack_name)