From b90c780d2b99a91dd479bcc5f20caddcfb652f76 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 23 Oct 2018 10:36:34 -0500 Subject: [PATCH] Add volume backup import/export commands This adds commands to import and export volume backup records so they can be imported and restored on other Cinder instances or to the original instance if the service or database has been lost and had to be rebuilt. I know this is a commonly used process by some users, so it would be good to have this functionality in osc so they do not have to switch clients. More details about the export and import process can be found here: https://docs.openstack.org/cinder/latest/admin/blockstorage-volume-backups-export-import.html Change-Id: Ic95f87b36a416a2b50cb2193fd5759ab59336975 Signed-off-by: Sean McGinnis --- .../cli/command-objects/volume-backup.rst | 198 +----------------- doc/source/cli/commands.rst | 2 + doc/source/cli/data/cinder.csv | 4 +- openstackclient/tests/unit/volume/v2/fakes.py | 29 +++ .../unit/volume/v2/test_backup_record.py | 114 ++++++++++ openstackclient/volume/v2/backup_record.py | 82 ++++++++ ...volume-backup-record-9f5987c45e294dc6.yaml | 15 ++ setup.cfg | 3 + 8 files changed, 250 insertions(+), 197 deletions(-) create mode 100644 openstackclient/tests/unit/volume/v2/test_backup_record.py create mode 100644 openstackclient/volume/v2/backup_record.py create mode 100644 releasenotes/notes/volume-backup-record-9f5987c45e294dc6.yaml diff --git a/doc/source/cli/command-objects/volume-backup.rst b/doc/source/cli/command-objects/volume-backup.rst index 585f47d46..632e215e2 100644 --- a/doc/source/cli/command-objects/volume-backup.rst +++ b/doc/source/cli/command-objects/volume-backup.rst @@ -2,200 +2,8 @@ volume backup ============= -Block Storage v1, v2 +Volume v1, v2 -volume backup create --------------------- +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup * -Create new volume backup - -.. program:: volume backup create -.. code:: bash - - openstack volume backup create - [--container ] - [--name ] - [--description ] - [--snapshot ] - [--force] - [--incremental] - - -.. option:: --container - - Optional backup container name - -.. option:: --name - - Name of the backup - -.. option:: --description - - Description of the backup - -.. option:: --snapshot - - Snapshot to backup (name or ID) - - *Volume version 2 only* - -.. option:: --force - - Allow to back up an in-use volume - - *Volume version 2 only* - -.. option:: --incremental - - Perform an incremental backup - - *Volume version 2 only* - -.. _volume_backup_create-backup: -.. describe:: - - Volume to backup (name or ID) - -volume backup delete --------------------- - -Delete volume backup(s) - -.. program:: volume backup delete -.. code:: bash - - openstack volume backup delete - [--force] - [ ...] - -.. option:: --force - - Allow delete in state other than error or available - - *Volume version 2 only* - -.. _volume_backup_delete-backup: -.. describe:: - - Backup(s) to delete (name or ID) - -volume backup list ------------------- - -List volume backups - -.. program:: volume backup list -.. code:: bash - - openstack volume backup list - [--long] - [--name ] - [--status ] - [--volume ] - [--marker ] - [--limit ] - [--all-projects] - -.. _volume_backup_list-backup: -.. option:: --long - - List additional fields in output - -.. option:: --name - - Filters results by the backup name - -.. option:: --status - - Filters results by the backup status - ('creating', 'available', 'deleting', 'error', 'restoring' or 'error_restoring') - -.. option:: --volume - - Filters results by the volume which they backup (name or ID)" - -.. option:: --marker - - The last backup of the previous page (name or ID) - - *Volume version 2 only* - -.. option:: --limit - - Maximum number of backups to display - - *Volume version 2 only* - -.. option:: --all-projects - - Include all projects (admin only) - -volume backup restore ---------------------- - -Restore volume backup - -.. program:: volume backup restore -.. code:: bash - - openstack volume backup restore - - - -.. _volume_backup_restore-backup: -.. describe:: - - Backup to restore (name or ID) - -.. describe:: - - Volume to restore to (name or ID) - -volume backup set ------------------ - -Set volume backup properties - -.. program:: volume backup set -.. code:: bash - - openstack volume backup set - [--name ] - [--description ] - [--state ] - - -.. option:: --name - - New backup name - -.. option:: --description - - New backup description - -.. option:: --state - - New backup state ("available" or "error") (admin only) - (This option simply changes the state of the backup in the database with - no regard to actual status, exercise caution when using) - -.. _backup_set-volume-backup: -.. describe:: - - Backup to modify (name or ID) - -volume backup show ------------------- - -Display volume backup details - -.. program:: volume backup show -.. code:: bash - - openstack volume backup show - - -.. _volume_backup_show-backup: -.. describe:: - - Backup to display (name or ID) diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index d7c912404..cdd5e63c9 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -158,6 +158,8 @@ referring to both Compute and Volume quotas. * ``volume backup``: (**Volume**) backup for volumes * ``volume backend capability``: (**volume**) volume backend storage capabilities * ``volume backend pool``: (**volume**) volume backend storage pools +* ``volume backup record``: (**Volume**) volume record that can be imported or exported +* ``volume backend``: (**volume**) volume backend storage * ``volume host``: (**Volume**) the physical computer for volumes * ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes * ``volume snapshot``: (**Volume**) a point-in-time copy of a volume diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index cc8ef2d91..22fb84cff 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -2,8 +2,8 @@ absolute-limits,limits show --absolute,Lists absolute limits for a user. availability-zone-list,availability zone list --volume,Lists all availability zones. backup-create,volume backup create,Creates a volume backup. backup-delete,volume backup delete,Removes a backup. -backup-export,volume backup export,Export backup metadata record. -backup-import,volume backup import,Import backup metadata record. +backup-export,volume backup record export,Export backup metadata record. +backup-import,volume backup record import,Import backup metadata record. backup-list,volume backup list,Lists all backups. backup-reset-state,volume backup set --state,Explicitly updates the backup state. backup-restore,volume backup restore,Restores a backup. diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 59b08d0b9..c245cbf66 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -596,6 +596,35 @@ class FakeBackup(object): return mock.Mock(side_effect=backups) + @staticmethod + def create_backup_record(): + """Gets a fake backup record for a given backup. + + :return: An "exported" backup record. + """ + + return { + 'backup_service': 'cinder.backup.drivers.swift.SwiftBackupDriver', + 'backup_url': 'eyJzdGF0dXMiOiAiYXZh', + } + + @staticmethod + def import_backup_record(): + """Creates a fake backup record import response from a backup. + + :return: The fake backup object that was encoded. + """ + return { + 'backup': { + 'id': 'backup.id', + 'name': 'backup.name', + 'links': [ + {'href': 'link1', 'rel': 'self'}, + {'href': 'link2', 'rel': 'bookmark'}, + ], + }, + } + class FakeConsistencyGroup(object): """Fake one or more consistency group.""" diff --git a/openstackclient/tests/unit/volume/v2/test_backup_record.py b/openstackclient/tests/unit/volume/v2/test_backup_record.py new file mode 100644 index 000000000..0e24174c5 --- /dev/null +++ b/openstackclient/tests/unit/volume/v2/test_backup_record.py @@ -0,0 +1,114 @@ +# +# 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 openstackclient.tests.unit.volume.v2 import fakes as volume_fakes +from openstackclient.volume.v2 import backup_record + + +class TestBackupRecord(volume_fakes.TestVolume): + + def setUp(self): + super(TestBackupRecord, self).setUp() + + self.backups_mock = self.app.client_manager.volume.backups + self.backups_mock.reset_mock() + + +class TestBackupRecordExport(TestBackupRecord): + + new_backup = volume_fakes.FakeBackup.create_one_backup( + attrs={'volume_id': 'a54708a2-0388-4476-a909-09579f885c25'}) + new_record = volume_fakes.FakeBackup.create_backup_record() + + def setUp(self): + super(TestBackupRecordExport, self).setUp() + + self.backups_mock.export_record.return_value = self.new_record + self.backups_mock.get.return_value = self.new_backup + + # Get the command object to mock + self.cmd = backup_record.ExportBackupRecord(self.app, None) + + def test_backup_export_table(self): + arglist = [ + self.new_backup.name, + ] + verifylist = [ + ("backup", self.new_backup.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + parsed_args.formatter = 'table' + columns, __ = self.cmd.take_action(parsed_args) + + self.backups_mock.export_record.assert_called_with( + self.new_backup.id, + ) + + expected_columns = ('Backup Service', 'Metadata') + self.assertEqual(columns, expected_columns) + + def test_backup_export_json(self): + arglist = [ + self.new_backup.name, + ] + verifylist = [ + ("backup", self.new_backup.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + parsed_args.formatter = 'json' + columns, __ = self.cmd.take_action(parsed_args) + + self.backups_mock.export_record.assert_called_with( + self.new_backup.id, + ) + + expected_columns = ('backup_service', 'backup_url') + self.assertEqual(columns, expected_columns) + + +class TestBackupRecordImport(TestBackupRecord): + + new_backup = volume_fakes.FakeBackup.create_one_backup( + attrs={'volume_id': 'a54708a2-0388-4476-a909-09579f885c25'}) + new_import = volume_fakes.FakeBackup.import_backup_record() + + def setUp(self): + super(TestBackupRecordImport, self).setUp() + + self.backups_mock.import_record.return_value = self.new_import + + # Get the command object to mock + self.cmd = backup_record.ImportBackupRecord(self.app, None) + + def test_backup_import(self): + arglist = [ + "cinder.backup.drivers.swift.SwiftBackupDriver", + "fake_backup_record_data", + ] + verifylist = [ + ("backup_service", + "cinder.backup.drivers.swift.SwiftBackupDriver"), + ("backup_metadata", "fake_backup_record_data"), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, __ = self.cmd.take_action(parsed_args) + + self.backups_mock.import_record.assert_called_with( + "cinder.backup.drivers.swift.SwiftBackupDriver", + "fake_backup_record_data", + ) + self.assertEqual(columns, ('backup',)) diff --git a/openstackclient/volume/v2/backup_record.py b/openstackclient/volume/v2/backup_record.py new file mode 100644 index 000000000..f49180327 --- /dev/null +++ b/openstackclient/volume/v2/backup_record.py @@ -0,0 +1,82 @@ +# +# 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. +# + +"""Volume v2 Backup action implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import utils +import six + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class ExportBackupRecord(command.ShowOne): + _description = _('Export volume backup details. Backup information can be ' + 'imported into a new service instance to be able to ' + 'restore.') + + def get_parser(self, prog_name): + parser = super(ExportBackupRecord, self).get_parser(prog_name) + parser.add_argument( + "backup", + metavar="", + help=_("Backup to export (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + backup = utils.find_resource(volume_client.backups, parsed_args.backup) + backup_data = volume_client.backups.export_record(backup.id) + + # We only want to show "friendly" display names, but also want to keep + # json structure compatibility with cinderclient + if parsed_args.formatter == 'table': + backup_data['Backup Service'] = backup_data.pop('backup_service') + backup_data['Metadata'] = backup_data.pop('backup_url') + + return zip(*sorted(six.iteritems(backup_data))) + + +class ImportBackupRecord(command.ShowOne): + _description = _('Import volume backup details. Exported backup details ' + 'contain the metadata necessary to restore to a new or ' + 'rebuilt service instance') + + def get_parser(self, prog_name): + parser = super(ImportBackupRecord, self).get_parser(prog_name) + parser.add_argument( + "backup_service", + metavar="", + help=_("Backup service containing the backup.") + ) + parser.add_argument( + "backup_metadata", + metavar="", + help=_("Encoded backup metadata from export.") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + backup_data = volume_client.backups.import_record( + parsed_args.backup_service, + parsed_args.backup_metadata) + backup_data.pop('links', None) + return zip(*sorted(six.iteritems(backup_data))) diff --git a/releasenotes/notes/volume-backup-record-9f5987c45e294dc6.yaml b/releasenotes/notes/volume-backup-record-9f5987c45e294dc6.yaml new file mode 100644 index 000000000..955e99714 --- /dev/null +++ b/releasenotes/notes/volume-backup-record-9f5987c45e294dc6.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Add ``openstack volume backup record export `` command that + provides encrypted backup record metadata that can then be used to import + and restore on a different or rebuilt block storage service. Information + about volume backup export and import can be found in the `Cinder block + storage service documentation `_. + - | + Add ``openstack volume backup record import + `` command that can be used to import and restore on a + different or rebuilt block storage service. Information about volume backup + export and import can be found in the `Cinder block storage service + documentation `_. + diff --git a/setup.cfg b/setup.cfg index 73c5fde93..818b6ef25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -629,6 +629,9 @@ openstack.volume.v2 = volume_backup_set = openstackclient.volume.v2.backup:SetVolumeBackup volume_backup_show = openstackclient.volume.v2.backup:ShowVolumeBackup + volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord + volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord + volume_backend_capability_show = openstackclient.volume.v2.volume_backend:ShowCapability volume_backend_pool_list = openstackclient.volume.v2.volume_backend:ListPool