diff --git a/setup.cfg b/setup.cfg index abbd0724e..e6566e236 100644 --- a/setup.cfg +++ b/setup.cfg @@ -90,6 +90,7 @@ openstack.tripleoclient.v2 = overcloud_external-upgrade_run = tripleoclient.v1.overcloud_external_upgrade:ExternalUpgradeRun overcloud_generate_fencing = tripleoclient.v1.overcloud_parameters:GenerateFencingParameters overcloud_backup = tripleoclient.v1.overcloud_backup:BackupOvercloud + overcloud_backup_snapshot = tripleoclient.v1.overcloud_backup:BackupSnapshot overcloud_restore = tripleoclient.v1.overcloud_restore:RestoreOvercloud tripleo_container_image_build = tripleoclient.v2.tripleo_container_image:Build tripleo_container_image_hotfix = tripleoclient.v2.tripleo_container_image:HotFix diff --git a/tripleoclient/tests/v1/overcloud_backup/test_backup.py b/tripleoclient/tests/v1/overcloud_backup/test_backup.py index 413db3dfe..44693d9cf 100644 --- a/tripleoclient/tests/v1/overcloud_backup/test_backup.py +++ b/tripleoclient/tests/v1/overcloud_backup/test_backup.py @@ -366,3 +366,137 @@ class TestOvercloudBackup(utils.TestCommand): 'The inventory file', self.cmd.take_action, parsed_args) + + +class TestOvercloudSnapshot(utils.TestCommand): + + def setUp(self): + super(TestOvercloudSnapshot, self).setUp() + + # Get the command object to test + app_args = mock.Mock() + app_args.verbose_level = 1 + self.app.options = fakes.FakeOptions() + self.cmd = overcloud_backup.BackupSnapshot(self.app, app_args) + self.app.client_manager.workflow_engine = mock.Mock() + self.workflow = self.app.client_manager.workflow_engine + self.inventory = '/tmp/test_inventory.yaml' + self.file = open(self.inventory, 'w').close() + + @mock.patch('os.path.isfile') + @mock.patch('os.access') + @mock.patch('tripleoclient.utils.run_ansible_playbook', + autospec=True) + def test_overcloud_snapshot_noargs(self, + mock_playbook, + mock_access, + mock_isfile): + arglist = [] + verifylist = [] + mock_isfile.return_value = True + mock_access.return_value = True + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + mock_playbook.assert_called_once_with( + workdir=mock.ANY, + playbook='cli-overcloud-snapshot.yaml', + inventory=parsed_args.inventory, + tags=None, + skip_tags=None, + playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, + verbosity=3, + extra_vars={} + ) + + @mock.patch('tripleoclient.utils.run_ansible_playbook', autospec=True) + def test_overcloud_snapshot_inventory(self, mock_playbook): + arglist = [ + '--inventory', + self.inventory + ] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + mock_playbook.assert_called_once_with( + workdir=mock.ANY, + playbook='cli-overcloud-snapshot.yaml', + inventory=parsed_args.inventory, + tags=None, + skip_tags=None, + playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, + verbosity=3, + extra_vars={} + ) + + @mock.patch('os.path.isfile') + @mock.patch('os.access') + @mock.patch('tripleoclient.utils.run_ansible_playbook', + autospec=True) + def test_overcloud_snapshot_extra_vars_inline(self, + mock_playbook, + mock_access, + mock_isfile): + arglist = [ + '--extra-vars', + '{"tripleo_snapshot_revert_var_size": "2G"}' + ] + verifylist = [] + mock_isfile.return_value = True + mock_access.return_value = True + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + extra_vars_dict = { + 'tripleo_snapshot_revert_var_size': '2G' + } + + self.cmd.take_action(parsed_args) + mock_playbook.assert_called_once_with( + workdir=mock.ANY, + playbook='cli-overcloud-snapshot.yaml', + inventory=parsed_args.inventory, + tags=None, + skip_tags=None, + playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, + verbosity=3, + extra_vars=extra_vars_dict + ) + + @mock.patch('tripleoclient.utils.run_ansible_playbook', + autospec=True) + def test_overcloud_backup_no_inventory(self, mock_playbook): + arglist = [ + '--inventory', + '/tmp/no_inventory.yaml' + ] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaisesRegex( + RuntimeError, + 'The inventory file', + self.cmd.take_action, + parsed_args) + + @mock.patch('os.access') + @mock.patch('tripleoclient.utils.run_ansible_playbook', + autospec=True) + def test_overcloud_backup_no_readable_inventory(self, + mock_playbook, + mock_access): + arglist = [ + '--inventory', + self.inventory + ] + verifylist = [] + mock_access.return_value = False + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaisesRegex( + RuntimeError, + 'The inventory file', + self.cmd.take_action, + parsed_args) diff --git a/tripleoclient/v1/overcloud_backup.py b/tripleoclient/v1/overcloud_backup.py index c2670213b..29d45b439 100644 --- a/tripleoclient/v1/overcloud_backup.py +++ b/tripleoclient/v1/overcloud_backup.py @@ -24,14 +24,14 @@ from osc_lib.i18n import _ from tripleoclient import constants from tripleoclient import utils -LOG = logging.getLogger(__name__ + ".BackupOvercloud") - INVENTORY = constants.ANSIBLE_INVENTORY.format('overcloud') class BackupOvercloud(command.Command): """Backup the Overcloud""" + log = logging.getLogger(__name__ + ".BackupOvercloud") + def get_parser(self, prog_name): parser = argparse.ArgumentParser( description=self.get_description(), @@ -160,52 +160,52 @@ class BackupOvercloud(command.Command): if not (os.path.isfile(parsed_args.inventory) and os.access(parsed_args.inventory, os.R_OK)): raise RuntimeError( - _('The inventory file {} does not exist or is not ' - 'readable'.format(parsed_args.inventory))) + _('The inventory file {} does not exist or is not ' + 'readable'.format(parsed_args.inventory))) if parsed_args.setup_nfs is True or parsed_args.init == 'nfs': - LOG.debug(_('Setting up NFS Backup node')) + self.log.debug(_('Setting up NFS Backup node')) self._run_ansible_playbook( - playbook='prepare-nfs-backup.yaml', - inventory=parsed_args.inventory, - tags='bar_setup_nfs_server', - skip_tags=None, - extra_vars=extra_vars - ) + playbook='prepare-nfs-backup.yaml', + inventory=parsed_args.inventory, + tags='bar_setup_nfs_server', + skip_tags=None, + extra_vars=extra_vars + ) if parsed_args.setup_rear is True or parsed_args.init == 'rear': - LOG.debug(_('Installing ReaR on controller nodes')) + self.log.debug(_('Installing ReaR on controller nodes')) self._run_ansible_playbook( - playbook='prepare-overcloud-backup.yaml', - inventory=parsed_args.inventory, - tags='bar_setup_rear', - skip_tags=None, - extra_vars=extra_vars - ) + playbook='prepare-overcloud-backup.yaml', + inventory=parsed_args.inventory, + tags='bar_setup_rear', + skip_tags=None, + extra_vars=extra_vars + ) if parsed_args.setup_ironic is True or parsed_args.init == 'ironic': - LOG.debug(_('Installing and configuring Rear/Ironic on nodes')) + self.log.debug(_('Installing Rear/Ironic on nodes')) self._run_ansible_playbook( - playbook='cli-overcloud-conf-ironic.yaml', - inventory=parsed_args.inventory, - tags='bar_setup_rear', - skip_tags=None, - extra_vars=extra_vars - ) + playbook='cli-overcloud-conf-ironic.yaml', + inventory=parsed_args.inventory, + tags='bar_setup_rear', + skip_tags=None, + extra_vars=extra_vars + ) if parsed_args.cron is True: - LOG.debug(_('Programming cron backup')) + self.log.debug(_('Programming cron backup')) self._run_ansible_playbook( - playbook='cli-overcloud-backup-cron.yaml', - inventory=parsed_args.inventory, - tags=None, - skip_tags=None, - extra_vars=extra_vars - ) + playbook='cli-overcloud-backup-cron.yaml', + inventory=parsed_args.inventory, + tags=None, + skip_tags=None, + extra_vars=extra_vars + ) if (parsed_args.setup_nfs is False and parsed_args.setup_rear is False and @@ -213,14 +213,14 @@ class BackupOvercloud(command.Command): parsed_args.cron is False and parsed_args.init is None): - LOG.debug(_('Starting Overcloud Backup')) + self.log.debug(_('Starting Overcloud Backup')) self._run_ansible_playbook( - playbook='cli-overcloud-backup.yaml', - inventory=parsed_args.inventory, - tags='bar_create_recover_image', - skip_tags=None, - extra_vars=extra_vars - ) + playbook='cli-overcloud-backup.yaml', + inventory=parsed_args.inventory, + tags='bar_create_recover_image', + skip_tags=None, + extra_vars=extra_vars + ) def _run_ansible_playbook(self, playbook, @@ -246,8 +246,8 @@ class BackupOvercloud(command.Command): if parsed_args.init: - LOG.warning("The following flags will be deprecated:" - "[--init, --storage-ip]") + self.log.warning("The following flags will be deprecated:" + "[--init, --storage-ip]") self._run_backup_overcloud(parsed_args) print( @@ -262,3 +262,111 @@ class BackupOvercloud(command.Command): ' # .-Stay safe and avoid future issues-. #\n' ' #############################################################\n' ) + + +class BackupSnapshot(command.Command): + """Takes and LVM snapshot ignoring all the rest + of the parameters passed. To be able to take + a snapshot, the following conditions must + be met: + - The disk must be configured to use LVM + - There must be an lv called lv_snapshot + - lv_snapshot must be 8GB or more + This operation will destroy the lv_snapshot volume + and replace it with snapshots of the disks. + """ + + log = logging.getLogger(__name__ + ".BackupSnapshotOvercloud") + + def get_parser(self, prog_name): + parser = argparse.ArgumentParser( + description=self.get_description(), + prog=prog_name, + add_help=False + ) + + parser.add_argument( + '--inventory', + default=INVENTORY, + help=_("Tripleo inventory file generated with " + "tripleo-ansible-inventory command. " + "Defaults to: " + INVENTORY) + ) + + parser.add_argument( + '--extra-vars', + default=None, + action='store', + help=_("Set additional variables as Dict or as " + "an absolute path of a JSON or YAML file type. " + "i.e. --extra-vars '{\"key\": \"val\", " + " \"key2\": \"val2\"}' " + "i.e. --extra-vars /path/to/my_vars.yaml " + "i.e. --extra-vars /path/to/my_vars.json. " + "For more information about the variables that " + "can be passed, visit: https://opendev.org/openstack/" + "tripleo-ansible/src/branch/master/tripleo_ansible/" + "roles/backup_and_restore/defaults/main.yml.") + ) + + return parser + + def _parse_extra_vars(self, raw_extra_vars): + + if raw_extra_vars is None: + extra_vars = {} + elif os.path.exists(raw_extra_vars): + with open(raw_extra_vars, 'r') as fp: + extra_vars = yaml.safe_load(fp.read()) + else: + try: + extra_vars = yaml.safe_load(raw_extra_vars) + except yaml.YAMLError as exc: + raise RuntimeError( + _('--extra-vars is not an existing file and cannot be ' + 'parsed as YAML / JSON: %s') % exc) + + return extra_vars + + def _run_snapshot_overcloud(self, parsed_args): + """Snapshot defined overcloud nodes.""" + + extra_vars = self._parse_extra_vars(parsed_args.extra_vars) + + if not (os.path.isfile(parsed_args.inventory) and + os.access(parsed_args.inventory, os.R_OK)): + raise RuntimeError( + _('The inventory file {} does not exist or is not ' + 'readable'.format(parsed_args.inventory))) + + self.log.debug(_('Starting Overcloud Snapshot')) + self._run_ansible_playbook( + playbook='cli-overcloud-snapshot.yaml', + inventory=parsed_args.inventory, + tags=None, + skip_tags=None, + extra_vars=extra_vars + ) + + def _run_ansible_playbook(self, + playbook, + inventory, + tags, + skip_tags, + extra_vars): + """Run ansible playbook""" + + with utils.TempDirs() as tmp: + utils.run_ansible_playbook( + playbook=playbook, + inventory=inventory, + workdir=tmp, + playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, + tags=tags, + skip_tags=skip_tags, + verbosity=utils.playbook_verbosity(self=self), + extra_vars=extra_vars + ) + + def take_action(self, parsed_args): + self._run_snapshot_overcloud(parsed_args)