summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
authorSean McGinnis <sean.mcginnis@gmail.com>2018-10-23 10:36:34 -0500
committerSean McGinnis <sean.mcginnis@gmail.com>2018-11-10 02:11:04 -0600
commitb90c780d2b99a91dd479bcc5f20caddcfb652f76 (patch)
tree0c172c7eb98ee2a38ae5d9bb763186603d30d481 /openstackclient
parent097b45686e74d7f3ef086a1684e745cf83e73767 (diff)
downloadpython-openstackclient-b90c780d2b99a91dd479bcc5f20caddcfb652f76.tar.gz
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 <sean.mcginnis@gmail.com>
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/tests/unit/volume/v2/fakes.py29
-rw-r--r--openstackclient/tests/unit/volume/v2/test_backup_record.py114
-rw-r--r--openstackclient/volume/v2/backup_record.py82
3 files changed, 225 insertions, 0 deletions
diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py
index 59b08d0b..c245cbf6 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 00000000..0e24174c
--- /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 00000000..f4918032
--- /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="<backup>",
+ 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="<backup_service>",
+ help=_("Backup service containing the backup.")
+ )
+ parser.add_argument(
+ "backup_metadata",
+ metavar="<backup_metadata>",
+ 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)))