From 2fce8634119af42cef580f52b6b11e9c13326532 Mon Sep 17 00:00:00 2001 From: Amey Bhide Date: Wed, 3 Jun 2015 15:05:18 -0700 Subject: [PATCH] Add support for volume backup v2 command openstack backup create openstack backup list openstack backup restore Implements: blueprint volume-v2 Change-Id: I77965730065dd44f256c46bcc43c1e6a03b63145 --- openstackclient/tests/volume/v2/fakes.py | 7 +- .../tests/volume/v2/test_backup.py | 147 ++++++++++++++++++ openstackclient/volume/v2/backup.py | 132 ++++++++++++++++ setup.cfg | 3 + 4 files changed, 288 insertions(+), 1 deletion(-) diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py index 3eade3910f..d6c07b9f88 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -93,6 +93,7 @@ backup_description = "fake description" backup_object_count = None backup_container = None backup_size = 10 +backup_status = "error" BACKUP = { "id": backup_id, @@ -101,7 +102,9 @@ BACKUP = { "description": backup_description, "object_count": backup_object_count, "container": backup_container, - "size": backup_size + "size": backup_size, + "status": backup_status, + "availability_zone": volume_availability_zone, } BACKUP_columns = tuple(sorted(BACKUP)) @@ -118,6 +121,8 @@ class FakeVolumeClient(object): self.backups.resource_class = fakes.FakeResource(None, {}) self.volume_types = mock.Mock() self.volume_types.resource_class = fakes.FakeResource(None, {}) + self.restores = mock.Mock() + self.restores.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/volume/v2/test_backup.py b/openstackclient/tests/volume/v2/test_backup.py index e24cac3cad..7af22e8a45 100644 --- a/openstackclient/tests/volume/v2/test_backup.py +++ b/openstackclient/tests/volume/v2/test_backup.py @@ -26,6 +26,55 @@ class TestBackup(volume_fakes.TestVolume): self.backups_mock = self.app.client_manager.volume.backups self.backups_mock.reset_mock() + self.volumes_mock = self.app.client_manager.volume.volumes + self.volumes_mock.reset_mock() + self.restores_mock = self.app.client_manager.volume.restores + self.restores_mock.reset_mock() + + +class TestBackupCreate(TestBackup): + def setUp(self): + super(TestBackupCreate, self).setUp() + + self.volumes_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True + ) + + self.backups_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.BACKUP), + loaded=True + ) + # Get the command object to test + self.cmd = backup.CreateBackup(self.app, None) + + def test_backup_create(self): + arglist = [ + volume_fakes.volume_id, + "--name", volume_fakes.backup_name, + "--description", volume_fakes.backup_description, + "--container", volume_fakes.backup_name + ] + verifylist = [ + ("volume", volume_fakes.volume_id), + ("name", volume_fakes.backup_name), + ("description", volume_fakes.backup_description), + ("container", volume_fakes.backup_name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.backups_mock.create.assert_called_with( + volume_fakes.volume_id, + container=volume_fakes.backup_name, + name=volume_fakes.backup_name, + description=volume_fakes.backup_description + ) + self.assertEqual(columns, volume_fakes.BACKUP_columns) + self.assertEqual(data, volume_fakes.BACKUP_data) class TestBackupShow(TestBackup): @@ -80,3 +129,101 @@ class TestBackupDelete(TestBackup): self.cmd.take_action(parsed_args) self.backups_mock.delete.assert_called_with(volume_fakes.backup_id) + + +class TestBackupRestore(TestBackup): + def setUp(self): + super(TestBackupRestore, self).setUp() + + self.backups_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.BACKUP), + loaded=True + ) + self.volumes_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True + ) + self.restores_mock.restore.return_value = None + # Get the command object to mock + self.cmd = backup.RestoreBackup(self.app, None) + + def test_backup_restore(self): + arglist = [ + volume_fakes.backup_id, + volume_fakes.volume_id + ] + verifylist = [ + ("backup", volume_fakes.backup_id), + ("volume", volume_fakes.volume_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.restores_mock.restore.assert_called_with(volume_fakes.backup_id, + volume_fakes.volume_id) + + +class TestBackupList(TestBackup): + def setUp(self): + super(TestBackupList, self).setUp() + + self.volumes_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True + ) + ] + self.backups_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.BACKUP), + loaded=True + ) + ] + # Get the command to test + self.cmd = backup.ListBackup(self.app, None) + + def test_backup_list_without_options(self): + arglist = [] + verifylist = [("long", False)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + collist = ['ID', 'Name', 'Description', 'Status', 'Size'] + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.backup_id, + volume_fakes.backup_name, + volume_fakes.backup_description, + volume_fakes.backup_status, + volume_fakes.backup_size + ),) + self.assertEqual(datalist, tuple(data)) + + def test_backup_list_with_options(self): + arglist = ["--long"] + verifylist = [("long", True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + collist = ['ID', 'Name', 'Description', 'Status', 'Size', + 'Availability Zone', 'Volume', 'Container'] + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.backup_id, + volume_fakes.backup_name, + volume_fakes.backup_description, + volume_fakes.backup_status, + volume_fakes.backup_size, + volume_fakes.volume_availability_zone, + volume_fakes.backup_volume_id, + volume_fakes.backup_container + ),) + self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/volume/v2/backup.py b/openstackclient/volume/v2/backup.py index bf2ea3a620..3525e701fe 100644 --- a/openstackclient/volume/v2/backup.py +++ b/openstackclient/volume/v2/backup.py @@ -14,15 +14,62 @@ """Volume v2 Backup action implementations""" +import copy import logging from cliff import command +from cliff import lister from cliff import show import six from openstackclient.common import utils +class CreateBackup(show.ShowOne): + """Create new backup""" + + log = logging.getLogger(__name__ + ".CreateBackup") + + def get_parser(self, prog_name): + parser = super(CreateBackup, self).get_parser(prog_name) + parser.add_argument( + "volume", + metavar="", + help="Volume to backup (name or ID)" + ) + parser.add_argument( + "--name", + metavar="", + required=True, + help="Name of the backup" + ) + parser.add_argument( + "--description", + metavar="", + help="Description of the backup" + ) + parser.add_argument( + "--container", + metavar="", + help="Optional backup container name" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action: (%s)", parsed_args) + volume_client = self.app.client_manager.volume + volume_id = utils.find_resource( + volume_client.volumes, parsed_args.volume).id + backup = volume_client.backups.create( + volume_id, + container=parsed_args.container, + name=parsed_args.name, + description=parsed_args.description + ) + backup._info.pop("links", None) + return zip(*sorted(six.iteritems(backup._info))) + + class DeleteBackup(command.Command): """Delete backup(s)""" @@ -48,6 +95,91 @@ class DeleteBackup(command.Command): return +class ListBackup(lister.Lister): + """List backups""" + + log = logging.getLogger(__name__ + ".ListBackup") + + def get_parser(self, prog_name): + parser = super(ListBackup, self).get_parser(prog_name) + parser.add_argument( + "--long", + action="store_true", + default=False, + help="List additional fields in output" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action: (%s)", parsed_args) + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].name + return volume + + if parsed_args.long: + columns = ['ID', 'Name', 'Description', 'Status', 'Size', + 'Availability Zone', 'Volume ID', 'Container'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + else: + columns = ['ID', 'Name', 'Description', 'Status', 'Size'] + column_headers = columns + + # Cache the volume list + volume_cache = {} + try: + for s in self.app.client_manager.volume.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + + data = self.app.client_manager.volume.backups.list() + + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters={'Volume ID': _format_volume_id}, + ) for s in data)) + + +class RestoreBackup(show.ShowOne): + """Restore backup""" + + log = logging.getLogger(__name__ + ".RestoreBackup") + + def get_parser(self, prog_name): + parser = super(RestoreBackup, self).get_parser(prog_name) + parser.add_argument( + "backup", + metavar="", + help="Backup to restore (ID only)" + ) + parser.add_argument( + "volume", + metavar="", + help="Volume to restore to (name or ID)" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action: (%s)", parsed_args) + volume_client = self.app.client_manager.volume + backup = utils.find_resource(volume_client.backups, parsed_args.backup) + destination_volume = utils.find_resource(volume_client.volumes, + parsed_args.volume) + return volume_client.restores.restore(backup.id, destination_volume.id) + + class ShowBackup(show.ShowOne): """Display backup details""" diff --git a/setup.cfg b/setup.cfg index ce6a0b9b00..838786ad18 100644 --- a/setup.cfg +++ b/setup.cfg @@ -367,7 +367,10 @@ openstack.volume.v1 = volume_type_unset = openstackclient.volume.v1.type:UnsetVolumeType openstack.volume.v2 = + backup_create = openstackclient.volume.v2.backup:CreateBackup backup_delete = openstackclient.volume.v2.backup:DeleteBackup + backup_list = openstackclient.volume.v2.backup:ListBackup + backup_restore = openstackclient.volume.v2.backup:RestoreBackup backup_show = openstackclient.volume.v2.backup:ShowBackup snapshot_delete = openstackclient.volume.v2.snapshot:DeleteSnapshot