From 8cda430e8b81ef991f22b84c49e24eda0bc2f4bd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 11 Sep 2017 08:26:34 -0600 Subject: [PATCH] Add method to set bootable flag on volumes If a person wants to create a bootable volume not from an image, they need to set a flag, which is done with this action call. Change-Id: I765eb97501a5ba9e54325c8c56573bb7311deb72 --- .../set-bootable-volume-454a7a41e7e77d08.yaml | 4 + shade/openstackcloud.py | 36 +++++++- shade/tests/unit/test_volume.py | 89 ++++++++++++++++++- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/set-bootable-volume-454a7a41e7e77d08.yaml diff --git a/releasenotes/notes/set-bootable-volume-454a7a41e7e77d08.yaml b/releasenotes/notes/set-bootable-volume-454a7a41e7e77d08.yaml new file mode 100644 index 000000000..c7d84fe03 --- /dev/null +++ b/releasenotes/notes/set-bootable-volume-454a7a41e7e77d08.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added a ``set_volume_bootable`` call to allow toggling the bootable state + of a volume. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ca167dd41..39012bc72 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4806,7 +4806,7 @@ class OpenStackCloud( def create_volume( self, size, - wait=True, timeout=None, image=None, **kwargs): + wait=True, timeout=None, image=None, bootable=None, **kwargs): """Create a volume. :param size: Size, in GB of the volume to create. @@ -4816,6 +4816,8 @@ class OpenStackCloud( :param timeout: Seconds to wait for volume creation. None is forever. :param image: (optional) Image name, ID or object from which to create the volume + :param bootable: (optional) Make this volume bootable. If set, wait + will also be set to true. :param kwargs: Keyword arguments as expected for cinder client. :returns: The created volume object. @@ -4823,6 +4825,9 @@ class OpenStackCloud( :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ + if bootable is not None: + wait = True + if image: image_obj = self.get_image(image) if not image_obj: @@ -4858,6 +4863,10 @@ class OpenStackCloud( continue if volume['status'] == 'available': + if bootable is not None: + self.set_volume_bootable(volume, bootable=bootable) + # no need to re-fetch to update the flag, just set it. + volume['bootable'] = bootable return volume if volume['status'] == 'error': @@ -4865,6 +4874,31 @@ class OpenStackCloud( return self._normalize_volume(volume) + def set_volume_bootable(self, name_or_id, bootable=True): + """Set a volume's bootable flag. + + :param name_or_id: Name, unique ID of the volume or a volume dict. + :param bool bootable: Whether the volume should be bootable. + (Defaults to True) + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + volume = self.get_volume(name_or_id) + + if not volume: + raise OpenStackCloudException( + "Volume {name_or_id} does not exist".format( + name_or_id=name_or_id)) + + self._volume_client.post( + 'volumes/{id}/action'.format(id=volume['id']), + json={'os-set_bootable': {'bootable': bootable}}, + error_message="Error setting bootable on volume {volume}".format( + volume=volume['id']) + ) + def delete_volume(self, name_or_id=None, wait=True, timeout=None, force=False): """Delete a volume. diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 5983c6af8..40d838881 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -292,7 +292,8 @@ class TestVolume(base.RequestsMockTestCase): uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', volume.id, 'action']), - json={'os-force_delete': None}), + validate=dict( + json={'os-force_delete': None})), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -300,6 +301,42 @@ class TestVolume(base.RequestsMockTestCase): self.assertTrue(self.cloud.delete_volume(volume['id'], force=True)) self.assert_calls() + def test_set_volume_bootable(self): + vol = {'id': 'volume001', 'status': 'attached', + 'name': '', 'attachments': []} + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [volume]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', volume.id, 'action']), + json={'os-set_bootable': {'bootable': True}}), + ]) + self.cloud.set_volume_bootable(volume['id']) + self.assert_calls() + + def test_set_volume_bootable_false(self): + vol = {'id': 'volume001', 'status': 'attached', + 'name': '', 'attachments': []} + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [volume]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', volume.id, 'action']), + json={'os-set_bootable': {'bootable': False}}), + ]) + self.cloud.set_volume_bootable(volume['id']) + self.assert_calls() + def test_list_volumes_with_pagination(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) @@ -448,3 +485,53 @@ class TestVolume(base.RequestsMockTestCase): self.cloud._normalize_volume(vol1), self.cloud.get_volume_by_id('01')) self.assert_calls() + + def test_create_volume(self): + vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes']), + json={'volume': vol1}, + validate=dict(json={ + 'volume': { + 'size': 50, + 'name': 'vol1', + }})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail']), + json={'volumes': [vol1]}), + ]) + + self.cloud.create_volume(50, name='vol1') + self.assert_calls() + + def test_create_bootable_volume(self): + vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes']), + json={'volume': vol1}, + validate=dict(json={ + 'volume': { + 'size': 50, + 'name': 'vol1', + }})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail']), + json={'volumes': [vol1]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', '01', 'action']), + validate=dict( + json={'os-set_bootable': {'bootable': True}})), + ]) + + self.cloud.create_volume(50, name='vol1', bootable=True) + self.assert_calls()