diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 4b5019491..54ec99910 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -975,6 +975,7 @@ nova boot [--trusted-image-certificate-id ] [--host ] [--hypervisor-hostname ] + [--hostname ] Boot a new server. @@ -1149,6 +1150,12 @@ quality of service support, microversion ``2.72`` is required. Requested hypervisor hostname to create servers. Admin only by default. (Supported by API versions '2.74' - '2.latest') +``--hostname `` + Hostname for the instance. This sets the hostname stored in the + metadata server: a utility such as cloud-init running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + .. _nova_clear-password: nova clear-password @@ -2885,6 +2892,7 @@ nova rebuild [--user-data ] [--user-data-unset] [--trusted-image-certificate-id ] [--trusted-image-certificates-unset] + [--hostname ] Shutdown, re-image, and re-boot a server. @@ -2958,6 +2966,12 @@ Shutdown, re-image, and re-boot a server. specified with the ``--trusted-image-certificate-id`` option. (Supported by API versions '2.63' - '2.latest') +``--hostname `` + New hostname for the instance. This only updates the hostname + stored in the metadata server: a utility running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + .. _nova_refresh-network: nova refresh-network @@ -3795,9 +3809,11 @@ nova update .. code-block:: console - usage: nova update [--name ] [--description ] + usage: nova update [--name ] [--description ] + [--hostname ] + -Update the name or the description for a server. +Update attributes of a server. **Positional arguments:** @@ -3815,6 +3831,12 @@ Update the name or the description for a server. will be removed. (Supported by API versions '2.19' - '2.latest') +``--hostname `` + New hostname for the instance. This only updates the hostname + stored in the metadata server: a utility running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + .. _nova_usage: nova usage diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 5f3b69530..d49e8841a 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1") # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.89") +API_MAX_VERSION = api_versions.APIVersion("2.90") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 40d88a19a..071ae5c18 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1905,3 +1905,90 @@ class ServersV278Test(ServersV273Test): self.cs.api_version = api_versions.APIVersion('2.77') s = self.cs.servers.get(1234) self.assertRaises(exceptions.VersionNotFoundForAPIMethod, s.topology) + + +class ServersV290Test(ServersV278Test): + + api_version = '2.90' + + def test_create_server_with_hostname(self): + self.cs.servers.create( + name='My server', + image=1, + flavor=1, + nics='auto', + hostname='new-hostname', + ) + self.assert_called( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'hostname': 'new-hostname' + }, + } + ) + + def test_create_server_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + ex = self.assertRaises( + exceptions.UnsupportedAttribute, + self.cs.servers.create, + name='My server', + image=1, + flavor=1, + nics='auto', + hostname='new-hostname') + self.assertIn( + "'hostname' argument is only allowed since microversion 2.90", + str(ex)) + + def test_rebuild_server_with_hostname(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", hostname='new-hostname') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', '/servers/1234/action', + { + 'rebuild': { + 'imageRef': '1', + 'hostname': 'new-hostname', + }, + }, + ) + + def test_rebuild_server_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + ex = self.assertRaises( + exceptions.UnsupportedAttribute, + self.cs.servers.rebuild, + '1234', fakes.FAKE_IMAGE_UUID_1, + hostname='new-hostname') + self.assertIn('hostname', str(ex)) + + def test_update_server_with_hostname(self): + s = self.cs.servers.get(1234) + + s.update(hostname='new-hostname') + self.assert_called( + 'PUT', '/servers/1234', + { + 'server': { + 'hostname': 'new-hostname', + }, + }, + ) + + def test_update_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + s = self.cs.servers.get(1234) + ex = self.assertRaises( + TypeError, + s.update, + hostname='new-hostname') + self.assertIn('hostname', str(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ed36685f2..e6c0fda7b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1502,6 +1502,34 @@ class ShellTest(utils.TestCase): self.assertRaises(SystemExit, self.run_command, cmd, api_version='2.73') + def test_boot_with_hostname(self): + self.run_command( + 'boot --flavor 1 --image %s ' + '--hostname my-hostname --nic auto ' + 'some-server' % FAKE_UUID_1, + api_version='2.90') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'hostname': 'my-hostname', + }}, + ) + + def test_boot_with_hostname_pre_v290(self): + cmd = ( + 'boot --flavor 1 --image %s --nic auto ' + '--hostname my-hostname some-server' % FAKE_UUID_1 + ) + self.assertRaises( + SystemExit, self.run_command, + cmd, api_version='2.89') + def test_flavor_list(self): out, _ = self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') @@ -2258,6 +2286,31 @@ class ShellTest(utils.TestCase): self.assertNotIn('server_groups', out) self.assertNotIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + def test_rebuild_with_hostname(self): + self.run_command( + 'rebuild sample-server %s --hostname new-hostname' % FAKE_UUID_1, + api_version='2.90') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called( + 'POST', '/servers/1234/action', + { + 'rebuild': { + 'imageRef': FAKE_UUID_1, + 'description': None, + 'hostname': 'new-hostname', + }, + }, + pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_hostname_pre_v290(self): + self.assertRaises( + SystemExit, self.run_command, + 'rebuild sample-server %s --hostname hostname' % FAKE_UUID_1, + api_version='2.89') + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) @@ -2424,6 +2477,25 @@ class ShellTest(utils.TestCase): 'update --description new-description sample-server', api_version='2.18') + def test_update_with_hostname(self): + self.run_command( + 'update --hostname new-hostname sample-server', + api_version='2.90') + expected_put_body = { + "server": { + "hostname": "new-hostname" + } + } + self.assert_called('GET', '/servers/1234', pos=-2) + self.assert_called('PUT', '/servers/1234', expected_put_body, pos=-1) + + def test_update_with_hostname_pre_v290(self): + self.assertRaises( + SystemExit, + self.run_command, + 'update --hostname new-hostname sample-server', + api_version='2.89') + def test_resize(self): self.run_command('resize sample-server 1') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 7768ef7af..76709ec5e 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -67,17 +67,17 @@ class Server(base.Resource): @api_versions.wraps("2.0", "2.18") def update(self, name=None): """ - Update the name for this server. + Update attributes of this server. :param name: Update the server's name. :returns: :class:`Server` """ return self.manager.update(self, name=name) - @api_versions.wraps("2.19") + @api_versions.wraps("2.19", "2.89") def update(self, name=None, description=None): """ - Update the name and the description for this server. + Update attributes of this server. :param name: Update the server's name. :param description: Update the server's description. @@ -88,6 +88,23 @@ class Server(base.Resource): update_kwargs["description"] = description return self.manager.update(self, **update_kwargs) + @api_versions.wraps("2.90") + def update(self, name=None, description=None, hostname=None): + """ + Update attributes of this server. + + :param name: Update the server's name. + :param description: Update the server's description. + :param hostname: Update the server's hostname. + :returns: :class:`Server` + """ + update_kwargs = {"name": name} + if description is not None: + update_kwargs["description"] = description + if hostname is not None: + update_kwargs["hostname"] = hostname + return self.manager.update(self, **update_kwargs) + def get_console_output(self, length=None): """ Get text console log output from Server. @@ -704,7 +721,7 @@ class ServerManager(base.BootingManagerWithFind): config_drive=None, admin_pass=None, disk_config=None, access_ip_v4=None, access_ip_v6=None, description=None, tags=None, trusted_image_certificates=None, - host=None, hypervisor_hostname=None, **kwargs): + host=None, hypervisor_hostname=None, hostname=None, **kwargs): """ Create (boot) a new server. """ @@ -833,6 +850,9 @@ class ServerManager(base.BootingManagerWithFind): if hypervisor_hostname: body['server']['hypervisor_hostname'] = hypervisor_hostname + if hostname: + body['server']['hostname'] = hostname + return self._create('/servers', body, response_key, return_raw=return_raw, **kwargs) @@ -1318,10 +1338,8 @@ class ServerManager(base.BootingManagerWithFind): config_drive=None, disk_config=None, admin_pass=None, access_ip_v4=None, access_ip_v6=None, trusted_image_certificates=None, - host=None, hypervisor_hostname=None, + host=None, hypervisor_hostname=None, hostname=None, **kwargs): - # TODO(anthony): indicate in doc string if param is an extension - # and/or optional """ Create (boot) a new server. @@ -1390,6 +1408,8 @@ class ServerManager(base.BootingManagerWithFind): (allowed since microversion 2.74) :param hypervisor_hostname: requested hypervisor hostname to create servers (allowed since microversion 2.74) + :param hostname: requested hostname of server (allowed since + microversion 2.90) """ if not min_count: min_count = 1 @@ -1453,6 +1473,10 @@ class ServerManager(base.BootingManagerWithFind): raise exceptions.UnsupportedAttribute( "hypervisor_hostname", "2.74") + hostname_microversion = api_versions.APIVersion("2.90") + if hostname and self.api_version < hostname_microversion: + raise exceptions.UnsupportedAttribute("hostname", "2.90") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, @@ -1463,7 +1487,7 @@ class ServerManager(base.BootingManagerWithFind): access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, trusted_image_certificates=trusted_image_certificates, host=host, hypervisor_hostname=hypervisor_hostname, - **kwargs) + hostname=hostname, **kwargs) if block_device_mapping: boot_kwargs['block_device_mapping'] = block_device_mapping @@ -1479,10 +1503,11 @@ class ServerManager(base.BootingManagerWithFind): @api_versions.wraps("2.0", "2.18") def update(self, server, name=None): """ - Update the name for a server. + Update attributes of a server. :param server: The :class:`Server` (or its ID) to update. :param name: Update the server's name. + :returns: :class:`Server` """ if name is None: return @@ -1495,15 +1520,16 @@ class ServerManager(base.BootingManagerWithFind): return self._update("/servers/%s" % base.getid(server), body, "server") - @api_versions.wraps("2.19") + @api_versions.wraps("2.19", "2.89") def update(self, server, name=None, description=None): """ - Update the name or the description for a server. + Update attributes of a server. :param server: The :class:`Server` (or its ID) to update. :param name: Update the server's name. :param description: Update the server's description. If it equals to empty string(i.g. ""), the server description will be removed. + :returns: :class:`Server` """ if name is None and description is None: return @@ -1518,6 +1544,36 @@ class ServerManager(base.BootingManagerWithFind): return self._update("/servers/%s" % base.getid(server), body, "server") + @api_versions.wraps("2.90") + def update(self, server, name=None, description=None, hostname=None): + """ + Update attributes of a server. + + :param server: The :class:`Server` (or its ID) to update. + :param name: Update the server's name. + :param description: Update the server's description. If it equals to + empty string(i.g. ""), the server description will be removed. + :param hostname: Update the server's hostname as recorded by the + metadata service. Note that a separate utility running on the + guest will be necessary to reflect these changes in the guest + itself. + :returns: :class:`Server` + """ + if name is None and description is None and hostname is None: + return + + body = {"server": {}} + if name: + body["server"]["name"] = name + if description == "": + body["server"]["description"] = None + elif description: + body["server"]["description"] = description + if hostname: + body["server"]["hostname"] = hostname + + return self._update("/servers/%s" % base.getid(server), body, "server") + def change_password(self, server, password): """ Update the password for a server. @@ -1548,6 +1604,7 @@ class ServerManager(base.BootingManagerWithFind): """ return self._action('reboot', server, {'type': reboot_type}) + # TODO(stephenfin): Expand out kwargs def rebuild(self, server, image, password=None, disk_config=None, preserve_ephemeral=False, name=None, meta=None, files=None, **kwargs): @@ -1555,34 +1612,36 @@ class ServerManager(base.BootingManagerWithFind): Rebuild -- shut down and then re-image -- a server. :param server: The :class:`Server` (or its ID) to share onto. - :param image: the :class:`Image` (or its ID) to re-image with. - :param password: string to set as password on the rebuilt server. - :param disk_config: partitioning mode to use on the rebuilt server. - Valid values are 'AUTO' or 'MANUAL' + :param image: The :class:`Image` (or its ID) to re-image with. + :param password: String to set as password on the rebuilt server. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' :param preserve_ephemeral: If True, request that any ephemeral device be preserved when rebuilding the instance. Defaults to False. :param name: Something to name the server. :param meta: A dict of arbitrary key/value metadata to store for this - server. Both keys and values must be <=255 characters. + server. Both keys and values must be <=255 characters. :param files: A dict of files to overwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - (deprecated starting with microversion 2.57) - :param description: optional description of the server (allowed since - microversion 2.19) - :param key_name: optional key pair name for rebuild operation; passing - None will unset the key for the server instance - (starting from microversion 2.54) - :param userdata: optional user data to pass to be exposed by the - metadata server; this can be a file type object as - well or a string. If None is specified, the existing - user_data is unset. - (starting from microversion 2.57) + Keys are file names (i.e. ``/etc/passwd``) and values are the file + contents (either as a string or as a file-like object). A maximum + of five entries is allowed, and each file must be 10k or less. + (deprecated starting with microversion 2.57) + :param description: Optional description of the server. If None is + specified, the existing description will be unset. + (starting from microversion 2.19) + :param key_name: Optional key pair name for rebuild operation. If None + is specified, the existing key will be unset. + (starting from microversion 2.54) + :param userdata: Optional user data to pass to be exposed by the + metadata server; this can be a file type object as well or a + string. If None is specified, the existing user_data is unset. + (starting from microversion 2.57) :param trusted_image_certificates: A list of trusted certificate IDs - or None to unset/reset the servers trusted image - certificates (allowed since microversion 2.63) + or None to unset/reset the servers trusted image certificates + (starting from microversion 2.63) + :param hostname: Optional hostname to configure for the instance. If + None is specified, the existing hostname will be unset. + (starting from microversion 2.90) :returns: :class:`Server` """ descr_microversion = api_versions.APIVersion("2.19") @@ -1612,6 +1671,12 @@ class ServerManager(base.BootingManagerWithFind): raise exceptions.UnsupportedAttribute("trusted_image_certificates", "2.63") + if ( + 'hostname' in kwargs and + self.api_version < api_versions.APIVersion("2.90") + ): + raise exceptions.UnsupportedAttribute('hostname', '2.90') + body = {'imageRef': base.getid(image)} if password is not None: body['adminPass'] = password @@ -1628,6 +1693,8 @@ class ServerManager(base.BootingManagerWithFind): if "trusted_image_certificates" in kwargs: body["trusted_image_certificates"] = kwargs[ "trusted_image_certificates"] + if "hostname" in kwargs: + body["hostname"] = kwargs["hostname"] if meta: body['metadata'] = meta if files: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e47fcbc01..dfe93c01a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -532,8 +532,10 @@ def _boot(cs, args): if include_files: boot_kwargs['files'] = files - if ('trusted_image_certificates' in args and - args.trusted_image_certificates): + if ( + 'trusted_image_certificates' in args and + args.trusted_image_certificates + ): boot_kwargs['trusted_image_certificates'] = ( args.trusted_image_certificates) elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): @@ -545,6 +547,9 @@ def _boot(cs, args): "OS_TRUSTED_IMAGE_CERTIFICATE_IDS", "2.63") + if 'hostname' in args and args.hostname: + boot_kwargs['hostname'] = args.hostname + return boot_args, boot_kwargs @@ -970,6 +975,14 @@ def _boot(cs, args): help=_('Requested hypervisor hostname to create servers. Admin only by ' 'default.'), start_version="2.74") +@utils.arg( + '--hostname', + help=_( + 'Hostname for the instance. This sets the hostname stored in the ' + 'metadata server: a utility such as cloud-init running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -2031,6 +2044,14 @@ def do_reboot(cs, args): help=_("Unset trusted_image_certificates in the server. Cannot be " "specified with the '--trusted-image-certificate-id' option."), start_version="2.63") +@utils.arg( + '--hostname', + help=_( + 'New hostname for the instance. This only updates the hostname ' + 'stored in the metadata server: a utility running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -2121,6 +2142,9 @@ def do_rebuild(cs, args): "OS_TRUSTED_IMAGE_CERTIFICATE_IDS", "2.63") + if 'hostname' in args and args.hostname is not None: + kwargs['hostname'] = args.hostname + server = server.rebuild(image, _password, **kwargs) _print_server(cs, args, server) @@ -2145,6 +2169,14 @@ def do_rebuild(cs, args): help=_('New description for the server. If it equals to empty string ' '(i.g. ""), the server description will be removed.'), start_version="2.19") +@utils.arg( + '--hostname', + help=_( + 'New hostname for the instance. This only updates the hostname ' + 'stored in the metadata server: a utility running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_update(cs, args): """Update the name or the description for a server.""" update_kwargs = {} @@ -2152,6 +2184,8 @@ def do_update(cs, args): update_kwargs["name"] = args.name if "description" in args and args.description is not None: update_kwargs["description"] = args.description + if "hostname" in args and args.hostname is not None: + update_kwargs["hostname"] = args.hostname _find_server(cs, args.server).update(**update_kwargs) diff --git a/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml b/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml new file mode 100644 index 000000000..8ab21c91d --- /dev/null +++ b/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Added support for `microversion 2.90`_. This microversion provides the + ability to manually configure the instance ``hostname`` attribute when + creating a new instance (``nova boot --hostname HOSTNAME ...``), updating + an existing instance (``nova update --hostname HOSTNAME ...``), or + rebuilding an existing instance (``nova rebuild --hostname HOSTNAME``). + This attribute is published via the metadata service and config drive and + can be used by init scripts such as ``cloud-init`` to configure the guest's + hostname. + + .. _microversion 2.90: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-90