diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index ea87d9cea1..a0afa389f6 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -575,14 +575,17 @@ class CreateServer(command.ShowOne): # NOTE(RuiChen): Add '\n' at the end of line to put each item in # the separated line, avoid the help message looks # messy, see _SmartHelpFormatter in cliff. + # FIXME(mriedem): Technically can be the name or ID. help=_('Create a block device on the server.\n' 'Block device mapping in the format\n' '=:::\n' ': block device name, like: vdb, xvdc ' '(required)\n' - ': UUID of the volume or snapshot (required)\n' - ': volume or snapshot; default: volume (optional)\n' - ': volume size if create from snapshot ' + ': UUID of the volume, volume snapshot or image ' + '(required)\n' + ': volume, snapshot or image; default: volume ' + '(optional)\n' + ': volume size if create from image or snapshot ' '(optional)\n' ': true or false; default: false ' '(optional)\n' @@ -793,7 +796,7 @@ class CreateServer(command.ShowOne): mapping = {'device_name': dev_name} # 1. decide source and destination type if (len(dev_map) > 1 and - dev_map[1] in ('volume', 'snapshot')): + dev_map[1] in ('volume', 'snapshot', 'image')): mapping['source_type'] = dev_map[1] else: mapping['source_type'] = 'volume' @@ -808,14 +811,29 @@ class CreateServer(command.ShowOne): snapshot_id = utils.find_resource( volume_client.volume_snapshots, dev_map[0]).id mapping['uuid'] = snapshot_id + elif mapping['source_type'] == 'image': + # NOTE(mriedem): In case --image is specified with the same + # image, that becomes the root disk for the server. If the + # block device is specified with a root device name, e.g. + # vda, then the compute API will likely fail complaining + # that there is a conflict. So if using the same image ID, + # which doesn't really make sense but it's allowed, the + # device name would need to be a non-root device, e.g. vdb. + # Otherwise if the block device image is different from the + # one specified by --image, then the compute service will + # create a volume from the image and attach it to the + # server as a non-root volume. + image_id = utils.find_resource( + image_client.images, dev_map[0]).id + mapping['uuid'] = image_id # 3. append size and delete_on_termination if exist if len(dev_map) > 2 and dev_map[2]: mapping['volume_size'] = dev_map[2] if len(dev_map) > 3 and dev_map[3]: mapping['delete_on_termination'] = dev_map[3] else: - msg = _("Volume or snapshot (name or ID) must be specified if " - "--block-device-mapping is specified") + msg = _("Volume, volume snapshot or image (name or ID) must " + "be specified if --block-device-mapping is specified") raise exceptions.CommandError(msg) block_device_mapping_v2.append(mapping) diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index e52a42d3a1..67e2a66ef8 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -614,6 +614,93 @@ class ServerTests(common.ComputeTestCase): # the attached volume had been deleted pass + def test_server_boot_with_bdm_image(self): + # Tests creating a server where the root disk is backed by the given + # --image but a --block-device-mapping with type=image is provided so + # that the compute service creates a volume from that image and + # attaches it as a non-root volume on the server. The block device is + # marked as delete_on_termination=True so it will be automatically + # deleted when the server is deleted. + + # create server with bdm type=image + # NOTE(mriedem): This test is a bit unrealistic in that specifying the + # same image in the block device as the --image option does not really + # make sense, but we just want to make sure everything is processed + # as expected where nova creates a volume from the image and attaches + # that volume to the server. + server_name = uuid.uuid4().hex + server = json.loads(self.openstack( + 'server create -f json ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + '--block-device-mapping ' + # This means create a 1GB volume from the specified image, attach + # it to the server at /dev/vdb and delete the volume when the + # server is deleted. + 'vdb=' + self.image_name + ':image:1:true ' + + self.network_arg + ' ' + + '--wait ' + + server_name + )) + self.assertIsNotNone(server["id"]) + self.assertEqual( + server_name, + server['name'], + ) + self.wait_for_status(server_name, 'ACTIVE') + + # check server volumes_attached, format is + # {"volumes_attached": "id='2518bc76-bf0b-476e-ad6b-571973745bb5'",} + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + server_name + )) + volumes_attached = cmd_output['volumes_attached'] + self.assertTrue(volumes_attached.startswith('id=')) + attached_volume_id = volumes_attached.replace('id=', '') + + # check the volume that attached on server + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + attached_volume_id + )) + attachments = cmd_output['attachments'] + self.assertEqual( + 1, + len(attachments), + ) + self.assertEqual( + server['id'], + attachments[0]['server_id'], + ) + self.assertEqual( + "in-use", + cmd_output['status'], + ) + # TODO(mriedem): If we can parse the volume_image_metadata field from + # the volume show output we could assert the image_name is what we + # specified. volume_image_metadata is something like this: + # {u'container_format': u'bare', u'min_ram': u'0', + # u'disk_format': u'qcow2', u'image_name': u'cirros-0.4.0-x86_64-disk', + # u'image_id': u'05496c83-e2df-4c2f-9e48-453b6e49160d', + # u'checksum': u'443b7623e27ecf03dc9e01ee93f67afe', u'min_disk': u'0', + # u'size': u'12716032'} + + # delete server, then check the attached volume has been deleted + self.openstack('server delete --wait ' + server_name) + cmd_output = json.loads(self.openstack( + 'volume list -f json' + )) + target_volume = [each_volume + for each_volume in cmd_output + if each_volume['ID'] == attached_volume_id] + if target_volume: + # check the attached volume is 'deleting' status + self.assertEqual('deleting', target_volume[0]['Status']) + else: + # the attached volume had been deleted + pass + def test_server_create_with_none_network(self): """Test server create with none network option.""" server_name = uuid.uuid4().hex diff --git a/releasenotes/notes/story-2006302-support-bdm-type-image-0becfb63bd4fb969.yaml b/releasenotes/notes/story-2006302-support-bdm-type-image-0becfb63bd4fb969.yaml new file mode 100644 index 0000000000..6e8e8ba0b8 --- /dev/null +++ b/releasenotes/notes/story-2006302-support-bdm-type-image-0becfb63bd4fb969.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``server create --block-device-mapping`` option now supports + an ``image`` type in addition to ``volume`` and ``snapshot``. When + specifying an image block device the compute service will create a volume + from the image of the specified size and attach it to the server. + [Story `2006302 `_]