summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/compute/v2/server.py30
-rw-r--r--openstackclient/tests/functional/compute/v2/test_server.py87
2 files changed, 111 insertions, 6 deletions
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index ea87d9ce..a0afa389 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 <id> can be the name or ID.
help=_('Create a block device on the server.\n'
'Block device mapping in the format\n'
'<dev-name>=<id>:<type>:<size(GB)>:<delete-on-terminate>\n'
'<dev-name>: block device name, like: vdb, xvdc '
'(required)\n'
- '<id>: UUID of the volume or snapshot (required)\n'
- '<type>: volume or snapshot; default: volume (optional)\n'
- '<size(GB)>: volume size if create from snapshot '
+ '<id>: UUID of the volume, volume snapshot or image '
+ '(required)\n'
+ '<type>: volume, snapshot or image; default: volume '
+ '(optional)\n'
+ '<size(GB)>: volume size if create from image or snapshot '
'(optional)\n'
'<delete-on-terminate>: 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 e52a42d3..67e2a66e 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