Add openstack server create --boot-from-volume option

This adds a --boot-from-volume option to the server create
command which is used with the --image or --image-property
option and will create a volume-backed server from the
specified image with the specified size. Similar to the
--volume option, the created root volume will not be deleted
when the server is deleted. The --boot-from-volume option
is not allowed with the --volume option since they both create
a block device mapping with boot_index=0.

Change-Id: I88c590361cb232c1df7b5bb010dcea307080d34c
Story: 2006302
Task: 36017
This commit is contained in:
Matt Riedemann 2019-08-01 15:11:29 -04:00
parent c28ed25e3a
commit b9d6310556
4 changed files with 142 additions and 2 deletions

View File

@ -567,6 +567,19 @@ class CreateServer(command.ShowOne):
'only by default. (supported by --os-compute-api-version '
'2.74 or above)'),
)
parser.add_argument(
'--boot-from-volume',
metavar='<volume-size>',
type=int,
help=_('When used in conjunction with the ``--image`` or '
'``--image-property`` option, this option automatically '
'creates a block device mapping with a boot index of 0 '
'and tells the compute service to create a volume of the '
'given size (in GB) from the specified image and use it '
'as the root disk of the server. The root volume will not '
'be deleted when the server is deleted. This option is '
'mutually exclusive with the ``--volume`` option.')
)
parser.add_argument(
'--block-device-mapping',
metavar='<dev-name=mapping>',
@ -730,6 +743,10 @@ class CreateServer(command.ShowOne):
# Lookup parsed_args.volume
volume = None
if parsed_args.volume:
# --volume and --boot-from-volume are mutually exclusive.
if parsed_args.boot_from_volume:
raise exceptions.CommandError(
_('--volume is not allowed with --boot-from-volume'))
volume = utils.find_resource(
volume_client.volumes,
parsed_args.volume,
@ -739,8 +756,6 @@ class CreateServer(command.ShowOne):
flavor = utils.find_resource(compute_client.flavors,
parsed_args.flavor)
boot_args = [parsed_args.server_name, image, flavor]
files = {}
for f in parsed_args.file:
dst, src = f.split('=', 1)
@ -787,6 +802,20 @@ class CreateServer(command.ShowOne):
'source_type': 'volume',
'destination_type': 'volume'
}]
elif parsed_args.boot_from_volume:
# Tell nova to create a root volume from the image provided.
block_device_mapping_v2 = [{
'uuid': image.id,
'boot_index': '0',
'source_type': 'image',
'destination_type': 'volume',
'volume_size': parsed_args.boot_from_volume
}]
# If booting from volume we do not pass an image to compute.
image = None
boot_args = [parsed_args.server_name, image, flavor]
# Handle block device by device name order, like: vdb -> vdc -> vdd
for dev_name in sorted(six.iterkeys(parsed_args.block_device_mapping)):
dev_map = parsed_args.block_device_mapping[dev_name]

View File

@ -701,6 +701,82 @@ class ServerTests(common.ComputeTestCase):
# the attached volume had been deleted
pass
def test_boot_from_volume(self):
# Tests creating a server using --image and --boot-from-volume where
# the compute service will create a root volume of the specified size
# using the provided image, attach it as the root disk for the server
# and not delete the volume when the server is deleted.
server_name = uuid.uuid4().hex
server = json.loads(self.openstack(
'server create -f json ' +
'--flavor ' + self.flavor_name + ' ' +
'--image ' + self.image_name + ' ' +
'--boot-from-volume 1 ' + # create a 1GB volume from the image
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=', '')
# Don't leak the volume when the test exits.
self.addCleanup(self.openstack, 'volume delete ' + attached_volume_id)
# Since the server is volume-backed the GET /servers/{server_id}
# response will have image=''.
self.assertEqual('', cmd_output['image'])
# check the volume that attached on server
cmd_output = json.loads(self.openstack(
'volume show -f json ' +
attached_volume_id
))
# The volume size should be what we specified on the command line.
self.assertEqual(1, int(cmd_output['size']))
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 was not deleted
self.openstack('server delete --wait ' + server_name)
cmd_output = json.loads(self.openstack(
'volume show -f json ' +
attached_volume_id
))
# check the volume is in 'available' status
self.assertEqual('available', cmd_output['status'])
def test_server_create_with_none_network(self):
"""Test server create with none network option."""
server_name = uuid.uuid4().hex

View File

@ -1698,6 +1698,33 @@ class TestServerCreate(TestServer):
self.cmd.take_action,
parsed_args)
def test_server_create_volume_boot_from_volume_conflict(self):
# Tests that specifying --volume and --boot-from-volume results in
# an error. Since --boot-from-volume requires --image or
# --image-property but those are in a mutex group with --volume, we
# only specify --volume and --boot-from-volume for this test since
# the validation is not handled with argparse.
arglist = [
'--flavor', self.flavor.id,
'--volume', 'volume1',
'--boot-from-volume', '1',
self.new_server.name,
]
verifylist = [
('flavor', self.flavor.id),
('volume', 'volume1'),
('boot_from_volume', 1),
('config_drive', False),
('server_name', self.new_server.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
ex = self.assertRaises(exceptions.CommandError,
self.cmd.take_action, parsed_args)
# Assert it is the error we expect.
self.assertIn('--volume is not allowed with --boot-from-volume',
six.text_type(ex))
def test_server_create_image_property(self):
arglist = [
'--image-property', 'hypervisor_type=qemu',

View File

@ -0,0 +1,8 @@
---
features:
- |
Add ``--boot-from-volume`` option to the ``server create`` command
to create a volume-backed server from the specified image with the
specified size when used in conjunction with the ``--image`` or
``--image-property`` options.
[Story `2006302 <https://storyboard.openstack.org/#!/story/2006302>`_]