diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 7fb218c76..435ff9c9f 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.55") +API_MAX_VERSION = api_versions.APIVersion("2.56") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index f019a2277..2402cd106 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -452,6 +452,8 @@ class V1(Base): # but we can not specify version in data_fixture now and this is # V1 data, so just let it pass pass + elif action == 'migrate': + return None elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index b59f55496..bdb8d6735 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -687,7 +687,7 @@ class FakeSessionClient(base_client.SessionClient): # Server actions # - none_actions = ['revertResize', 'migrate', 'os-stop', 'os-start', + none_actions = ['revertResize', 'os-stop', 'os-start', 'forceDelete', 'restore', 'pause', 'unpause', 'unlock', 'unrescue', 'resume', 'suspend', 'lock', 'shelve', 'shelveOffload', 'unshelve', 'resetNetwork'] @@ -749,6 +749,15 @@ class FakeSessionClient(base_client.SessionClient): if self.api_version < api_versions.APIVersion("2.25"): expected.add('disk_over_commit') assert set(body[action].keys()) == expected + elif action == 'migrate': + if self.api_version < api_versions.APIVersion("2.56"): + assert body[action] is None + else: + expected = set() + if 'host' in body[action].keys(): + # host can be optional + expected.add('host') + assert set(body[action].keys()) == expected elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 396e3b763..fb18261b3 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1581,3 +1581,26 @@ class ServersV254Test(ServersV252Test): '1234', fakes.FAKE_IMAGE_UUID_1, key_name='test_keypair') self.assertIn('key_name', six.text_type(ex.message)) + + +class ServersV256Test(ServersV254Test): + + api_version = "2.56" + + def test_migrate_server(self): + s = self.cs.servers.get(1234) + ret = s.migrate() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'migrate': {}}) + ret = s.migrate(host='target-host') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'migrate': {'host': 'target-host'}}) + + def test_migrate_server_pre_256_fails(self): + self.cs.api_version = api_versions.APIVersion('2.55') + s = self.cs.servers.get(1234) + ex = self.assertRaises(TypeError, + s.migrate, host='target-host') + self.assertIn('host', six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7198715ca..daf6da016 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1628,6 +1628,23 @@ class ShellTest(utils.TestCase): self.run_command('migrate sample-server') self.assert_called('POST', '/servers/1234/action', {'migrate': None}) + def test_migrate_pre_v256(self): + self.assertRaises(SystemExit, + self.run_command, + 'migrate --host target-host sample-server', + api_version='2.55') + + def test_migrate_v256(self): + self.run_command('migrate sample-server', + api_version='2.56') + self.assert_called('POST', '/servers/1234/action', + {'migrate': {}}) + + self.run_command('migrate --host target-host sample-server', + api_version='2.56') + self.assert_called('POST', '/servers/1234/action', + {'migrate': {'host': 'target-host'}}) + 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 ed8e2b55f..1e236c894 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -327,6 +327,7 @@ class Server(base.Resource): """Diagnostics -- Retrieve server diagnostics.""" return self.manager.diagnostics(self) + @api_versions.wraps("2.0", "2.55") def migrate(self): """ Migrate a server to a new host. @@ -335,6 +336,16 @@ class Server(base.Resource): """ return self.manager.migrate(self) + @api_versions.wraps("2.56") + def migrate(self, host=None): + """ + Migrate a server to a new host. + + :param host: (Optional) The target host. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.migrate(self, host=host) + def remove_fixed_ip(self, address): """ Remove an IP address. @@ -1545,6 +1556,7 @@ class ServerManager(base.BootingManagerWithFind): body, **kwargs) return Server(self, body['server'], resp=resp) + @api_versions.wraps("2.0", "2.55") def migrate(self, server): """ Migrate a server to a new host. @@ -1554,6 +1566,22 @@ class ServerManager(base.BootingManagerWithFind): """ return self._action('migrate', server) + @api_versions.wraps("2.56") + def migrate(self, server, host=None): + """ + Migrate a server to a new host. + + :param server: The :class:`Server` (or its ID). + :param host: (Optional) The target host. + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = {} + + if host: + info['host'] = host + + return self._action('migrate', server, info) + def resize(self, server, flavor, disk_config=None, **kwargs): """ Resize a server's resources. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a25e90b7f..f9487a8b9 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1950,6 +1950,12 @@ def do_resize_revert(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--host', + metavar='', + default=None, + help=_('Destination host name.'), + start_version='2.56') @utils.arg( '--poll', dest='poll', @@ -1957,9 +1963,13 @@ def do_resize_revert(cs, args): default=False, help=_('Report the server migration progress until it completes.')) def do_migrate(cs, args): - """Migrate a server. The new host will be selected by the scheduler.""" + """Migrate a server.""" + update_kwargs = {} + if 'host' in args and args.host: + update_kwargs['host'] = args.host + server = _find_server(cs, args.server) - server.migrate() + server.migrate(**update_kwargs) if args.poll: _poll_for_status(cs.servers.get, server.id, 'migrating', diff --git a/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml b/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml new file mode 100644 index 000000000..00317ec6a --- /dev/null +++ b/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml @@ -0,0 +1,8 @@ +--- +features: + - Added a new ``--host`` option to ``nova migrate`` command + in microversion 2.56. It enables administrators to specify + a target host when cold migating a server. The target host will be + validated by the scheduler. The target host cannot be the same as + the current host on which the server is running and must be in the + same cell that the server is currently in.