diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a09fd44d0a..4750583812 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1154,6 +1154,18 @@ class CreateServer(command.ShowOne): '(supported by --os-compute-api-version 2.52 or above)' ), ) + parser.add_argument( + '--hostname', + metavar='', + help=_( + 'Hostname configured for the server in the metadata service. ' + 'If unset, a hostname will be automatically generated from ' + 'the server name. ' + 'A utility such as cloud-init is required to propagate the ' + 'hostname in the metadata service to the guest OS itself. ' + '(supported by --os-compute-api-version 2.90 or above)' + ), + ) parser.add_argument( '--wait', action='store_true', @@ -1618,6 +1630,16 @@ class CreateServer(command.ShowOne): boot_kwargs['hypervisor_hostname'] = ( parsed_args.hypervisor_hostname) + if parsed_args.hostname: + if compute_client.api_version < api_versions.APIVersion("2.90"): + msg = _( + '--os-compute-api-version 2.90 or greater is required to ' + 'support the --hostname option' + ) + raise exceptions.CommandError(msg) + + boot_kwargs['hostname'] = parsed_args.hostname + LOG.debug('boot_args: %s', boot_args) LOG.debug('boot_kwargs: %s', boot_kwargs) @@ -3273,6 +3295,16 @@ class RebuildServer(command.ShowOne): '(supported by --os-compute-api-version 2.63 or above)' ), ) + parser.add_argument( + '--hostname', + metavar='', + help=_( + 'Hostname configured for the server in the metadata service. ' + 'A separate utility running in the guest is required to ' + 'propagate changes to this value to the guest OS itself. ' + '(supported by --os-compute-api-version 2.90 or above)' + ), + ) parser.add_argument( '--wait', action='store_true', @@ -3390,6 +3422,16 @@ class RebuildServer(command.ShowOne): kwargs['trusted_image_certificates'] = None + if parsed_args.hostname: + if compute_client.api_version < api_versions.APIVersion('2.90'): + msg = _( + '--os-compute-api-version 2.90 or greater is required to ' + 'support the --hostname option' + ) + raise exceptions.CommandError(msg) + + kwargs['hostname'] = parsed_args.hostname + try: server = server.rebuild(image, parsed_args.password, **kwargs) finally: @@ -4076,6 +4118,16 @@ class SetServer(command.Command): '(supported by --os-compute-api-version 2.26 or above)' ), ) + parser.add_argument( + '--hostname', + metavar='', + help=_( + 'Hostname configured for the server in the metadata service. ' + 'A separate utility running in the guest is required to ' + 'propagate changes to this value to the guest OS itself. ' + '(supported by --os-compute-api-version 2.90 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -4102,8 +4154,27 @@ class SetServer(command.Command): ) raise exceptions.CommandError(msg) + if parsed_args.hostname: + if server.api_version < api_versions.APIVersion('2.90'): + msg = _( + '--os-compute-api-version 2.90 or greater is required to ' + 'support the --hostname option' + ) + raise exceptions.CommandError(msg) + + update_kwargs = {} + if parsed_args.name: - server.update(name=parsed_args.name) + update_kwargs['name'] = parsed_args.name + + if parsed_args.description: + update_kwargs['description'] = parsed_args.description + + if parsed_args.hostname: + update_kwargs['hostname'] = parsed_args.hostname + + if update_kwargs: + server.update(**update_kwargs) if parsed_args.properties: compute_client.servers.set_meta(server, parsed_args.properties) @@ -4124,9 +4195,6 @@ class SetServer(command.Command): elif parsed_args.no_password: server.clear_password() - if parsed_args.description: - server.update(description=parsed_args.description) - if parsed_args.tags: for tag in parsed_args.tags: server.add_tag(tag=tag) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 57840cb076..cab9efd0c0 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -3554,6 +3554,76 @@ class TestServerCreate(TestServer): self.assertFalse(self.images_mock.called) self.assertFalse(self.flavors_mock.called) + def test_server_create_with_hostname_v290(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.90') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--hostname', 'hostname', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('hostname', 'hostname'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + admin_pass=None, + block_device_mapping_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + hostname='hostname', + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_hostname_pre_v290(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.89') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--hostname', 'hostname', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('hostname', 'hostname'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerDelete(TestServer): @@ -6235,6 +6305,46 @@ class TestServerRebuild(TestServer): self.cmd.take_action, parsed_args) + def test_rebuild_with_hostname(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.90') + + arglist = [ + self.server.id, + '--hostname', 'new-hostname' + ] + verifylist = [ + ('server', self.server.id), + ('hostname', 'new-hostname') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, hostname='new-hostname') + + def test_rebuild_with_hostname_pre_v290(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.89') + + arglist = [ + self.server.id, + '--hostname', 'new-hostname', + ] + verifylist = [ + ('server', self.server.id), + ('hostname', 'new-hostname') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + class TestEvacuateServer(TestServer): @@ -7340,10 +7450,10 @@ class TestServerSet(TestServer): mock.sentinel.fake_pass) self.assertIsNone(result) - def test_server_set_with_description_api_newer(self): + def test_server_set_with_description(self): # Description is supported for nova api version 2.19 or above - self.fake_servers[0].api_version = 2.19 + self.fake_servers[0].api_version = api_versions.APIVersion('2.19') arglist = [ '--description', 'foo_description', @@ -7354,18 +7464,15 @@ class TestServerSet(TestServer): ('server', 'foo_vm'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.19): - result = self.cmd.take_action(parsed_args) - self.fake_servers[0].update.assert_called_once_with( - description='foo_description') - self.assertIsNone(result) + result = self.cmd.take_action(parsed_args) + self.fake_servers[0].update.assert_called_once_with( + description='foo_description') + self.assertIsNone(result) - def test_server_set_with_description_api_older(self): + def test_server_set_with_description_pre_v219(self): # Description is not supported for nova api version below 2.19 - self.fake_servers[0].api_version = 2.18 + self.fake_servers[0].api_version = api_versions.APIVersion('2.18') arglist = [ '--description', 'foo_description', @@ -7376,11 +7483,8 @@ class TestServerSet(TestServer): ('server', 'foo_vm'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.19): - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) def test_server_set_with_tag(self): self.fake_servers[0].api_version = api_versions.APIVersion('2.26') @@ -7426,6 +7530,41 @@ class TestServerSet(TestServer): '--os-compute-api-version 2.26 or greater is required', str(ex)) + def test_server_set_with_hostname(self): + + self.fake_servers[0].api_version = api_versions.APIVersion('2.90') + + arglist = [ + '--hostname', 'foo-hostname', + 'foo_vm', + ] + verifylist = [ + ('hostname', 'foo-hostname'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.fake_servers[0].update.assert_called_once_with( + hostname='foo-hostname') + self.assertIsNone(result) + + def test_server_set_with_hostname_pre_v290(self): + + self.fake_servers[0].api_version = api_versions.APIVersion('2.89') + + arglist = [ + '--hostname', 'foo-hostname', + 'foo_vm', + ] + verifylist = [ + ('hostname', 'foo-hostname'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerShelve(TestServer): diff --git a/releasenotes/notes/add-server-hostname-opts-3cb4fd90b5bf47ca.yaml b/releasenotes/notes/add-server-hostname-opts-3cb4fd90b5bf47ca.yaml new file mode 100644 index 0000000000..458d1529e8 --- /dev/null +++ b/releasenotes/notes/add-server-hostname-opts-3cb4fd90b5bf47ca.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``server create``, ``server set`` and ``server rebuild`` commands now + accept an optional ``--hostname HOSTNAME`` option. This can be used to + configure the hostname stored in the metadata service and/or config drive. + Utilities such as ``cloud-init`` can then consume this information to set + the hostname within the guest OS.