From 96afb8b1b7b59a8a53b6614457fbbf36fc9882dc Mon Sep 17 00:00:00 2001 From: Amey Bhide Date: Mon, 1 Jun 2015 23:40:48 -0700 Subject: [PATCH] Add support for volume v2 commands Adds the following commands: openstack volume create openstack volume set openstack volume unset Implements: blueprint volume-v2 Change-Id: Icb7404815763aa88550112fb42f5200ce05c2486 --- openstackclient/tests/volume/v2/fakes.py | 17 +- .../tests/volume/v2/test_volume.py | 469 +++++++++++++++++- openstackclient/volume/v2/volume.py | 235 +++++++++ setup.cfg | 3 + 4 files changed, 722 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py index c896ed6dd1..a95bc94b14 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -17,6 +17,7 @@ import mock from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.image.v2 import fakes as image_fakes from openstackclient.tests import utils volume_id = "ce26708d-a7f8-4b4b-9861-4a80256615a6" @@ -26,8 +27,11 @@ volume_status = "available" volume_size = 20 volume_type = "fake_lvmdriver-1" volume_metadata = { - "foo": "bar" + 'Alpha': 'a', + 'Beta': 'b', + 'Gamma': 'g', } +volume_metadata_str = "Alpha='a', Beta='b', Gamma='g'" volume_snapshot_id = 1 volume_availability_zone = "nova" volume_attachments = ["fake_attachments"] @@ -169,6 +173,13 @@ QOS_WITH_ASSOCIATIONS = { 'associations': [qos_association] } +image_id = 'im1' +image_name = 'graven' +IMAGE = { + 'id': image_id, + 'name': image_name +} + class FakeVolumeClient(object): def __init__(self, **kwargs): @@ -200,3 +211,7 @@ class TestVolume(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) + self.app.client_manager.image = image_fakes.FakeImagev2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) diff --git a/openstackclient/tests/volume/v2/test_volume.py b/openstackclient/tests/volume/v2/test_volume.py index 9e991b7278..4fffefa4d7 100644 --- a/openstackclient/tests/volume/v2/test_volume.py +++ b/openstackclient/tests/volume/v2/test_volume.py @@ -15,18 +15,485 @@ import copy from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume class TestVolume(volume_fakes.TestVolume): - def setUp(self): super(TestVolume, self).setUp() self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() + self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.reset_mock() + + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + self.images_mock = self.app.client_manager.image.images + self.images_mock.reset_mock() + + +class TestVolumeCreate(TestVolume): + def setUp(self): + super(TestVolumeCreate, self).setUp() + + self.volumes_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True, + ) + + # Get the command object to test + self.cmd = volume.CreateVolume(self.app, None) + + def test_volume_create_min_options(self): + arglist = [ + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_options(self): + arglist = [ + '--size', str(volume_fakes.volume_size), + '--description', volume_fakes.volume_description, + '--type', volume_fakes.volume_type, + '--availability-zone', volume_fakes.volume_availability_zone, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('description', volume_fakes.volume_description), + ('type', volume_fakes.volume_type), + ('availability_zone', volume_fakes.volume_availability_zone), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=volume_fakes.volume_description, + volume_type=volume_fakes.volume_type, + user_id=None, + project_id=None, + availability_zone=volume_fakes.volume_availability_zone, + metadata=None, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_user_project_id(self): + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + # Return a user + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--size', str(volume_fakes.volume_size), + '--project', identity_fakes.project_id, + '--user', identity_fakes.user_id, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('project', identity_fakes.project_id), + ('user', identity_fakes.user_id), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=identity_fakes.user_id, + project_id=identity_fakes.project_id, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_user_project_name(self): + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + # Return a user + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--size', str(volume_fakes.volume_size), + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_name, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('project', identity_fakes.project_name), + ('user', identity_fakes.user_name), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=identity_fakes.user_id, + project_id=identity_fakes.project_id, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_properties(self): + arglist = [ + '--property', 'Alpha=a', + '--property', 'Beta=b', + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('property', {'Alpha': 'a', 'Beta': 'b'}), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata={'Alpha': 'a', 'Beta': 'b'}, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_image_id(self): + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.IMAGE), + loaded=True, + ) + + arglist = [ + '--image', volume_fakes.image_id, + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('image', volume_fakes.image_id), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=volume_fakes.image_id, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_image_name(self): + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.IMAGE), + loaded=True, + ) + + arglist = [ + '--image', volume_fakes.image_name, + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('image', volume_fakes.image_name), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=volume_fakes.image_id, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + class TestVolumeShow(TestVolume): def setUp(self): diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index e50a6f0c9d..d4536f5149 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -20,9 +20,139 @@ from cliff import command from cliff import show import six +from openstackclient.common import parseractions from openstackclient.common import utils +class CreateVolume(show.ShowOne): + """Create new volume""" + + log = logging.getLogger(__name__ + ".CreateVolume") + + def get_parser(self, prog_name): + parser = super(CreateVolume, self).get_parser(prog_name) + parser.add_argument( + "name", + metavar="", + help="New volume name" + ) + parser.add_argument( + "--size", + metavar="", + type=int, + required=True, + help="New volume size in GB" + ) + parser.add_argument( + "--snapshot", + metavar="", + help="Use as source of new volume (name or ID)" + ) + parser.add_argument( + "--description", + metavar="", + help="New volume description" + ) + parser.add_argument( + "--type", + metavar="", + help="Use as the new volume type", + ) + parser.add_argument( + '--user', + metavar='', + help='Specify an alternate user (name or ID)', + ) + parser.add_argument( + '--project', + metavar='', + help='Specify an alternate project (name or ID)', + ) + parser.add_argument( + "--availability-zone", + metavar="", + help="Create new volume in " + ) + parser.add_argument( + "--image", + metavar="", + help="Use as source of new volume (name or ID)" + ) + parser.add_argument( + "--source", + metavar="", + help="Volume to clone (name or ID)" + ) + parser.add_argument( + "--property", + metavar="", + action=parseractions.KeyValueAction, + help="Set a property to this volume " + "(repeat option to set multiple properties)" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action: (%s)", parsed_args) + + identity_client = self.app.client_manager.identity + volume_client = self.app.client_manager.volume + image_client = self.app.client_manager.image + + source_volume = None + if parsed_args.source: + source_volume = utils.find_resource( + volume_client.volumes, + parsed_args.source).id + + image = None + if parsed_args.image: + image = utils.find_resource( + image_client.images, + parsed_args.image).id + + snapshot = None + if parsed_args.snapshot: + snapshot = utils.find_resource( + volume_client.snapshots, + parsed_args.snapshot).id + + project = None + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project).id + + user = None + if parsed_args.user: + user = utils.find_resource( + identity_client.users, + parsed_args.user).id + + volume = volume_client.volumes.create( + size=parsed_args.size, + snapshot_id=snapshot, + name=parsed_args.name, + description=parsed_args.description, + volume_type=parsed_args.type, + user_id=user, + project_id=project, + availability_zone=parsed_args.availability_zone, + metadata=parsed_args.property, + imageRef=image, + source_volid=source_volume + ) + # Remove key links from being displayed + volume._info.update( + { + 'properties': utils.format_dict(volume._info.pop('metadata')), + 'type': volume._info.pop('volume_type') + } + ) + volume._info.pop("links", None) + return zip(*sorted(six.iteritems(volume._info))) + + class DeleteVolume(command.Command): """Delete volume(s)""" @@ -59,6 +189,77 @@ class DeleteVolume(command.Command): return +class SetVolume(show.ShowOne): + """Set volume properties""" + + log = logging.getLogger(__name__ + '.SetVolume') + + def get_parser(self, prog_name): + parser = super(SetVolume, self).get_parser(prog_name) + parser.add_argument( + 'volume', + metavar='', + help='Volume to change (name or ID)', + ) + parser.add_argument( + '--name', + metavar='', + help='New volume name', + ) + parser.add_argument( + '--description', + metavar='', + help='New volume description', + ) + parser.add_argument( + '--size', + metavar='', + type=int, + help='Extend volume size in GB', + ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Property to add or modify for this volume ' + '(repeat option to set multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + + if parsed_args.size: + if volume.status != 'available': + self.app.log.error("Volume is in %s state, it must be " + "available before size can be extended" % + volume.status) + return + if parsed_args.size <= volume.size: + self.app.log.error("New size must be greater than %s GB" % + volume.size) + return + volume_client.volumes.extend(volume.id, parsed_args.size) + + if parsed_args.property: + volume_client.volumes.set_metadata(volume.id, parsed_args.property) + + kwargs = {} + if parsed_args.name: + kwargs['display_name'] = parsed_args.name + if parsed_args.description: + kwargs['display_description'] = parsed_args.description + if kwargs: + volume_client.volumes.update(volume.id, **kwargs) + + if not kwargs and not parsed_args.property and not parsed_args.size: + self.app.log.error("No changes requested\n") + + return + + class ShowVolume(show.ShowOne): """Display volume details""" @@ -81,3 +282,37 @@ class ShowVolume(show.ShowOne): # Remove key links from being displayed volume._info.pop("links", None) return zip(*sorted(six.iteritems(volume._info))) + + +class UnsetVolume(command.Command): + """Unset volume properties""" + + log = logging.getLogger(__name__ + '.UnsetVolume') + + def get_parser(self, prog_name): + parser = super(UnsetVolume, self).get_parser(prog_name) + parser.add_argument( + 'volume', + metavar='', + help='Volume to modify (name or ID)', + ) + parser.add_argument( + '--property', + metavar='', + required=True, + action='append', + default=[], + help='Property to remove from volume ' + '(repeat option to remove multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + volume = utils.find_resource( + volume_client.volumes, parsed_args.volume) + + volume_client.volumes.delete_metadata( + volume.id, parsed_args.property) + return diff --git a/setup.cfg b/setup.cfg index 706ea98033..6f7cad61f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -393,8 +393,11 @@ openstack.volume.v2 = snapshot_show = openstackclient.volume.v2.snapshot:ShowSnapshot snapshot_unset = openstackclient.volume.v2.snapshot:UnsetSnapshot + volume_create = openstackclient.volume.v2.volume:CreateVolume volume_delete = openstackclient.volume.v2.volume:DeleteVolume + volume_set = openstackclient.volume.v2.volume:SetVolume volume_show = openstackclient.volume.v2.volume:ShowVolume + volume_unset = openstackclient.volume.v2.volume:UnsetVolume volume_type_create = openstackclient.volume.v2.volume_type:CreateVolumeType volume_type_delete = openstackclient.volume.v2.volume_type:DeleteVolumeType