diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index afe73ce51..c2fd0c8cf 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -127,3 +127,104 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): 'x-image-meta-status': 'ACTIVE', 'x-image-meta-property-test_key': 'test_value'} return 200, headers, '' + + # + # Servers + # + get_servers_1234_os_server_diagnostics = ( + fakes_v1_1.FakeHTTPClient.get_servers_1234_diagnostics) + + delete_servers_1234_os_attach_interfaces_port_id = ( + fakes_v1_1.FakeHTTPClient.delete_servers_1234_os_interface_port_id) + + def get_servers_1234_os_attach_interfaces(self, **kw): + return (200, {}, {"interface_attachments": [ + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }]}) + + def post_servers_1234_os_attach_interfaces(self, **kw): + return (200, {}, {'interface_attachment': {}}) + + # + # Server Actions + # + def post_servers_1234_action(self, body, **kw): + _headers = None + resp = 202 + body_is_none_list = [ + 'revert_resize', 'migrate', 'stop', 'start', 'force_delete', + 'restore', 'pause', 'unpause', 'lock', 'unlock', 'unrescue', + 'resume', 'suspend', 'lock', 'unlock', 'shelve', 'shelve_offload', + 'unshelve', 'reset_network', 'rescue', 'confirm_resize'] + body_return_map = { + 'rescue': {'admin_password': 'RescuePassword'}, + 'get_console_output': {'output': 'foo'}, + 'rebuild': self.get_servers_1234()[2], + } + body_param_check_exists = { + 'rebuild': 'image_ref', + 'resize': 'flavor_ref'} + body_params_check_exact = { + 'reboot': ['type'], + 'add_fixed_ip': ['network_id'], + 'evacuate': ['host', 'on_shared_storage'], + 'remove_fixed_ip': ['address'], + 'change_password': ['admin_password'], + 'get_console_output': ['length'], + 'get_vnc_console': ['type'], + 'get_spice_console': ['type'], + 'reset_state': ['state'], + 'create_image': ['name', 'metadata'], + 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], + 'create_backup': ['name', 'backup_type', 'rotation']} + + assert len(body.keys()) == 1 + action = list(body)[0] + _body = body_return_map.get(action) + + if action in body_is_none_list: + assert body[action] is None + + if action in body_param_check_exists: + assert body_param_check_exists[action] in body[action] + + if action == 'evacuate': + body[action].pop('admin_password', None) + + if action in body_params_check_exact: + assert set(body[action]) == set(body_params_check_exact[action]) + + if action == 'reboot': + assert body[action]['type'] in ['HARD', 'SOFT'] + elif action == 'confirm_resize': + # This one method returns a different response code + resp = 204 + elif action == 'create_image': + _headers = dict(location="http://blah/images/456") + + if action not in set.union(set(body_is_none_list), + set(body_params_check_exact.keys()), + set(body_param_check_exists.keys())): + raise AssertionError("Unexpected server action: %s" % action) + + return (resp, _headers, _body) + + # + # Server password + # + + def get_servers_1234_os_server_password(self, **kw): + return (200, {}, {'password': ''}) + + def delete_servers_1234_os_server_password(self, **kw): + return (202, {}, None) diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py new file mode 100644 index 000000000..e51b3fd9c --- /dev/null +++ b/novaclient/tests/v3/test_servers.py @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from novaclient import exceptions +from novaclient.tests import utils +from novaclient.tests.v3 import fakes +from novaclient.v3 import servers + + +cs = fakes.FakeClient() + + +class ServersTest(utils.TestCase): + + def test_list_servers(self): + sl = cs.servers.list() + cs.assert_called('GET', '/servers/detail') + for s in sl: + self.assertTrue(isinstance(s, servers.Server)) + + def test_list_servers_undetailed(self): + sl = cs.servers.list(detailed=False) + cs.assert_called('GET', '/servers') + for s in sl: + self.assertTrue(isinstance(s, servers.Server)) + + def test_list_servers_with_marker_limit(self): + sl = cs.servers.list(marker=1234, limit=2) + cs.assert_called('GET', '/servers/detail?limit=2&marker=1234') + for s in sl: + self.assertTrue(isinstance(s, servers.Server)) + + def test_get_server_details(self): + s = cs.servers.get(1234) + cs.assert_called('GET', '/servers/1234') + self.assertTrue(isinstance(s, servers.Server)) + self.assertEqual(s.id, 1234) + self.assertEqual(s.status, 'BUILD') + + def test_get_server_promote_details(self): + s1 = cs.servers.list(detailed=False)[0] + s2 = cs.servers.list(detailed=True)[0] + self.assertNotEqual(s1._info, s2._info) + s1.get() + self.assertEqual(s1._info, s2._info) + + def test_create_server(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + } + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + def test_create_server_userdata_file_object(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata=six.StringIO('hello moto'), + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + def test_create_server_userdata_unicode(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata=six.u('こんにちは'), + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + def test_create_server_userdata_utf8(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata='こんにちは', + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + def test_update_server(self): + s = cs.servers.get(1234) + + # Update via instance + s.update(name='hi') + cs.assert_called('PUT', '/servers/1234') + s.update(name='hi') + cs.assert_called('PUT', '/servers/1234') + + # Silly, but not an error + s.update() + + # Update via manager + cs.servers.update(s, name='hi') + cs.assert_called('PUT', '/servers/1234') + + def test_delete_server(self): + s = cs.servers.get(1234) + s.delete() + cs.assert_called('DELETE', '/servers/1234') + cs.servers.delete(1234) + cs.assert_called('DELETE', '/servers/1234') + cs.servers.delete(s) + cs.assert_called('DELETE', '/servers/1234') + + def test_delete_server_meta(self): + s = cs.servers.delete_meta(1234, ['test_key']) + cs.assert_called('DELETE', '/servers/1234/metadata/test_key') + + def test_set_server_meta(self): + s = cs.servers.set_meta(1234, {'test_key': 'test_value'}) + reval = cs.assert_called('POST', '/servers/1234/metadata', + {'metadata': {'test_key': 'test_value'}}) + + def test_find(self): + server = cs.servers.find(name='sample-server') + cs.assert_called('GET', '/servers', pos=-2) + cs.assert_called('GET', '/servers/1234', pos=-1) + self.assertEqual(server.name, 'sample-server') + + self.assertRaises(exceptions.NoUniqueMatch, cs.servers.find, + flavor={"id": 1, "name": "256 MB Server"}) + + sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + self.assertEqual([s.id for s in sl], [1234, 5678, 9012]) + + def test_reboot_server(self): + s = cs.servers.get(1234) + s.reboot() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.reboot(s, reboot_type='HARD') + cs.assert_called('POST', '/servers/1234/action') + + def test_rebuild_server(self): + s = cs.servers.get(1234) + s.rebuild(image=1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.rebuild(s, image=1) + cs.assert_called('POST', '/servers/1234/action') + s.rebuild(image=1, password='5678') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.rebuild(s, image=1, password='5678') + cs.assert_called('POST', '/servers/1234/action') + + def test_resize_server(self): + s = cs.servers.get(1234) + s.resize(flavor=1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.resize(s, flavor=1) + cs.assert_called('POST', '/servers/1234/action') + + def test_confirm_resized_server(self): + s = cs.servers.get(1234) + s.confirm_resize() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.confirm_resize(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_revert_resized_server(self): + s = cs.servers.get(1234) + s.revert_resize() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.revert_resize(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_migrate_server(self): + s = cs.servers.get(1234) + s.migrate() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.migrate(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_add_fixed_ip(self): + s = cs.servers.get(1234) + s.add_fixed_ip(1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.add_fixed_ip(s, 1) + cs.assert_called('POST', '/servers/1234/action') + + def test_remove_fixed_ip(self): + s = cs.servers.get(1234) + s.remove_fixed_ip('10.0.0.1') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.remove_fixed_ip(s, '10.0.0.1') + cs.assert_called('POST', '/servers/1234/action') + + def test_stop(self): + s = cs.servers.get(1234) + s.stop() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.stop(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_force_delete(self): + s = cs.servers.get(1234) + s.force_delete() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.force_delete(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_restore(self): + s = cs.servers.get(1234) + s.restore() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.restore(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_start(self): + s = cs.servers.get(1234) + s.start() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.start(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_rescue(self): + s = cs.servers.get(1234) + s.rescue() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.rescue(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_unrescue(self): + s = cs.servers.get(1234) + s.unrescue() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.unrescue(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_lock(self): + s = cs.servers.get(1234) + s.lock() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.lock(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_unlock(self): + s = cs.servers.get(1234) + s.unlock() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.unlock(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_backup(self): + s = cs.servers.get(1234) + s.backup('back1', 'daily', 1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.backup(s, 'back1', 'daily', 2) + cs.assert_called('POST', '/servers/1234/action') + + def test_get_console_output_without_length(self): + success = 'foo' + s = cs.servers.get(1234) + s.get_console_output() + self.assertEqual(s.get_console_output(), success) + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_console_output(s) + self.assertEqual(cs.servers.get_console_output(s), success) + cs.assert_called('POST', '/servers/1234/action') + + def test_get_console_output_with_length(self): + success = 'foo' + + s = cs.servers.get(1234) + s.get_console_output(length=50) + self.assertEqual(s.get_console_output(length=50), success) + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_console_output(s, length=50) + self.assertEqual(cs.servers.get_console_output(s, length=50), success) + cs.assert_called('POST', '/servers/1234/action') + + def test_get_password(self): + s = cs.servers.get(1234) + self.assertEqual(s.get_password('/foo/id_rsa'), '') + cs.assert_called('GET', '/servers/1234/os-server-password') + + def test_clear_password(self): + s = cs.servers.get(1234) + s.clear_password() + cs.assert_called('DELETE', '/servers/1234/os-server-password') + + def test_get_server_diagnostics(self): + s = cs.servers.get(1234) + diagnostics = s.diagnostics() + self.assertTrue(diagnostics is not None) + cs.assert_called('GET', '/servers/1234/os-server-diagnostics') + + diagnostics_from_manager = cs.servers.diagnostics(1234) + self.assertTrue(diagnostics_from_manager is not None) + cs.assert_called('GET', '/servers/1234/os-server-diagnostics') + + self.assertEqual(diagnostics, diagnostics_from_manager) + + def test_get_vnc_console(self): + s = cs.servers.get(1234) + s.get_vnc_console('fake') + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_vnc_console(s, 'fake') + cs.assert_called('POST', '/servers/1234/action') + + def test_get_spice_console(self): + s = cs.servers.get(1234) + s.get_spice_console('fake') + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_spice_console(s, 'fake') + cs.assert_called('POST', '/servers/1234/action') + + def test_create_image(self): + s = cs.servers.get(1234) + s.create_image('123') + cs.assert_called('POST', '/servers/1234/action') + s.create_image('123', {}) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.create_image(s, '123') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.create_image(s, '123', {}) + + def test_live_migrate_server(self): + s = cs.servers.get(1234) + s.live_migrate(host='hostname', block_migration=False, + disk_over_commit=False) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.live_migrate(s, host='hostname', block_migration=False, + disk_over_commit=False) + cs.assert_called('POST', '/servers/1234/action') + + def test_reset_state(self): + s = cs.servers.get(1234) + s.reset_state('newstate') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.reset_state(s, 'newstate') + cs.assert_called('POST', '/servers/1234/action') + + def test_reset_network(self): + s = cs.servers.get(1234) + s.reset_network() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.reset_network(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_evacuate(self): + s = cs.servers.get(1234) + s.evacuate('fake_target_host', 'True') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.evacuate(s, 'fake_target_host', 'False', 'NewAdminPassword') + cs.assert_called('POST', '/servers/1234/action') + + def test_interface_list(self): + s = cs.servers.get(1234) + s.interface_list() + cs.assert_called('GET', '/servers/1234/os-attach-interfaces') + + def test_interface_attach(self): + s = cs.servers.get(1234) + s.interface_attach(None, None, None) + cs.assert_called('POST', '/servers/1234/os-attach-interfaces') + + def test_interface_detach(self): + s = cs.servers.get(1234) + s.interface_detach('port-id') + cs.assert_called('DELETE', + '/servers/1234/os-attach-interfaces/port-id') diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index c8af7202d..4cbf8d3e7 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -20,6 +20,7 @@ from novaclient.v3 import flavor_access from novaclient.v3 import flavors from novaclient.v3 import hosts from novaclient.v3 import images +from novaclient.v3 import servers class Client(object): @@ -59,6 +60,7 @@ class Client(object): self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) + self.servers = servers.ServerManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py new file mode 100644 index 000000000..f27b2643d --- /dev/null +++ b/novaclient/v3/servers.py @@ -0,0 +1,892 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Server interface. +""" + +import six + +from novaclient import base +from novaclient import crypto +from novaclient.openstack.common.py3kcompat import urlutils + +REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' + + +class Server(base.Resource): + HUMAN_ID = True + + def __repr__(self): + return "" % self.name + + def delete(self): + """ + Delete (i.e. shut down and delete the image) this server. + """ + self.manager.delete(self) + + def update(self, name=None): + """ + Update the name or the password for this server. + + :param name: Update the server's name. + :param password: Update the root password. + """ + self.manager.update(self, name=name) + + def get_console_output(self, length=None): + """ + Get text console log output from Server. + + :param length: The number of lines you would like to retrieve (as int) + """ + return self.manager.get_console_output(self, length) + + def get_vnc_console(self, console_type): + """ + Get vnc console for a Server. + + :param console_type: Type of console ('novnc' or 'xvpvnc') + """ + return self.manager.get_vnc_console(self, console_type) + + def get_spice_console(self, console_type): + """ + Get spice console for a Server. + + :param console_type: Type of console ('spice-html5') + """ + return self.manager.get_spice_console(self, console_type) + + def get_password(self, private_key): + """ + Get password for a Server. + + :param private_key: Path to private key file for decryption + """ + return self.manager.get_password(self, private_key) + + def clear_password(self): + """ + Get password for a Server. + + """ + return self.manager.clear_password(self) + + def add_fixed_ip(self, network_id): + """ + Add an IP address on a network. + + :param network_id: The ID of the network the IP should be on. + """ + self.manager.add_fixed_ip(self, network_id) + + def remove_floating_ip(self, address): + """ + Remove floating IP from an instance + + :param address: The ip address or FloatingIP to remove + """ + self.manager.remove_floating_ip(self, address) + + def stop(self): + """ + Stop -- Stop the running server. + """ + self.manager.stop(self) + + def force_delete(self): + """ + Force delete -- Force delete a server. + """ + self.manager.force_delete(self) + + def restore(self): + """ + Restore -- Restore a server in 'soft-deleted' state. + """ + self.manager.restore(self) + + def start(self): + """ + Start -- Start the paused server. + """ + self.manager.start(self) + + def pause(self): + """ + Pause -- Pause the running server. + """ + self.manager.pause(self) + + def unpause(self): + """ + Unpause -- Unpause the paused server. + """ + self.manager.unpause(self) + + def lock(self): + """ + Lock -- Lock the instance from certain operations. + """ + self.manager.lock(self) + + def unlock(self): + """ + Unlock -- Remove instance lock. + """ + self.manager.unlock(self) + + def suspend(self): + """ + Suspend -- Suspend the running server. + """ + self.manager.suspend(self) + + def resume(self): + """ + Resume -- Resume the suspended server. + """ + self.manager.resume(self) + + def rescue(self): + """ + Rescue -- Rescue the problematic server. + """ + return self.manager.rescue(self) + + def unrescue(self): + """ + Unrescue -- Unrescue the rescued server. + """ + self.manager.unrescue(self) + + def shelve(self): + """ + Shelve -- Shelve the server. + """ + self.manager.shelve(self) + + def shelve_offload(self): + """ + Shelve_offload -- Remove a shelved server from the compute node. + """ + self.manager.shelve_offload(self) + + def unshelve(self): + """ + Unshelve -- Unshelve the server. + """ + self.manager.unshelve(self) + + def diagnostics(self): + """Diagnostics -- Retrieve server diagnostics.""" + return self.manager.diagnostics(self) + + def migrate(self): + """ + Migrate a server to a new host. + """ + self.manager.migrate(self) + + def remove_fixed_ip(self, address): + """ + Remove an IP address. + + :param address: The IP address to remove. + """ + self.manager.remove_fixed_ip(self, address) + + def change_password(self, password): + """ + Update the password for a server. + """ + self.manager.change_password(self, password) + + def reboot(self, reboot_type=REBOOT_SOFT): + """ + Reboot the server. + + :param reboot_type: either :data:`REBOOT_SOFT` for a software-level + reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + """ + self.manager.reboot(self, reboot_type) + + def rebuild(self, image, password=None, **kwargs): + """ + Rebuild -- shut down and then re-image -- this server. + + :param image: the :class:`Image` (or its ID) to re-image with. + :param password: string to set as password on the rebuilt server. + """ + return self.manager.rebuild(self, image, password=password, **kwargs) + + def resize(self, flavor, **kwargs): + """ + Resize the server's resources. + + :param flavor: the :class:`Flavor` (or its ID) to resize to. + + Until a resize event is confirmed with :meth:`confirm_resize`, the old + server will be kept around and you'll be able to roll back to the old + flavor quickly with :meth:`revert_resize`. All resizes are + automatically confirmed after 24 hours. + """ + self.manager.resize(self, flavor, **kwargs) + + def create_image(self, image_name, metadata=None): + """ + Create an image based on this server. + + :param image_name: The name to assign the newly create image. + :param metadata: Metadata to assign to the image. + """ + return self.manager.create_image(self, image_name, metadata) + + def backup(self, backup_name, backup_type, rotation): + """ + Backup a server instance. + + :param backup_name: Name of the backup image + :param backup_type: The backup type, like 'daily' or 'weekly' + :param rotation: Int parameter representing how many backups to + keep around. + """ + self.manager.backup(self, backup_name, backup_type, rotation) + + def confirm_resize(self): + """ + Confirm that the resize worked, thus removing the original server. + """ + self.manager.confirm_resize(self) + + def revert_resize(self): + """ + Revert a previous resize, switching back to the old server. + """ + self.manager.revert_resize(self) + + @property + def networks(self): + """ + Generate a simplified list of addresses + """ + networks = {} + try: + for network_label, address_list in self.addresses.items(): + networks[network_label] = [a['addr'] for a in address_list] + return networks + except Exception: + return {} + + def live_migrate(self, host=None, + block_migration=False, + disk_over_commit=False): + """ + Migrates a running instance to a new machine. + """ + self.manager.live_migrate(self, host, + block_migration, + disk_over_commit) + + def reset_state(self, state='error'): + """ + Reset the state of an instance to active or error. + """ + self.manager.reset_state(self, state) + + def reset_network(self): + """ + Reset network of an instance. + """ + self.manager.reset_network(self) + + def evacuate(self, host, on_shared_storage, password=None): + """ + Evacuate an instance from failed host to specified host. + + :param host: Name of the target host + :param on_shared_storage: Specifies whether instance files located + on shared storage + :param password: string to set as password on the evacuated server. + """ + return self.manager.evacuate(self, host, on_shared_storage, password) + + def interface_list(self): + """ + List interfaces attached to an instance. + """ + return self.manager.interface_list(self) + + def interface_attach(self, port_id, net_id, fixed_ip): + """ + Attach a network interface to an instance. + """ + return self.manager.interface_attach(self, port_id, net_id, fixed_ip) + + def interface_detach(self, port_id): + """ + Detach a network interface from an instance. + """ + return self.manager.interface_detach(self, port_id) + + +class ServerManager(base.BootingManagerWithFind): + resource_class = Server + + def get(self, server): + """ + Get a server. + + :param server: ID of the :class:`Server` to get. + :rtype: :class:`Server` + """ + return self._get("/servers/%s" % base.getid(server), "server") + + def list(self, detailed=True, search_opts=None, marker=None, limit=None): + """ + Get a list of servers. + + :param detailed: Whether to return detailed server info (optional). + :param search_opts: Search options to filter out servers (optional). + :param marker: Begin returning servers that appear later in the server + list than that represented by this server id (optional). + :param limit: Maximum number of servers to return (optional). + + :rtype: list of :class:`Server` + """ + if search_opts is None: + search_opts = {} + + qparams = {} + + for opt, val in six.iteritems(search_opts): + if val: + qparams[opt] = val + + if marker: + qparams['marker'] = marker + + if limit: + qparams['limit'] = limit + + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if qparams: + new_qparams = sorted(qparams.items(), key=lambda x: x[0]) + query_string = "?%s" % urlutils.urlencode(new_qparams) + else: + query_string = "" + + detail = "" + if detailed: + detail = "/detail" + return self._list("/servers%s%s" % (detail, query_string), "servers") + + def add_fixed_ip(self, server, network_id): + """ + Add an IP address on a network. + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param network_id: The ID of the network the IP should be on. + """ + self._action('add_fixed_ip', server, {'network_id': network_id}) + + def remove_fixed_ip(self, server, address): + """ + Remove an IP address. + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param address: The IP address to remove. + """ + self._action('remove_fixed_ip', server, {'address': address}) + + def get_vnc_console(self, server, console_type): + """ + Get a vnc console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') + """ + + return self._action('get_vnc_console', server, + {'type': console_type})[1] + + def get_spice_console(self, server, console_type): + """ + Get a spice console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of spice console to get ('spice-html5') + """ + + return self._action('get_spice_console', server, + {'type': console_type})[1] + + def get_password(self, server, private_key): + """ + Get password for an instance + + Requires that openssl is installed and in the path + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param private_key: The private key to decrypt password + """ + + _resp, body = self.api.client.get("/servers/%s/os-server-password" + % base.getid(server)) + if body and body.get('password'): + try: + return crypto.decrypt_password(private_key, body['password']) + except Exception as exc: + return '%sFailed to decrypt:\n%s' % (exc, body['password']) + return '' + + def clear_password(self, server): + """ + Clear password for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + """ + + return self._delete("/servers/%s/os-server-password" + % base.getid(server)) + + def stop(self, server): + """ + Stop the server. + """ + return self._action('stop', server, None) + + def force_delete(self, server): + """ + Force delete the server. + """ + return self._action('force_delete', server, None) + + def restore(self, server): + """ + Restore soft-deleted server. + """ + return self._action('restore', server, None) + + def start(self, server): + """ + Start the server. + """ + self._action('start', server, None) + + def pause(self, server): + """ + Pause the server. + """ + self._action('pause', server, None) + + def unpause(self, server): + """ + Unpause the server. + """ + self._action('unpause', server, None) + + def lock(self, server): + """ + Lock the server. + """ + self._action('lock', server, None) + + def unlock(self, server): + """ + Unlock the server. + """ + self._action('unlock', server, None) + + def suspend(self, server): + """ + Suspend the server. + """ + self._action('suspend', server, None) + + def resume(self, server): + """ + Resume the server. + """ + self._action('resume', server, None) + + def rescue(self, server): + """ + Rescue the server. + """ + return self._action('rescue', server, None) + + def unrescue(self, server): + """ + Unrescue the server. + """ + self._action('unrescue', server, None) + + def shelve(self, server): + """ + Shelve the server. + """ + self._action('shelve', server, None) + + def shelve_offload(self, server): + """ + Remove a shelved instance from the compute node. + """ + self._action('shelve_offload', server, None) + + def unshelve(self, server): + """ + Unshelve the server. + """ + self._action('unshelve', server, None) + + def diagnostics(self, server): + """Retrieve server diagnostics.""" + return self.api.client.get("/servers/%s/os-server-diagnostics" % + base.getid(server)) + + def create(self, name, image, flavor, meta=None, files=None, + reservation_id=None, min_count=None, + max_count=None, security_groups=None, userdata=None, + key_name=None, availability_zone=None, + block_device_mapping=None, block_device_mapping_v2=None, + nics=None, scheduler_hints=None, + config_drive=None, **kwargs): + # TODO(anthony): indicate in doc string if param is an extension + # and/or optional + """ + Create (boot) a new server. + + :param name: Something to name the server. + :param image: The :class:`Image` to boot with. + :param flavor: The :class:`Flavor` to boot onto. + :param meta: A dict of arbitrary key/value metadata to store for this + server. A maximum of five entries is allowed, and both + keys and values must be 255 characters or less. + :param files: A dict of files to overrwrite 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. + :param userdata: user data to pass to be exposed by the metadata + server this can be a file type object as well or a + string. + :param reservation_id: a UUID for the set of servers being requested. + :param key_name: (optional extension) name of previously created + keypair to inject into the instance. + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping: (optional extension) A dict of block + device mappings for this server. + :param block_device_mapping_v2: (optional extension) A dict of block + device mappings for this server. + :param nics: (optional extension) an ordered list of nics to be + added to this server, with information about + connected networks, fixed ips, port etc. + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance + :param config_drive: (optional extension) value for config drive + either boolean, or volume-id + """ + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + if min_count > max_count: + min_count = max_count + + boot_args = [name, image, flavor] + + boot_kwargs = dict( + meta=meta, files=files, userdata=userdata, + reservation_id=reservation_id, min_count=min_count, + max_count=max_count, security_groups=security_groups, + key_name=key_name, availability_zone=availability_zone, + scheduler_hints=scheduler_hints, config_drive=config_drive, + **kwargs) + + if block_device_mapping: + resource_url = "/os-volumes_boot" + boot_kwargs['block_device_mapping'] = block_device_mapping + elif block_device_mapping_v2: + resource_url = "/os-volumes_boot" + boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 + else: + resource_url = "/servers" + if nics: + boot_kwargs['nics'] = nics + + response_key = "server" + return self._boot(resource_url, response_key, *boot_args, + **boot_kwargs) + + def update(self, server, name=None): + """ + Update the name or the password for a server. + + :param server: The :class:`Server` (or its ID) to update. + :param name: Update the server's name. + """ + if name is None: + return + + body = { + "server": { + "name": name, + }, + } + + return self._update("/servers/%s" % base.getid(server), body, "server") + + def change_password(self, server, password): + """ + Update the password for a server. + """ + self._action("change_password", server, {"admin_password": password}) + + def delete(self, server): + """ + Delete (i.e. shut down and delete the image) this server. + """ + self._delete("/servers/%s" % base.getid(server)) + + def reboot(self, server, reboot_type=REBOOT_SOFT): + """ + Reboot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param reboot_type: either :data:`REBOOT_SOFT` for a software-level + reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + """ + self._action('reboot', server, {'type': reboot_type}) + + def rebuild(self, server, image, password=None, **kwargs): + """ + 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. + """ + body = {'image_ref': base.getid(image)} + if password is not None: + body['admin_password'] = password + + _resp, body = self._action('rebuild', server, body, **kwargs) + return Server(self, body['server']) + + def migrate(self, server): + """ + Migrate a server to a new host. + + :param server: The :class:`Server` (or its ID). + """ + self._action('migrate', server) + + def resize(self, server, flavor, **kwargs): + """ + Resize a server's resources. + + :param server: The :class:`Server` (or its ID) to share onto. + :param flavor: the :class:`Flavor` (or its ID) to resize to. + + Until a resize event is confirmed with :meth:`confirm_resize`, the old + server will be kept around and you'll be able to roll back to the old + flavor quickly with :meth:`revert_resize`. All resizes are + automatically confirmed after 24 hours. + """ + info = {'flavor_ref': base.getid(flavor)} + + self._action('resize', server, info=info, **kwargs) + + def confirm_resize(self, server): + """ + Confirm that the resize worked, thus removing the original server. + + :param server: The :class:`Server` (or its ID) to share onto. + """ + self._action('confirm_resize', server) + + def revert_resize(self, server): + """ + Revert a previous resize, switching back to the old server. + + :param server: The :class:`Server` (or its ID) to share onto. + """ + self._action('revert_resize', server) + + def create_image(self, server, image_name, metadata=None): + """ + Snapshot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param image_name: Name to give the snapshot image + :param meta: Metadata to give newly-created image entity + """ + body = {'name': image_name, 'metadata': metadata or {}} + resp = self._action('create_image', server, body)[0] + location = resp.headers['location'] + image_uuid = location.split('/')[-1] + return image_uuid + + def backup(self, server, backup_name, backup_type, rotation): + """ + Backup a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param backup_name: Name of the backup image + :param backup_type: The backup type, like 'daily' or 'weekly' + :param rotation: Int parameter representing how many backups to + keep around. + """ + body = {'name': backup_name, + 'backup_type': backup_type, + 'rotation': rotation} + self._action('create_backup', server, body) + + def set_meta(self, server, metadata): + """ + Set a servers metadata + :param server: The :class:`Server` to add metadata to + :param metadata: A dict of metadata to add to the server + """ + body = {'metadata': metadata} + return self._create("/servers/%s/metadata" % base.getid(server), + body, "metadata") + + def get_console_output(self, server, length=None): + """ + Get text console log output from Server. + + :param server: The :class:`Server` (or its ID) whose console output + you would like to retrieve. + :param length: The number of tail loglines you would like to retrieve. + """ + return self._action('get_console_output', + server, {'length': length})[1]['output'] + + def delete_meta(self, server, keys): + """ + Delete metadata from an server + :param server: The :class:`Server` to add metadata to + :param keys: A list of metadata keys to delete from the server + """ + for k in keys: + self._delete("/servers/%s/metadata/%s" % (base.getid(server), k)) + + def live_migrate(self, server, host, block_migration, disk_over_commit): + """ + Migrates a running instance to a new machine. + + :param server: instance id which comes from nova list. + :param host: destination host name. + :param block_migration: if True, do block_migration. + :param disk_over_commit: if True, Allow overcommit. + + """ + self._action('migrate_live', server, + {'host': host, + 'block_migration': block_migration, + 'disk_over_commit': disk_over_commit}) + + def reset_state(self, server, state='error'): + """ + Reset the state of an instance to active or error. + + :param server: ID of the instance to reset the state of. + :param state: Desired state; either 'active' or 'error'. + Defaults to 'error'. + """ + self._action('reset_state', server, dict(state=state)) + + def reset_network(self, server): + """ + Reset network of an instance. + """ + self._action('reset_network', server) + + def evacuate(self, server, host, on_shared_storage, password=None): + """ + Evacuate a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param host: Name of the target host. + :param on_shared_storage: Specifies whether instance files located + on shared storage + :param password: string to set as password on the evacuated server. + """ + body = { + 'host': host, + 'on_shared_storage': on_shared_storage, + } + + if password is not None: + body['admin_password'] = password + + return self._action('evacuate', server, body) + + def interface_list(self, server): + """ + List attached network interfaces + + :param server: The :class:`Server` (or its ID) to query. + """ + return self._list('/servers/%s/os-attach-interfaces' + % base.getid(server), 'interface_attachments') + + def interface_attach(self, server, port_id, net_id, fixed_ip): + """ + Attach a network_interface to an instance. + + :param server: The :class:`Server` (or its ID) to attach to. + :param port_id: The port to attach. + """ + + body = {'interface_attachment': {}} + if port_id: + body['interface_attachment']['port_id'] = port_id + if net_id: + body['interface_attachment']['net_id'] = net_id + if fixed_ip: + body['interface_attachment']['fixed_ips'] = [ + {'ip_address': fixed_ip}] + + return self._create('/servers/%s/os-attach-interfaces' + % base.getid(server), + body, 'interface_attachment') + + def interface_detach(self, server, port_id): + """ + Detach a network_interface from an instance. + + :param server: The :class:`Server` (or its ID) to detach from. + :param port_id: The port to detach. + """ + self._delete('/servers/%s/os-attach-interfaces/%s' + % (base.getid(server), port_id)) + + def _action(self, action, server, info=None, **kwargs): + """ + Perform a server "action" -- reboot/rebuild/resize/etc. + """ + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/servers/%s/action' % base.getid(server) + return self.api.client.post(url, body=body) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 9d36099b8..52aa45936 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -34,7 +34,7 @@ from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v1_1 import availability_zones from novaclient.v1_1 import quotas -from novaclient.v1_1 import servers +from novaclient.v3 import servers def _key_value_pairing(text): @@ -307,7 +307,7 @@ def do_boot(cs, args): server = cs.servers.create(*boot_args, **boot_kwargs) - # Keep any information (like adminPass) returned by create + # Keep any information (like admin_password) returned by create info = server._info server = cs.servers.get(info['id']) info.update(server._info) @@ -998,11 +998,11 @@ def do_list(cs, args): servers = cs.servers.list(detailed=detailed, search_opts=search_opts) - convert = [('OS-EXT-SRV-ATTR:host', 'host'), - ('OS-EXT-STS:task_state', 'task_state'), - ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), - ('OS-EXT-STS:power_state', 'power_state'), - ('hostId', 'host_id')] + convert = [('os-extended-server-attributes:hypervisor_hostname', 'host'), + ('os-extended-status:task_state', 'task_state'), + ('os-extended-server-attributes:instance_name', + 'instance_name'), + ('os-extended-status:power_state', 'power_state')] _translate_keys(servers, convert) _translate_extended_states(servers) if args.minimal: