From c7f4190af26ad106bea29f66770b9569ab3fc4e1 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Sat, 29 Sep 2018 08:07:18 -0400 Subject: [PATCH] Add microversion 2.67 to support volume_type Add a new microversion 2.67 to support specify ``volume_type`` when boot instances. Part of bp boot-instance-specific-storage-backend Change-Id: I13102243f7ce36a5d44c1790f3a633703373ebf7 --- api-ref/source/parameters.yaml | 17 ++++ api-ref/source/servers.inc | 1 + .../servers/v2.67/server-create-req.json | 19 ++++ .../servers/v2.67/server-create-resp.json | 22 +++++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- doc/source/user/block-device-mapping.rst | 8 ++ nova/api/openstack/api_version_request.py | 4 +- nova/api/openstack/compute/schemas/servers.py | 7 ++ nova/api/openstack/compute/servers.py | 5 +- nova/api/validation/parameter_types.py | 5 ++ nova/block_device.py | 15 +++- nova/tests/fixtures.py | 9 ++ .../servers/v2.67/server-create-req.json.tpl | 19 ++++ .../servers/v2.67/server-create-resp.json.tpl | 22 +++++ .../api_sample_tests/test_servers.py | 13 +++ .../api/openstack/compute/test_serversV21.py | 86 +++++++++++++++++++ nova/tests/unit/test_block_device.py | 40 +++++++++ ...ific-storage-backend-c34ee0a871efec3b.yaml | 11 +++ 19 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 doc/api_samples/servers/v2.67/server-create-req.json create mode 100644 doc/api_samples/servers/v2.67/server-create-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.67/server-create-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.67/server-create-resp.json.tpl create mode 100644 releasenotes/notes/boot-instance-specific-storage-backend-c34ee0a871efec3b.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 226cc742a71a..31d329c135b3 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -2269,6 +2269,23 @@ device_type: in: body required: false type: string +device_volume_type: + description: | + The device ``volume_type``. This can be used to specify the type of volume + which the compute service will create and attach to the server. + If not specified, the block storage service will provide a default volume + type. See the `block storage volume types API `_ + for more details. + There are some restrictions on ``volume_type``: + + - It can be a volume type ID or name. + - It is only supported with ``source_type`` of ``blank``, ``image`` or + ``snapshot``. + - It is only supported with ``destination_type`` of ``volume``. + in: body + required: false + type: string + min_version: 2.67 # Optional input parameter in the body for PUT /os-services/{service_id} added # in microversion 2.53. disabled_reason_2_53_in: diff --git a/api-ref/source/servers.inc b/api-ref/source/servers.inc index f848905ee9f2..372caa465480 100644 --- a/api-ref/source/servers.inc +++ b/api-ref/source/servers.inc @@ -363,6 +363,7 @@ Request - block_device_mapping_v2.uuid: block_device_uuid - block_device_mapping_v2.volume_size: volume_size - block_device_mapping_v2.tag: device_tag_bdm + - block_device_mapping_v2.volume_type: device_volume_type - config_drive: config_drive - imageRef: imageRef - key_name: key_name diff --git a/doc/api_samples/servers/v2.67/server-create-req.json b/doc/api_samples/servers/v2.67/server-create-req.json new file mode 100644 index 000000000000..3ca0579b9c46 --- /dev/null +++ b/doc/api_samples/servers/v2.67/server-create-req.json @@ -0,0 +1,19 @@ +{ + "server" : { + "name" : "bfv-server-with-volume-type", + "flavorRef" : "http://openstack.example.com/flavors/1", + "networks" : [{ + "uuid" : "ff608d40-75e9-48cb-b745-77bb55b5eaf2", + "tag": "nic1" + }], + "block_device_mapping_v2": [{ + "uuid": "70a599e0-31e7-49b7-b260-868f441e862b", + "source_type": "image", + "destination_type": "volume", + "boot_index": 0, + "volume_size": "1", + "tag": "disk1", + "volume_type": "lvm-1" + }] + } +} diff --git a/doc/api_samples/servers/v2.67/server-create-resp.json b/doc/api_samples/servers/v2.67/server-create-resp.json new file mode 100644 index 000000000000..dd0bb9f2284e --- /dev/null +++ b/doc/api_samples/servers/v2.67/server-create-resp.json @@ -0,0 +1,22 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "S5wqy9sPYUvU", + "id": "97108291-2fd7-4dc2-a909-eaae0306a6a9", + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/97108291-2fd7-4dc2-a909-eaae0306a6a9", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/97108291-2fd7-4dc2-a909-eaae0306a6a9", + "rel": "bookmark" + } + ], + "security_groups": [ + { + "name": "default" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 43e4ef47c9cc..a06ffd030dd7 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.66", + "version": "2.67", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index 65dfad432f77..b2036e7c4dc3 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.66", + "version": "2.67", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/source/user/block-device-mapping.rst b/doc/source/user/block-device-mapping.rst index 4517a5bf9a1e..21d0b029617c 100644 --- a/doc/source/user/block-device-mapping.rst +++ b/doc/source/user/block-device-mapping.rst @@ -161,6 +161,14 @@ fields (in addition to the ones that were already there): usage is to set it to 0 for the boot device and leave it as None for any other devices. +* volume_type - Added in microversion 2.67 to the servers create API to + support specifying volume type when booting instances. When we snapshot a + volume-backed server, the block_device_mapping_v2 image metadata will + include the volume_type from the BDM record so if the user then creates + another server from that snapshot, the volume that nova creates from that + snapshot will use the same volume_type. If a user wishes to change that + volume type in the image metadata, they can do so via the image API. + Valid source / dest combinations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 7598b156b628..da8dbd7044dd 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -162,6 +162,8 @@ REST_API_VERSION_HISTORY = """REST API Version History: ``updated_at`` time to filter nova resources, the resources include the servers API, os-instance-action API and os-migrations API. + * 2.67 - Adds the optional ``volume_type`` field to the + ``block_device_mapping_v2`` parameter when creating a server. """ # The minimum and maximum versions of the API supported @@ -170,7 +172,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.66" +_MAX_API_VERSION = "2.67" DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which are related to network, images and baremetal diff --git a/nova/api/openstack/compute/schemas/servers.py b/nova/api/openstack/compute/schemas/servers.py index 025f8de58b42..374f38c2c8e8 100644 --- a/nova/api/openstack/compute/schemas/servers.py +++ b/nova/api/openstack/compute/schemas/servers.py @@ -349,6 +349,13 @@ base_create_v263['properties']['server']['properties'][ 'trusted_image_certificates'] = parameter_types.trusted_certs +# Add volume type in block_device_mapping_v2. +base_create_v267 = copy.deepcopy(base_create_v263) +base_create_v267['properties']['server']['properties'][ + 'block_device_mapping_v2']['items'][ + 'properties']['volume_type'] = parameter_types.volume_type + + base_update = { 'type': 'object', 'properties': { diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index e46e9ed329d9..4dc0b2e546d8 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -417,7 +417,8 @@ class ServersController(wsgi.Controller): @validation.schema(schema_servers.base_create_v242, '2.42', '2.51') @validation.schema(schema_servers.base_create_v252, '2.52', '2.56') @validation.schema(schema_servers.base_create_v257, '2.57', '2.62') - @validation.schema(schema_servers.base_create_v263, '2.63') + @validation.schema(schema_servers.base_create_v263, '2.63', '2.66') + @validation.schema(schema_servers.base_create_v267, '2.67') def create(self, req, body): """Creates a new server for a given user.""" context = req.environ['nova.context'] @@ -646,6 +647,7 @@ class ServersController(wsgi.Controller): exception.InvalidBDMEphemeralSize, exception.InvalidBDMFormat, exception.InvalidBDMSwapSize, + exception.VolumeTypeNotFound, exception.AutoDiskConfigDisabledByImage, exception.ImageCPUPinningForbidden, exception.ImageCPUThreadPolicyForbidden, @@ -673,6 +675,7 @@ class ServersController(wsgi.Controller): exception.NetworkAmbiguous, exception.NoUniqueMatch, exception.MultiattachSupportNotYetAvailable, + exception.VolumeTypeSupportNotYetAvailable, exception.CertificateValidationNotYetAvailable) as error: raise exc.HTTPConflict(explanation=error.format_message()) diff --git a/nova/api/validation/parameter_types.py b/nova/api/validation/parameter_types.py index 57f8fb1e63f8..411c01188f01 100644 --- a/nova/api/validation/parameter_types.py +++ b/nova/api/validation/parameter_types.py @@ -374,6 +374,11 @@ volume_id = { } +volume_type = { + 'type': ['string', 'null'], 'minLength': 0, 'maxLength': 255 +} + + network_id = { 'type': 'string', 'format': 'uuid' } diff --git a/nova/block_device.py b/nova/block_device.py index 7ff3cde3b9ce..c0113a350ad1 100644 --- a/nova/block_device.py +++ b/nova/block_device.py @@ -173,6 +173,7 @@ class BlockDeviceDict(dict): source_type = api_dict.get('source_type') device_uuid = api_dict.get('uuid') destination_type = api_dict.get('destination_type') + volume_type = api_dict.get('volume_type') if source_type == 'blank' and device_uuid: raise exception.InvalidBDMFormat( @@ -191,12 +192,24 @@ class BlockDeviceDict(dict): boot_index = -1 boot_index = int(boot_index) - # if this bdm is generated from --image ,then + # if this bdm is generated from --image, then # source_type = image and destination_type = local is allowed if not (image_uuid_specified and boot_index == 0): raise exception.InvalidBDMFormat( details=_("Mapping image to local is not supported.")) + if destination_type == 'local' and volume_type: + raise exception.InvalidBDMFormat( + details=_("Specifying a volume_type with destination_type=" + "local is not supported.")) + + # Specifying a volume_type with a pre-existing source volume is + # not supported. + if source_type == 'volume' and volume_type: + raise exception.InvalidBDMFormat( + details=_("Specifying volume type to existing volume is " + "not supported.")) + api_dict.pop('uuid', None) return cls(api_dict) diff --git a/nova/tests/fixtures.py b/nova/tests/fixtures.py index ff88eb2c5b54..ca9510ed9495 100644 --- a/nova/tests/fixtures.py +++ b/nova/tests/fixtures.py @@ -1723,6 +1723,13 @@ class CinderFixtureNewAttachFlow(fixtures.Fixture): 'target_lun': '1'}}} return attachment_ref + def fake_get_all_volume_types(*args, **kwargs): + return [{ + # This is used in the 2.67 API sample test. + 'id': '5f9204ec-3e94-4f27-9beb-fe7bb73b6eb9', + 'name': 'lvm-1' + }] + self.test.stub_out('nova.volume.cinder.API.attachment_create', fake_attachment_create) self.test.stub_out('nova.volume.cinder.API.attachment_delete', @@ -1746,6 +1753,8 @@ class CinderFixtureNewAttachFlow(fixtures.Fixture): lambda *args, **kwargs: None) self.test.stub_out('nova.volume.cinder.API.check_attached', lambda *args, **kwargs: None) + self.test.stub_out('nova.volume.cinder.API.get_all_volume_types', + fake_get_all_volume_types) class PlacementApiClient(object): diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.67/server-create-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.67/server-create-req.json.tpl new file mode 100644 index 000000000000..9477b43b4869 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.67/server-create-req.json.tpl @@ -0,0 +1,19 @@ +{ + "server" : { + "name" : "bfv-server-with-volume-type", + "flavorRef" : "%(host)s/flavors/1", + "networks" : [{ + "uuid" : "ff608d40-75e9-48cb-b745-77bb55b5eaf2", + "tag": "nic1" + }], + "block_device_mapping_v2": [{ + "uuid": "%(image_id)s", + "source_type": "image", + "destination_type": "volume", + "boot_index": 0, + "volume_size": "1", + "tag": "disk1", + "volume_type": "lvm-1" + }] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.67/server-create-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.67/server-create-resp.json.tpl new file mode 100644 index 000000000000..4b30e0cfbdb8 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.67/server-create-resp.json.tpl @@ -0,0 +1,22 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "%(password)s", + "id": "%(id)s", + "links": [ + { + "href": "%(versioned_compute_endpoint)s/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(compute_endpoint)s/servers/%(uuid)s", + "rel": "bookmark" + } + ], + "security_groups": [ + { + "name": "default" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/test_servers.py b/nova/tests/functional/api_sample_tests/test_servers.py index f901b6929016..ca21ca2c9a1e 100644 --- a/nova/tests/functional/api_sample_tests/test_servers.py +++ b/nova/tests/functional/api_sample_tests/test_servers.py @@ -21,6 +21,7 @@ import six from nova.api.openstack import api_version_request as avr import nova.conf +from nova.tests import fixtures as nova_fixtures from nova.tests.functional.api_sample_tests import api_sample_base from nova.tests.unit.api.openstack import fakes from nova.tests.unit.image import fake @@ -353,6 +354,18 @@ class ServersSampleJson266Test(ServersSampleBase): 'servers-details-with-changes-before', subs, response, 200) +class ServersSampleJson267Test(ServersSampleBase): + microversion = '2.67' + scenarios = [('v2_67', {'api_major_version': 'v2.1'})] + + def setUp(self): + super(ServersSampleJson267Test, self).setUp() + self.useFixture(nova_fixtures.CinderFixtureNewAttachFlow(self)) + + def test_servers_post(self): + return self._post_server(use_common_server_api_samples=False) + + class ServersUpdateSampleJsonTest(ServersSampleBase): def test_update_server(self): diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index 6dd4035d0c64..f26c33373df4 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -6409,6 +6409,92 @@ class ServersControllerCreateTestV263(ServersControllerCreateTest): six.text_type(ex)) +class ServersControllerCreateTestV267(ServersControllerCreateTest): + def setUp(self): + super(ServersControllerCreateTestV267, self).setUp() + + self.block_device_mapping_v2 = [ + {'uuid': '70a599e0-31e7-49b7-b260-868f441e862b', + 'source_type': 'image', + 'destination_type': 'volume', + 'boot_index': 0, + 'volume_size': '1', + 'volume_type': 'fake-lvm-1' + }] + + def _test_create_extra(self, *args, **kwargs): + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.67') + return super(ServersControllerCreateTestV267, self)._test_create_extra( + *args, **kwargs) + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_VOLUME_TYPE) + def test_create_server_with_trusted_volume_type_pre_2_67_fails(self, + get_min_ver): + """Make sure we can't use volume_type before 2.67""" + self.body['server'].update( + {'block_device_mapping_v2': self.block_device_mapping_v2}) + self.req.body = jsonutils.dump_as_bytes(self.block_device_mapping_v2) + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.66') + ex = self.assertRaises( + exception.ValidationError, self.controller.create, self.req, + body=self.body) + self.assertIn("'volume_type' was unexpected", six.text_type(ex)) + + @mock.patch.object(compute_api.API, 'create', + side_effect=exception.VolumeTypeNotFound( + id_or_name='fake-lvm-1')) + def test_create_instance_with_volume_type_not_found(self, mock_create): + """Trying to boot from volume with a volume type that does not exist + will result in a 400 error. + """ + params = {'block_device_mapping_v2': self.block_device_mapping_v2} + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + self.assertIn('Volume type fake-lvm-1 could not be found', + six.text_type(ex)) + + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=compute_api.MIN_COMPUTE_VOLUME_TYPE - 1) + def test_check_volume_type_new_inst_old_compute(self, get_min_version): + """Trying to boot from volume with a volume_type but not all computes + are upgraded will result in a 409 error. + """ + params = {'block_device_mapping_v2': self.block_device_mapping_v2} + ex = self.assertRaises(webob.exc.HTTPConflict, + self._test_create_extra, params) + self.assertIn('Volume type support is not yet available', + six.text_type(ex)) + + def test_create_instance_with_volume_type_empty_string(self): + """Test passing volume_type='' which is accepted but not used.""" + self.block_device_mapping_v2[0]['volume_type'] = '' + params = {'block_device_mapping_v2': self.block_device_mapping_v2} + self._test_create_extra(params) + + def test_create_instance_with_none_volume_type(self): + """Test passing volume_type=None which is accepted but not used.""" + self.block_device_mapping_v2[0]['volume_type'] = None + params = {'block_device_mapping_v2': self.block_device_mapping_v2} + self._test_create_extra(params) + + def test_create_instance_without_volume_type(self): + """Test passing without volume_type which is accepted but not used.""" + self.block_device_mapping_v2[0].pop('volume_type') + params = {'block_device_mapping_v2': self.block_device_mapping_v2} + self._test_create_extra(params) + + def test_create_instance_with_volume_type_too_long(self): + """Tests the maxLength schema validation on volume_type.""" + self.block_device_mapping_v2[0]['volume_type'] = 'X' * 256 + params = {'block_device_mapping_v2': self.block_device_mapping_v2} + ex = self.assertRaises(exception.ValidationError, + self._test_create_extra, params) + self.assertIn('is too long', six.text_type(ex)) + + class ServersControllerCreateTestWithMock(test.TestCase): image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' flavor_ref = 'http://localhost/123/flavors/3' diff --git a/nova/tests/unit/test_block_device.py b/nova/tests/unit/test_block_device.py index c5668a705a91..4e829812e589 100644 --- a/nova/tests/unit/test_block_device.py +++ b/nova/tests/unit/test_block_device.py @@ -17,6 +17,7 @@ Tests for Block Device utility functions. """ from oslo_utils.fixture import uuidsentinel as uuids +import six from nova import block_device from nova import exception @@ -599,6 +600,45 @@ class TestBlockDeviceDict(test.NoDBTestCase): self.assertEqual(retexp, block_device.BlockDeviceDict.from_api(api_dict, True)) + def test_from_api_invalid_oneof_image_id_or_destination_local_mapping( + self): + api_dict = {'id': 1, + 'source_type': 'image', + 'destination_type': 'local', + 'uuid': 'fake-volume-id-1', + 'volume_type': 'fake-lvm-1', + 'boot_index': 1} + ex = self.assertRaises(exception.InvalidBDMFormat, + block_device.BlockDeviceDict.from_api, + api_dict, False) + self.assertIn('Mapping image to local is not supported', + six.text_type(ex)) + + def test_from_api_invalid_volume_type_to_destination_local_mapping(self): + api_dict = {'id': 1, + 'source_type': 'volume', + 'destination_type': 'local', + 'uuid': 'fake-volume-id-1', + 'volume_type': 'fake-lvm-1'} + ex = self.assertRaises(exception.InvalidBDMFormat, + block_device.BlockDeviceDict.from_api, + api_dict, False) + self.assertIn('Specifying a volume_type with destination_type=local ' + 'is not supported', six.text_type(ex)) + + def test_from_api_invalid_specify_volume_type_with_source_volume_mapping( + self): + api_dict = {'id': 1, + 'source_type': 'volume', + 'destination_type': 'volume', + 'uuid': 'fake-volume-id-1', + 'volume_type': 'fake-lvm-1'} + ex = self.assertRaises(exception.InvalidBDMFormat, + block_device.BlockDeviceDict.from_api, + api_dict, False) + self.assertIn('Specifying volume type to existing volume is ' + 'not supported', six.text_type(ex)) + def test_legacy(self): for legacy, new in zip(self.legacy_mapping, self.new_mapping): self.assertThat( diff --git a/releasenotes/notes/boot-instance-specific-storage-backend-c34ee0a871efec3b.yaml b/releasenotes/notes/boot-instance-specific-storage-backend-c34ee0a871efec3b.yaml new file mode 100644 index 000000000000..b95bf18c3a25 --- /dev/null +++ b/releasenotes/notes/boot-instance-specific-storage-backend-c34ee0a871efec3b.yaml @@ -0,0 +1,11 @@ +--- +features: + - Microversion 2.67 adds the optional parameter ``volume_type`` to + block_device_mapping_v2, which can be used to specify ``volume_type`` + when creating a server. + + This would only apply to BDMs with ``source_type`` of `blank`, `image` and + `snapshot` and ``destination_type`` of `volume`. + + The compute API will reject server create requests with a specified + ``volume_type`` until all nova-compute services are upgraded to Stein.