diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 25065e220..1e685f893 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -891,9 +891,7 @@ def get_parser(self, prog_name): required=True, help=_('Create server with this flavor (name or ID)'), ) - disk_group = parser.add_mutually_exclusive_group( - required=True, - ) + disk_group = parser.add_mutually_exclusive_group() disk_group.add_argument( '--image', metavar='', @@ -1451,14 +1449,14 @@ def _match_image(image_api, wanted_properties): if volume: block_device_mapping_v2 = [{ 'uuid': volume, - 'boot_index': '0', + 'boot_index': 0, 'source_type': 'volume', 'destination_type': 'volume' }] elif snapshot: block_device_mapping_v2 = [{ 'uuid': snapshot, - 'boot_index': '0', + 'boot_index': 0, 'source_type': 'snapshot', 'destination_type': 'volume', 'delete_on_termination': False @@ -1467,7 +1465,7 @@ def _match_image(image_api, wanted_properties): # Tell nova to create a root volume from the image provided. block_device_mapping_v2 = [{ 'uuid': image.id, - 'boot_index': '0', + 'boot_index': 0, 'source_type': 'image', 'destination_type': 'volume', 'volume_size': parsed_args.boot_from_volume @@ -1604,6 +1602,15 @@ def _match_image(image_api, wanted_properties): block_device_mapping_v2.append(mapping) + if not image and not any( + [bdm.get('boot_index') == 0 for bdm in block_device_mapping_v2] + ): + msg = _( + 'An image (--image, --image-property) or bootable volume ' + '(--volume, --snapshot, --block-device) is required' + ) + raise exceptions.CommandError(msg) + nics = parsed_args.nics if 'auto' in nics or 'none' in nics: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 27ae3e9be..1fd92e93f 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2455,7 +2455,7 @@ def test_server_create_with_volume(self): 'admin_pass': None, 'block_device_mapping_v2': [{ 'uuid': self.volume.id, - 'boot_index': '0', + 'boot_index': 0, 'source_type': 'volume', 'destination_type': 'volume', }], @@ -2506,7 +2506,7 @@ def test_server_create_with_snapshot(self): 'admin_pass': None, 'block_device_mapping_v2': [{ 'uuid': self.snapshot.id, - 'boot_index': '0', + 'boot_index': 0, 'source_type': 'snapshot', 'destination_type': 'volume', 'delete_on_termination': False, @@ -2529,20 +2529,20 @@ def test_server_create_with_snapshot(self): self.assertEqual(self.datalist(), data) def test_server_create_with_block_device(self): - block_device = f'uuid={self.volume.id},source_type=volume' + block_device = f'uuid={self.volume.id},source_type=volume,boot_index=0' arglist = [ - '--image', 'image1', '--flavor', self.flavor.id, '--block-device', block_device, self.new_server.name, ] verifylist = [ - ('image', 'image1'), + ('image', None), ('flavor', self.flavor.id), ('block_devices', [ { 'uuid': self.volume.id, 'source_type': 'volume', + 'boot_index': '0', }, ]), ('server_name', self.new_server.name), @@ -2569,6 +2569,7 @@ def test_server_create_with_block_device(self): 'uuid': self.volume.id, 'source_type': 'volume', 'destination_type': 'volume', + 'boot_index': 0, }, ], 'nics': [], @@ -2578,7 +2579,7 @@ def test_server_create_with_block_device(self): # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( self.new_server.name, - self.image, + None, self.flavor, **kwargs ) @@ -3506,6 +3507,37 @@ def test_server_create_image_property_with_image_list(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_no_boot_device(self): + block_device = f'uuid={self.volume.id},source_type=volume,boot_index=1' + arglist = [ + '--block-device', block_device, + '--flavor', self.flavor.id, + self.new_server.name, + ] + verifylist = [ + ('image', None), + ('flavor', self.flavor.id), + ('block_devices', [ + { + 'uuid': self.volume.id, + 'source_type': 'volume', + 'boot_index': '1', + }, + ]), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertIn( + 'An image (--image, --image-property) or bootable volume ' + '(--volume, --snapshot, --block-device) is required', + str(exc), + ) + def test_server_create_with_swap(self): arglist = [ '--image', 'image1', diff --git a/releasenotes/notes/bug-2010376-e15362bdd6c8d6ec.yaml b/releasenotes/notes/bug-2010376-e15362bdd6c8d6ec.yaml new file mode 100644 index 000000000..9f2ea1278 --- /dev/null +++ b/releasenotes/notes/bug-2010376-e15362bdd6c8d6ec.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + The ``server create`` command will no longer insist on an ``--image``, + ``--image-property``, ``--volume`` or ``--snapshot`` argument when a + volume is provided with a boot index of ``0`` via the ``--block-device`` + option.