Merge "Add openstack server create --boot-from-volume option"
This commit is contained in:
commit
8ef2602447
@ -567,6 +567,19 @@ class CreateServer(command.ShowOne):
|
|||||||
'only by default. (supported by --os-compute-api-version '
|
'only by default. (supported by --os-compute-api-version '
|
||||||
'2.74 or above)'),
|
'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(
|
parser.add_argument(
|
||||||
'--block-device-mapping',
|
'--block-device-mapping',
|
||||||
metavar='<dev-name=mapping>',
|
metavar='<dev-name=mapping>',
|
||||||
@ -730,6 +743,10 @@ class CreateServer(command.ShowOne):
|
|||||||
# Lookup parsed_args.volume
|
# Lookup parsed_args.volume
|
||||||
volume = None
|
volume = None
|
||||||
if parsed_args.volume:
|
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 = utils.find_resource(
|
||||||
volume_client.volumes,
|
volume_client.volumes,
|
||||||
parsed_args.volume,
|
parsed_args.volume,
|
||||||
@ -739,8 +756,6 @@ class CreateServer(command.ShowOne):
|
|||||||
flavor = utils.find_resource(compute_client.flavors,
|
flavor = utils.find_resource(compute_client.flavors,
|
||||||
parsed_args.flavor)
|
parsed_args.flavor)
|
||||||
|
|
||||||
boot_args = [parsed_args.server_name, image, flavor]
|
|
||||||
|
|
||||||
files = {}
|
files = {}
|
||||||
for f in parsed_args.file:
|
for f in parsed_args.file:
|
||||||
dst, src = f.split('=', 1)
|
dst, src = f.split('=', 1)
|
||||||
@ -787,6 +802,20 @@ class CreateServer(command.ShowOne):
|
|||||||
'source_type': 'volume',
|
'source_type': 'volume',
|
||||||
'destination_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
|
# Handle block device by device name order, like: vdb -> vdc -> vdd
|
||||||
for dev_name in sorted(six.iterkeys(parsed_args.block_device_mapping)):
|
for dev_name in sorted(six.iterkeys(parsed_args.block_device_mapping)):
|
||||||
dev_map = parsed_args.block_device_mapping[dev_name]
|
dev_map = parsed_args.block_device_mapping[dev_name]
|
||||||
|
@ -701,6 +701,82 @@ class ServerTests(common.ComputeTestCase):
|
|||||||
# the attached volume had been deleted
|
# the attached volume had been deleted
|
||||||
pass
|
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):
|
def test_server_create_with_none_network(self):
|
||||||
"""Test server create with none network option."""
|
"""Test server create with none network option."""
|
||||||
server_name = uuid.uuid4().hex
|
server_name = uuid.uuid4().hex
|
||||||
|
@ -1698,6 +1698,33 @@ class TestServerCreate(TestServer):
|
|||||||
self.cmd.take_action,
|
self.cmd.take_action,
|
||||||
parsed_args)
|
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):
|
def test_server_create_image_property(self):
|
||||||
arglist = [
|
arglist = [
|
||||||
'--image-property', 'hypervisor_type=qemu',
|
'--image-property', 'hypervisor_type=qemu',
|
||||||
|
@ -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>`_]
|
Loading…
Reference in New Issue
Block a user