From b9d63105566c84db11a976846844ad7b3a0b331e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 1 Aug 2019 15:11:29 -0400 Subject: [PATCH] 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 --- openstackclient/compute/v2/server.py | 33 +++++++- .../functional/compute/v2/test_server.py | 76 +++++++++++++++++++ .../tests/unit/compute/v2/test_server.py | 27 +++++++ ...add-boot-from-volume-cd411b1ca776bb3c.yaml | 8 ++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/story-2006302-add-boot-from-volume-cd411b1ca776bb3c.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index fee2b27d4a..95c2f28a81 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -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='', + 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='', @@ -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] diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 67e2a66ef8..2bca70d0ec 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -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 diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index cd12db066f..ae6b6e40f4 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -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', diff --git a/releasenotes/notes/story-2006302-add-boot-from-volume-cd411b1ca776bb3c.yaml b/releasenotes/notes/story-2006302-add-boot-from-volume-cd411b1ca776bb3c.yaml new file mode 100644 index 0000000000..1176c883f1 --- /dev/null +++ b/releasenotes/notes/story-2006302-add-boot-from-volume-cd411b1ca776bb3c.yaml @@ -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 `_]