Merge "port test_images and test_server_actions into v3 part1"
diff --git a/tempest/api/compute/v3/images/test_images.py b/tempest/api/compute/v3/images/test_images.py
new file mode 100644
index 0000000..383ea1d
--- /dev/null
+++ b/tempest/api/compute/v3/images/test_images.py
@@ -0,0 +1,172 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+#
+# 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.
+
+from tempest.api import compute
+from tempest.api.compute import base
+from tempest import clients
+from tempest.common.utils.data_utils import parse_image_id
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.test import attr
+
+
+class ImagesTestJSON(base.BaseV2ComputeTest):
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(ImagesTestJSON, cls).setUpClass()
+ if not cls.config.service_available.glance:
+ skip_msg = ("%s skipped as glance is not available" % cls.__name__)
+ raise cls.skipException(skip_msg)
+ cls.client = cls.images_client
+ cls.servers_client = cls.servers_client
+
+ cls.image_ids = []
+
+ if compute.MULTI_USER:
+ if cls.config.compute.allow_tenant_isolation:
+ creds = cls.isolated_creds.get_alt_creds()
+ username, tenant_name, password = creds
+ cls.alt_manager = clients.Manager(username=username,
+ password=password,
+ tenant_name=tenant_name)
+ else:
+ # Use the alt_XXX credentials in the config file
+ cls.alt_manager = clients.AltManager()
+ cls.alt_client = cls.alt_manager.images_client
+
+ def tearDown(self):
+ """Terminate test instances created after a test is executed."""
+ for image_id in self.image_ids:
+ self.client.delete_image(image_id)
+ self.image_ids.remove(image_id)
+ super(ImagesTestJSON, self).tearDown()
+
+ def __create_image__(self, server_id, name, meta=None):
+ resp, body = self.client.create_image(server_id, name, meta)
+ image_id = parse_image_id(resp['location'])
+ self.client.wait_for_image_status(image_id, 'ACTIVE')
+ self.image_ids.append(image_id)
+ return resp, body
+
+ @attr(type=['negative', 'gate'])
+ def test_create_image_from_deleted_server(self):
+ # An image should not be created if the server instance is removed
+ resp, server = self.create_server(wait_until='ACTIVE')
+
+ # Delete server before trying to create server
+ self.servers_client.delete_server(server['id'])
+ self.servers_client.wait_for_server_termination(server['id'])
+ # Create a new image after server is deleted
+ name = rand_name('image')
+ meta = {'image_type': 'test'}
+ self.assertRaises(exceptions.NotFound,
+ self.__create_image__,
+ server['id'], name, meta)
+
+ @attr(type=['negative', 'gate'])
+ def test_create_image_from_invalid_server(self):
+ # An image should not be created with invalid server id
+ # Create a new image with invalid server id
+ name = rand_name('image')
+ meta = {'image_type': 'test'}
+ resp = {}
+ resp['status'] = None
+ self.assertRaises(exceptions.NotFound, self.__create_image__,
+ '!@#$%^&*()', name, meta)
+
+ @attr(type=['negative', 'gate'])
+ def test_create_image_from_stopped_server(self):
+ resp, server = self.create_server(wait_until='ACTIVE')
+ self.servers_client.stop(server['id'])
+ self.servers_client.wait_for_server_status(server['id'],
+ 'SHUTOFF')
+ self.addCleanup(self.servers_client.delete_server, server['id'])
+ snapshot_name = rand_name('test-snap-')
+ resp, image = self.create_image_from_server(server['id'],
+ name=snapshot_name,
+ wait_until='ACTIVE')
+ self.addCleanup(self.client.delete_image, image['id'])
+ self.assertEqual(snapshot_name, image['name'])
+
+ @attr(type='gate')
+ def test_delete_saving_image(self):
+ snapshot_name = rand_name('test-snap-')
+ resp, server = self.create_server(wait_until='ACTIVE')
+ self.addCleanup(self.servers_client.delete_server, server['id'])
+ resp, image = self.create_image_from_server(server['id'],
+ name=snapshot_name,
+ wait_until='SAVING')
+ resp, body = self.client.delete_image(image['id'])
+ self.assertEqual('204', resp['status'])
+
+ @attr(type=['negative', 'gate'])
+ def test_create_image_specify_uuid_35_characters_or_less(self):
+ # Return an error if Image ID passed is 35 characters or less
+ snapshot_name = rand_name('test-snap-')
+ test_uuid = ('a' * 35)
+ self.assertRaises(exceptions.NotFound, self.client.create_image,
+ test_uuid, snapshot_name)
+
+ @attr(type=['negative', 'gate'])
+ def test_create_image_specify_uuid_37_characters_or_more(self):
+ # Return an error if Image ID passed is 37 characters or more
+ snapshot_name = rand_name('test-snap-')
+ test_uuid = ('a' * 37)
+ self.assertRaises(exceptions.NotFound, self.client.create_image,
+ test_uuid, snapshot_name)
+
+ @attr(type=['negative', 'gate'])
+ def test_delete_image_with_invalid_image_id(self):
+ # An image should not be deleted with invalid image id
+ self.assertRaises(exceptions.NotFound, self.client.delete_image,
+ '!@$%^&*()')
+
+ @attr(type=['negative', 'gate'])
+ def test_delete_non_existent_image(self):
+ # Return an error while trying to delete a non-existent image
+
+ non_existent_image_id = '11a22b9-12a9-5555-cc11-00ab112223fa'
+ self.assertRaises(exceptions.NotFound, self.client.delete_image,
+ non_existent_image_id)
+
+ @attr(type=['negative', 'gate'])
+ def test_delete_image_blank_id(self):
+ # Return an error while trying to delete an image with blank Id
+ self.assertRaises(exceptions.NotFound, self.client.delete_image, '')
+
+ @attr(type=['negative', 'gate'])
+ def test_delete_image_non_hex_string_id(self):
+ # Return an error while trying to delete an image with non hex id
+ image_id = '11a22b9-120q-5555-cc11-00ab112223gj'
+ self.assertRaises(exceptions.NotFound, self.client.delete_image,
+ image_id)
+
+ @attr(type=['negative', 'gate'])
+ def test_delete_image_negative_image_id(self):
+ # Return an error while trying to delete an image with negative id
+ self.assertRaises(exceptions.NotFound, self.client.delete_image, -1)
+
+ @attr(type=['negative', 'gate'])
+ def test_delete_image_id_is_over_35_character_limit(self):
+ # Return an error while trying to delete image with id over limit
+ self.assertRaises(exceptions.NotFound, self.client.delete_image,
+ '11a22b9-12a9-5555-cc11-00ab112223fa-3fac')
+
+
+class ImagesTestXML(ImagesTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/compute/v3/servers/test_server_actions.py b/tempest/api/compute/v3/servers/test_server_actions.py
new file mode 100644
index 0000000..961737a
--- /dev/null
+++ b/tempest/api/compute/v3/servers/test_server_actions.py
@@ -0,0 +1,278 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 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.
+
+import base64
+import time
+
+import testtools
+
+from tempest.api import compute
+from tempest.api.compute import base
+from tempest.common.utils.data_utils import rand_name
+from tempest.common.utils.linux.remote_client import RemoteClient
+import tempest.config
+from tempest import exceptions
+from tempest.test import attr
+from tempest.test import skip_because
+
+
+class ServerActionsTestJSON(base.BaseV2ComputeTest):
+ _interface = 'json'
+ resize_available = tempest.config.TempestConfig().\
+ compute_feature_enabled.resize
+ run_ssh = tempest.config.TempestConfig().compute.run_ssh
+
+ def setUp(self):
+ # NOTE(afazekas): Normally we use the same server with all test cases,
+ # but if it has an issue, we build a new one
+ super(ServerActionsTestJSON, self).setUp()
+ # Check if the server is in a clean state after test
+ try:
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+ except Exception:
+ # Rebuild server if something happened to it during a test
+ self.rebuild_server()
+
+ @classmethod
+ def setUpClass(cls):
+ super(ServerActionsTestJSON, cls).setUpClass()
+ cls.client = cls.servers_client
+ cls.rebuild_server()
+
+ @testtools.skipUnless(compute.CHANGE_PASSWORD_AVAILABLE,
+ 'Change password not available.')
+ @attr(type='gate')
+ def test_change_server_password(self):
+ # The server's password should be set to the provided password
+ new_password = 'Newpass1234'
+ resp, body = self.client.change_password(self.server_id, new_password)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ if self.run_ssh:
+ # Verify that the user can authenticate with the new password
+ resp, server = self.client.get_server(self.server_id)
+ linux_client = RemoteClient(server, self.ssh_user, new_password)
+ self.assertTrue(linux_client.can_authenticate())
+
+ @attr(type='smoke')
+ def test_reboot_server_hard(self):
+ # The server should be power cycled
+ if self.run_ssh:
+ # Get the time the server was last rebooted,
+ resp, server = self.client.get_server(self.server_id)
+ linux_client = RemoteClient(server, self.ssh_user, self.password)
+ boot_time = linux_client.get_boot_time()
+
+ resp, body = self.client.reboot(self.server_id, 'HARD')
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ if self.run_ssh:
+ # Log in and verify the boot time has changed
+ linux_client = RemoteClient(server, self.ssh_user, self.password)
+ new_boot_time = linux_client.get_boot_time()
+ self.assertGreater(new_boot_time, boot_time)
+
+ @skip_because(bug="1014647")
+ @attr(type='smoke')
+ def test_reboot_server_soft(self):
+ # The server should be signaled to reboot gracefully
+ if self.run_ssh:
+ # Get the time the server was last rebooted,
+ resp, server = self.client.get_server(self.server_id)
+ linux_client = RemoteClient(server, self.ssh_user, self.password)
+ boot_time = linux_client.get_boot_time()
+
+ resp, body = self.client.reboot(self.server_id, 'SOFT')
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ if self.run_ssh:
+ # Log in and verify the boot time has changed
+ linux_client = RemoteClient(server, self.ssh_user, self.password)
+ new_boot_time = linux_client.get_boot_time()
+ self.assertGreater(new_boot_time, boot_time)
+
+ @attr(type='smoke')
+ def test_rebuild_server(self):
+ # The server should be rebuilt using the provided image and data
+ meta = {'rebuild': 'server'}
+ new_name = rand_name('server')
+ file_contents = 'Test server rebuild.'
+ personality = [{'path': 'rebuild.txt',
+ 'contents': base64.b64encode(file_contents)}]
+ password = 'rebuildPassw0rd'
+ resp, rebuilt_server = self.client.rebuild(self.server_id,
+ self.image_ref_alt,
+ name=new_name,
+ metadata=meta,
+ personality=personality,
+ adminPass=password)
+
+ # Verify the properties in the initial response are correct
+ self.assertEqual(self.server_id, rebuilt_server['id'])
+ rebuilt_image_id = rebuilt_server['image']['id']
+ self.assertTrue(self.image_ref_alt.endswith(rebuilt_image_id))
+ self.assertEqual(self.flavor_ref, int(rebuilt_server['flavor']['id']))
+
+ # Verify the server properties after the rebuild completes
+ self.client.wait_for_server_status(rebuilt_server['id'], 'ACTIVE')
+ resp, server = self.client.get_server(rebuilt_server['id'])
+ rebuilt_image_id = rebuilt_server['image']['id']
+ self.assertTrue(self.image_ref_alt.endswith(rebuilt_image_id))
+ self.assertEqual(new_name, rebuilt_server['name'])
+
+ if self.run_ssh:
+ # Verify that the user can authenticate with the provided password
+ linux_client = RemoteClient(server, self.ssh_user, password)
+ self.assertTrue(linux_client.can_authenticate())
+
+ def _detect_server_image_flavor(self, server_id):
+ # Detects the current server image flavor ref.
+ resp, server = self.client.get_server(self.server_id)
+ current_flavor = server['flavor']['id']
+ new_flavor_ref = self.flavor_ref_alt \
+ if int(current_flavor) == self.flavor_ref else self.flavor_ref
+ return int(current_flavor), int(new_flavor_ref)
+
+ @testtools.skipIf(not resize_available, 'Resize not available.')
+ @attr(type='smoke')
+ def test_resize_server_confirm(self):
+ # The server's RAM and disk space should be modified to that of
+ # the provided flavor
+
+ previous_flavor_ref, new_flavor_ref = \
+ self._detect_server_image_flavor(self.server_id)
+
+ resp, server = self.client.resize(self.server_id, new_flavor_ref)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'VERIFY_RESIZE')
+
+ self.client.confirm_resize(self.server_id)
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ resp, server = self.client.get_server(self.server_id)
+ self.assertEqual(new_flavor_ref, int(server['flavor']['id']))
+
+ @testtools.skipIf(not resize_available, 'Resize not available.')
+ @attr(type='gate')
+ def test_resize_server_revert(self):
+ # The server's RAM and disk space should return to its original
+ # values after a resize is reverted
+
+ previous_flavor_ref, new_flavor_ref = \
+ self._detect_server_image_flavor(self.server_id)
+
+ resp, server = self.client.resize(self.server_id, new_flavor_ref)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'VERIFY_RESIZE')
+
+ self.client.revert_resize(self.server_id)
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ # Need to poll for the id change until lp#924371 is fixed
+ resp, server = self.client.get_server(self.server_id)
+ start = int(time.time())
+
+ while int(server['flavor']['id']) != previous_flavor_ref:
+ time.sleep(self.build_interval)
+ resp, server = self.client.get_server(self.server_id)
+
+ if int(time.time()) - start >= self.build_timeout:
+ message = 'Server %s failed to revert resize within the \
+ required time (%s s).' % (self.server_id, self.build_timeout)
+ raise exceptions.TimeoutException(message)
+
+ @attr(type='gate')
+ def test_get_console_output(self):
+ # Positive test:Should be able to GET the console output
+ # for a given server_id and number of lines
+ def get_output():
+ resp, output = self.servers_client.get_console_output(
+ self.server_id, 10)
+ self.assertEqual(200, resp.status)
+ self.assertTrue(output, "Console output was empty.")
+ lines = len(output.split('\n'))
+ self.assertEqual(lines, 10)
+ self.wait_for(get_output)
+
+ @skip_because(bug="1014683")
+ @attr(type='gate')
+ def test_get_console_output_server_id_in_reboot_status(self):
+ # Positive test:Should be able to GET the console output
+ # for a given server_id in reboot status
+ resp, output = self.servers_client.reboot(self.server_id, 'SOFT')
+ self.servers_client.wait_for_server_status(self.server_id,
+ 'REBOOT')
+ resp, output = self.servers_client.get_console_output(self.server_id,
+ 10)
+ self.assertEqual(200, resp.status)
+ self.assertIsNotNone(output)
+ lines = len(output.split('\n'))
+ self.assertEqual(lines, 10)
+
+ @attr(type='gate')
+ def test_pause_unpause_server(self):
+ resp, server = self.client.pause_server(self.server_id)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'PAUSED')
+ resp, server = self.client.unpause_server(self.server_id)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ @attr(type='gate')
+ def test_suspend_resume_server(self):
+ resp, server = self.client.suspend_server(self.server_id)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'SUSPENDED')
+ resp, server = self.client.resume_server(self.server_id)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ @attr(type='gate')
+ def test_stop_start_server(self):
+ resp, server = self.servers_client.stop(self.server_id)
+ self.assertEqual(202, resp.status)
+ self.servers_client.wait_for_server_status(self.server_id, 'SHUTOFF')
+ resp, server = self.servers_client.start(self.server_id)
+ self.assertEqual(202, resp.status)
+ self.servers_client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ @attr(type='gate')
+ def test_lock_unlock_server(self):
+ # Lock the server,try server stop(exceptions throw),unlock it and retry
+ resp, server = self.servers_client.lock_server(self.server_id)
+ self.assertEqual(202, resp.status)
+ resp, server = self.servers_client.get_server(self.server_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(server['status'], 'ACTIVE')
+ # Locked server is not allowed to be stopped by non-admin user
+ self.assertRaises(exceptions.Conflict,
+ self.servers_client.stop, self.server_id)
+ resp, server = self.servers_client.unlock_server(self.server_id)
+ self.assertEqual(202, resp.status)
+ resp, server = self.servers_client.stop(self.server_id)
+ self.assertEqual(202, resp.status)
+ self.servers_client.wait_for_server_status(self.server_id, 'SHUTOFF')
+ resp, server = self.servers_client.start(self.server_id)
+ self.assertEqual(202, resp.status)
+ self.servers_client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+
+class ServerActionsTestXML(ServerActionsTestJSON):
+ _interface = 'xml'
diff --git a/tempest/services/compute/v3/json/servers_client.py b/tempest/services/compute/v3/json/servers_client.py
new file mode 100644
index 0000000..07bb6ce
--- /dev/null
+++ b/tempest/services/compute/v3/json/servers_client.py
@@ -0,0 +1,392 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+# 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.
+
+import json
+import time
+import urllib
+
+from tempest.common.rest_client import RestClient
+from tempest.common import waiters
+from tempest import exceptions
+
+
+class ServersClientJSON(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None,
+ auth_version='v2'):
+ super(ServersClientJSON, self).__init__(config, username, password,
+ auth_url, tenant_name,
+ auth_version=auth_version)
+ self.service = self.config.compute.catalog_type
+
+ def create_server(self, name, image_ref, flavor_ref, **kwargs):
+ """
+ Creates an instance of a server.
+ name (Required): The name of the server.
+ image_ref (Required): Reference to the image used to build the server.
+ flavor_ref (Required): The flavor used to build the server.
+ Following optional keyword arguments are accepted:
+ adminPass: Sets the initial root password.
+ key_name: Key name of keypair that was created earlier.
+ meta: A dictionary of values to be used as metadata.
+ personality: A list of dictionaries for files to be injected into
+ the server.
+ security_groups: A list of security group dicts.
+ networks: A list of network dicts with UUID and fixed_ip.
+ user_data: User data for instance.
+ availability_zone: Availability zone in which to launch instance.
+ accessIPv4: The IPv4 access address for the server.
+ accessIPv6: The IPv6 access address for the server.
+ min_count: Count of minimum number of instances to launch.
+ max_count: Count of maximum number of instances to launch.
+ disk_config: Determines if user or admin controls disk configuration.
+ return_reservation_id: Enable/Disable the return of reservation id
+ """
+ post_body = {
+ 'name': name,
+ 'imageRef': image_ref,
+ 'flavorRef': flavor_ref
+ }
+
+ for option in ['personality', 'adminPass', 'key_name',
+ 'security_groups', 'networks', 'user_data',
+ 'availability_zone', 'accessIPv4', 'accessIPv6',
+ 'min_count', 'max_count', ('metadata', 'meta'),
+ ('OS-DCF:diskConfig', 'disk_config'),
+ 'return_reservation_id']:
+ if isinstance(option, tuple):
+ post_param = option[0]
+ key = option[1]
+ else:
+ post_param = option
+ key = option
+ value = kwargs.get(key)
+ if value is not None:
+ post_body[post_param] = value
+ post_body = json.dumps({'server': post_body})
+ resp, body = self.post('servers', post_body, self.headers)
+
+ body = json.loads(body)
+ # NOTE(maurosr): this deals with the case of multiple server create
+ # with return reservation id set True
+ if 'reservation_id' in body:
+ return resp, body
+ return resp, body['server']
+
+ def update_server(self, server_id, name=None, meta=None, accessIPv4=None,
+ accessIPv6=None, disk_config=None):
+ """
+ Updates the properties of an existing server.
+ server_id: The id of an existing server.
+ name: The name of the server.
+ personality: A list of files to be injected into the server.
+ accessIPv4: The IPv4 access address for the server.
+ accessIPv6: The IPv6 access address for the server.
+ """
+
+ post_body = {}
+
+ if meta is not None:
+ post_body['metadata'] = meta
+
+ if name is not None:
+ post_body['name'] = name
+
+ if accessIPv4 is not None:
+ post_body['accessIPv4'] = accessIPv4
+
+ if accessIPv6 is not None:
+ post_body['accessIPv6'] = accessIPv6
+
+ if disk_config is not None:
+ post_body['OS-DCF:diskConfig'] = disk_config
+
+ post_body = json.dumps({'server': post_body})
+ resp, body = self.put("servers/%s" % str(server_id),
+ post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['server']
+
+ def get_server(self, server_id):
+ """Returns the details of an existing server."""
+ resp, body = self.get("servers/%s" % str(server_id))
+ body = json.loads(body)
+ return resp, body['server']
+
+ def delete_server(self, server_id):
+ """Deletes the given server."""
+ return self.delete("servers/%s" % str(server_id))
+
+ def list_servers(self, params=None):
+ """Lists all servers for a user."""
+
+ url = 'servers'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body
+
+ def list_servers_with_detail(self, params=None):
+ """Lists all servers in detail for a user."""
+
+ url = 'servers/detail'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body
+
+ def wait_for_server_status(self, server_id, status):
+ """Waits for a server to reach a given status."""
+ return waiters.wait_for_server_status(self, server_id, status)
+
+ def wait_for_server_termination(self, server_id, ignore_error=False):
+ """Waits for server to reach termination."""
+ start_time = int(time.time())
+ while True:
+ try:
+ resp, body = self.get_server(server_id)
+ except exceptions.NotFound:
+ return
+
+ server_status = body['status']
+ if server_status == 'ERROR' and not ignore_error:
+ raise exceptions.BuildErrorException(server_id=server_id)
+
+ if int(time.time()) - start_time >= self.build_timeout:
+ raise exceptions.TimeoutException
+
+ time.sleep(self.build_interval)
+
+ def list_addresses(self, server_id):
+ """Lists all addresses for a server."""
+ resp, body = self.get("servers/%s/ips" % str(server_id))
+ body = json.loads(body)
+ return resp, body['addresses']
+
+ def list_addresses_by_network(self, server_id, network_id):
+ """Lists all addresses of a specific network type for a server."""
+ resp, body = self.get("servers/%s/ips/%s" %
+ (str(server_id), network_id))
+ body = json.loads(body)
+ return resp, body
+
+ def action(self, server_id, action_name, response_key, **kwargs):
+ post_body = json.dumps({action_name: kwargs})
+ resp, body = self.post('servers/%s/action' % str(server_id),
+ post_body, self.headers)
+ if response_key is not None:
+ body = json.loads(body)[response_key]
+ return resp, body
+
+ def change_password(self, server_id, adminPass):
+ """Changes the root password for the server."""
+ return self.action(server_id, 'changePassword', None,
+ adminPass=adminPass)
+
+ def reboot(self, server_id, reboot_type):
+ """Reboots a server."""
+ return self.action(server_id, 'reboot', None, type=reboot_type)
+
+ def rebuild(self, server_id, image_ref, **kwargs):
+ """Rebuilds a server with a new image."""
+ kwargs['imageRef'] = image_ref
+ if 'disk_config' in kwargs:
+ kwargs['OS-DCF:diskConfig'] = kwargs['disk_config']
+ del kwargs['disk_config']
+ return self.action(server_id, 'rebuild', 'server', **kwargs)
+
+ def resize(self, server_id, flavor_ref, **kwargs):
+ """Changes the flavor of a server."""
+ kwargs['flavorRef'] = flavor_ref
+ if 'disk_config' in kwargs:
+ kwargs['OS-DCF:diskConfig'] = kwargs['disk_config']
+ del kwargs['disk_config']
+ return self.action(server_id, 'resize', None, **kwargs)
+
+ def confirm_resize(self, server_id, **kwargs):
+ """Confirms the flavor change for a server."""
+ return self.action(server_id, 'confirmResize', None, **kwargs)
+
+ def revert_resize(self, server_id, **kwargs):
+ """Reverts a server back to its original flavor."""
+ return self.action(server_id, 'revertResize', None, **kwargs)
+
+ def create_image(self, server_id, name):
+ """Creates an image of the given server."""
+ return self.action(server_id, 'createImage', None, name=name)
+
+ def list_server_metadata(self, server_id):
+ resp, body = self.get("servers/%s/metadata" % str(server_id))
+ body = json.loads(body)
+ return resp, body['metadata']
+
+ def set_server_metadata(self, server_id, meta, no_metadata_field=False):
+ if no_metadata_field:
+ post_body = ""
+ else:
+ post_body = json.dumps({'metadata': meta})
+ resp, body = self.put('servers/%s/metadata' % str(server_id),
+ post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['metadata']
+
+ def update_server_metadata(self, server_id, meta):
+ post_body = json.dumps({'metadata': meta})
+ resp, body = self.post('servers/%s/metadata' % str(server_id),
+ post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['metadata']
+
+ def get_server_metadata_item(self, server_id, key):
+ resp, body = self.get("servers/%s/metadata/%s" % (str(server_id), key))
+ body = json.loads(body)
+ return resp, body['meta']
+
+ def set_server_metadata_item(self, server_id, key, meta):
+ post_body = json.dumps({'meta': meta})
+ resp, body = self.put('servers/%s/metadata/%s' % (str(server_id), key),
+ post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['meta']
+
+ def delete_server_metadata_item(self, server_id, key):
+ resp, body = self.delete("servers/%s/metadata/%s" %
+ (str(server_id), key))
+ return resp, body
+
+ def stop(self, server_id, **kwargs):
+ return self.action(server_id, 'os-stop', None, **kwargs)
+
+ def start(self, server_id, **kwargs):
+ return self.action(server_id, 'os-start', None, **kwargs)
+
+ def attach_volume(self, server_id, volume_id, device='/dev/vdz'):
+ """Attaches a volume to a server instance."""
+ post_body = json.dumps({
+ 'volumeAttachment': {
+ 'volumeId': volume_id,
+ 'device': device,
+ }
+ })
+ resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
+ post_body, self.headers)
+ return resp, body
+
+ def detach_volume(self, server_id, volume_id):
+ """Detaches a volume from a server instance."""
+ resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
+ (server_id, volume_id))
+ return resp, body
+
+ def add_security_group(self, server_id, name):
+ """Adds a security group to the server."""
+ return self.action(server_id, 'addSecurityGroup', None, name=name)
+
+ def remove_security_group(self, server_id, name):
+ """Removes a security group from the server."""
+ return self.action(server_id, 'removeSecurityGroup', None, name=name)
+
+ def live_migrate_server(self, server_id, dest_host, use_block_migration):
+ """This should be called with administrator privileges ."""
+
+ migrate_params = {
+ "disk_over_commit": False,
+ "block_migration": use_block_migration,
+ "host": dest_host
+ }
+
+ req_body = json.dumps({'os-migrateLive': migrate_params})
+
+ resp, body = self.post("servers/%s/action" % str(server_id),
+ req_body, self.headers)
+ return resp, body
+
+ def migrate_server(self, server_id, **kwargs):
+ """Migrates a server to a new host."""
+ return self.action(server_id, 'migrate', None, **kwargs)
+
+ def lock_server(self, server_id, **kwargs):
+ """Locks the given server."""
+ return self.action(server_id, 'lock', None, **kwargs)
+
+ def unlock_server(self, server_id, **kwargs):
+ """UNlocks the given server."""
+ return self.action(server_id, 'unlock', None, **kwargs)
+
+ def suspend_server(self, server_id, **kwargs):
+ """Suspends the provded server."""
+ return self.action(server_id, 'suspend', None, **kwargs)
+
+ def resume_server(self, server_id, **kwargs):
+ """Un-suspends the provded server."""
+ return self.action(server_id, 'resume', None, **kwargs)
+
+ def pause_server(self, server_id, **kwargs):
+ """Pauses the provded server."""
+ return self.action(server_id, 'pause', None, **kwargs)
+
+ def unpause_server(self, server_id, **kwargs):
+ """Un-pauses the provded server."""
+ return self.action(server_id, 'unpause', None, **kwargs)
+
+ def reset_state(self, server_id, state='error'):
+ """Resets the state of a server to active/error."""
+ return self.action(server_id, 'os-resetState', None, state=state)
+
+ def get_console_output(self, server_id, length):
+ return self.action(server_id, 'os-getConsoleOutput', 'output',
+ length=length)
+
+ def list_virtual_interfaces(self, server_id):
+ """
+ List the virtual interfaces used in an instance.
+ """
+ resp, body = self.get('/'.join(['servers', server_id,
+ 'os-virtual-interfaces']))
+ return resp, json.loads(body)
+
+ def rescue_server(self, server_id, adminPass=None):
+ """Rescue the provided server."""
+ return self.action(server_id, 'rescue', None, adminPass=adminPass)
+
+ def unrescue_server(self, server_id):
+ """Unrescue the provided server."""
+ return self.action(server_id, 'unrescue', None)
+
+ def get_server_diagnostics(self, server_id):
+ """Get the usage data for a server."""
+ resp, body = self.get("servers/%s/diagnostics" % str(server_id))
+ return resp, json.loads(body)
+
+ def list_instance_actions(self, server_id):
+ """List the provided server action."""
+ resp, body = self.get("servers/%s/os-instance-actions" %
+ str(server_id))
+ body = json.loads(body)
+ return resp, body['instanceActions']
+
+ def get_instance_action(self, server_id, request_id):
+ """Returns the action details of the provided server."""
+ resp, body = self.get("servers/%s/os-instance-actions/%s" %
+ (str(server_id), str(request_id)))
+ body = json.loads(body)
+ return resp, body['instanceAction']
diff --git a/tempest/services/compute/v3/xml/servers_client.py b/tempest/services/compute/v3/xml/servers_client.py
new file mode 100644
index 0000000..43de4ef
--- /dev/null
+++ b/tempest/services/compute/v3/xml/servers_client.py
@@ -0,0 +1,602 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM Corp.
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+# 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.
+
+import time
+import urllib
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest.common import waiters
+from tempest import exceptions
+from tempest.openstack.common import log as logging
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
+from tempest.services.compute.xml.common import xml_to_json
+from tempest.services.compute.xml.common import XMLNS_11
+
+
+LOG = logging.getLogger(__name__)
+
+
+def _translate_ip_xml_json(ip):
+ """
+ Convert the address version to int.
+ """
+ ip = dict(ip)
+ version = ip.get('version')
+ if version:
+ ip['version'] = int(version)
+ # NOTE(maurosr): just a fast way to avoid the xml version with the
+ # expanded xml namespace.
+ type_ns_prefix = ('{http://docs.openstack.org/compute/ext/extended_ips/'
+ 'api/v1.1}type')
+ mac_ns_prefix = ('{http://docs.openstack.org/compute/ext/extended_ips_mac'
+ '/api/v1.1}mac_addr')
+
+ if type_ns_prefix in ip:
+ ip['OS-EXT-IPS:type'] = ip.pop(type_ns_prefix)
+
+ if mac_ns_prefix in ip:
+ ip['OS-EXT-IPS-MAC:mac_addr'] = ip.pop(mac_ns_prefix)
+ return ip
+
+
+def _translate_network_xml_to_json(network):
+ return [_translate_ip_xml_json(ip.attrib)
+ for ip in network.findall('{%s}ip' % XMLNS_11)]
+
+
+def _translate_addresses_xml_to_json(xml_addresses):
+ return dict((network.attrib['id'], _translate_network_xml_to_json(network))
+ for network in xml_addresses.findall('{%s}network' % XMLNS_11))
+
+
+def _translate_server_xml_to_json(xml_dom):
+ """Convert server XML to server JSON.
+
+ The addresses collection does not convert well by the dumb xml_to_json.
+ This method does some pre and post-processing to deal with that.
+
+ Translate XML addresses subtree to JSON.
+
+ Having xml_doc similar to
+ <api:server xmlns:api="http://docs.openstack.org/compute/api/v1.1">
+ <api:addresses>
+ <api:network id="foo_novanetwork">
+ <api:ip version="4" addr="192.168.0.4"/>
+ </api:network>
+ <api:network id="bar_novanetwork">
+ <api:ip version="4" addr="10.1.0.4"/>
+ <api:ip version="6" addr="2001:0:0:1:2:3:4:5"/>
+ </api:network>
+ </api:addresses>
+ </api:server>
+
+ the _translate_server_xml_to_json(etree.fromstring(xml_doc)) should produce
+ something like
+
+ {'addresses': {'bar_novanetwork': [{'addr': '10.1.0.4', 'version': 4},
+ {'addr': '2001:0:0:1:2:3:4:5',
+ 'version': 6}],
+ 'foo_novanetwork': [{'addr': '192.168.0.4', 'version': 4}]}}
+ """
+ nsmap = {'api': XMLNS_11}
+ addresses = xml_dom.xpath('/api:server/api:addresses', namespaces=nsmap)
+ if addresses:
+ if len(addresses) > 1:
+ raise ValueError('Expected only single `addresses` element.')
+ json_addresses = _translate_addresses_xml_to_json(addresses[0])
+ json = xml_to_json(xml_dom)
+ json['addresses'] = json_addresses
+ else:
+ json = xml_to_json(xml_dom)
+ diskConfig = ('{http://docs.openstack.org'
+ '/compute/ext/disk_config/api/v1.1}diskConfig')
+ terminated_at = ('{http://docs.openstack.org/'
+ 'compute/ext/server_usage/api/v1.1}terminated_at')
+ launched_at = ('{http://docs.openstack.org'
+ '/compute/ext/server_usage/api/v1.1}launched_at')
+ power_state = ('{http://docs.openstack.org'
+ '/compute/ext/extended_status/api/v1.1}power_state')
+ availability_zone = ('{http://docs.openstack.org'
+ '/compute/ext/extended_availability_zone/api/v2}'
+ 'availability_zone')
+ vm_state = ('{http://docs.openstack.org'
+ '/compute/ext/extended_status/api/v1.1}vm_state')
+ task_state = ('{http://docs.openstack.org'
+ '/compute/ext/extended_status/api/v1.1}task_state')
+ if diskConfig in json:
+ json['OS-DCF:diskConfig'] = json.pop(diskConfig)
+ if terminated_at in json:
+ json['OS-SRV-USG:terminated_at'] = json.pop(terminated_at)
+ if launched_at in json:
+ json['OS-SRV-USG:launched_at'] = json.pop(launched_at)
+ if power_state in json:
+ json['OS-EXT-STS:power_state'] = json.pop(power_state)
+ if availability_zone in json:
+ json['OS-EXT-AZ:availability_zone'] = json.pop(availability_zone)
+ if vm_state in json:
+ json['OS-EXT-STS:vm_state'] = json.pop(vm_state)
+ if task_state in json:
+ json['OS-EXT-STS:task_state'] = json.pop(task_state)
+ return json
+
+
+class ServersClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None,
+ auth_version='v2'):
+ super(ServersClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name,
+ auth_version=auth_version)
+ self.service = self.config.compute.catalog_type
+
+ def _parse_key_value(self, node):
+ """Parse <foo key='key'>value</foo> data into {'key': 'value'}."""
+ data = {}
+ for node in node.getchildren():
+ data[node.get('key')] = node.text
+ return data
+
+ def _parse_links(self, node, json):
+ del json['link']
+ json['links'] = []
+ for linknode in node.findall('{http://www.w3.org/2005/Atom}link'):
+ json['links'].append(xml_to_json(linknode))
+
+ def _parse_server(self, body):
+ json = _translate_server_xml_to_json(body)
+
+ if 'metadata' in json and json['metadata']:
+ # NOTE(danms): if there was metadata, we need to re-parse
+ # that as a special type
+ metadata_tag = body.find('{%s}metadata' % XMLNS_11)
+ json["metadata"] = self._parse_key_value(metadata_tag)
+ if 'link' in json:
+ self._parse_links(body, json)
+ for sub in ['image', 'flavor']:
+ if sub in json and 'link' in json[sub]:
+ self._parse_links(body, json[sub])
+ return json
+
+ def _parse_xml_virtual_interfaces(self, xml_dom):
+ """
+ Return server's virtual interfaces XML as JSON.
+ """
+ data = {"virtual_interfaces": []}
+ for iface in xml_dom.getchildren():
+ data["virtual_interfaces"].append(
+ {"id": iface.get("id"),
+ "mac_address": iface.get("mac_address")})
+ return data
+
+ def get_server(self, server_id):
+ """Returns the details of an existing server."""
+ resp, body = self.get("servers/%s" % str(server_id), self.headers)
+ server = self._parse_server(etree.fromstring(body))
+ return resp, server
+
+ def lock_server(self, server_id, **kwargs):
+ """Locks the given server."""
+ return self.action(server_id, 'lock', None, **kwargs)
+
+ def unlock_server(self, server_id, **kwargs):
+ """Unlocks the given server."""
+ return self.action(server_id, 'unlock', None, **kwargs)
+
+ def suspend_server(self, server_id, **kwargs):
+ """Suspends the provided server."""
+ return self.action(server_id, 'suspend', None, **kwargs)
+
+ def resume_server(self, server_id, **kwargs):
+ """Un-suspends the provided server."""
+ return self.action(server_id, 'resume', None, **kwargs)
+
+ def pause_server(self, server_id, **kwargs):
+ """Pauses the provided server."""
+ return self.action(server_id, 'pause', None, **kwargs)
+
+ def unpause_server(self, server_id, **kwargs):
+ """Un-pauses the provided server."""
+ return self.action(server_id, 'unpause', None, **kwargs)
+
+ def reset_state(self, server_id, state='error'):
+ """Resets the state of a server to active/error."""
+ return self.action(server_id, 'os-resetState', None, state=state)
+
+ def delete_server(self, server_id):
+ """Deletes the given server."""
+ return self.delete("servers/%s" % str(server_id))
+
+ def _parse_array(self, node):
+ array = []
+ for child in node.getchildren():
+ array.append(xml_to_json(child))
+ return array
+
+ def list_servers(self, params=None):
+ url = 'servers'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ servers = self._parse_array(etree.fromstring(body))
+ return resp, {"servers": servers}
+
+ def list_servers_with_detail(self, params=None):
+ url = 'servers/detail'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ servers = self._parse_array(etree.fromstring(body))
+ return resp, {"servers": servers}
+
+ def update_server(self, server_id, name=None, meta=None, accessIPv4=None,
+ accessIPv6=None, disk_config=None):
+ doc = Document()
+ server = Element("server")
+ doc.append(server)
+
+ if name is not None:
+ server.add_attr("name", name)
+ if accessIPv4 is not None:
+ server.add_attr("accessIPv4", accessIPv4)
+ if accessIPv6 is not None:
+ server.add_attr("accessIPv6", accessIPv6)
+ if disk_config is not None:
+ server.add_attr('xmlns:OS-DCF', "http://docs.openstack.org/"
+ "compute/ext/disk_config/api/v1.1")
+ server.add_attr("OS-DCF:diskConfig", disk_config)
+ if meta is not None:
+ metadata = Element("metadata")
+ server.append(metadata)
+ for k, v in meta:
+ meta = Element("meta", key=k)
+ meta.append(Text(v))
+ metadata.append(meta)
+
+ resp, body = self.put('servers/%s' % str(server_id),
+ str(doc), self.headers)
+ return resp, xml_to_json(etree.fromstring(body))
+
+ def create_server(self, name, image_ref, flavor_ref, **kwargs):
+ """
+ Creates an instance of a server.
+ name (Required): The name of the server.
+ image_ref (Required): Reference to the image used to build the server.
+ flavor_ref (Required): The flavor used to build the server.
+ Following optional keyword arguments are accepted:
+ adminPass: Sets the initial root password.
+ key_name: Key name of keypair that was created earlier.
+ meta: A dictionary of values to be used as metadata.
+ personality: A list of dictionaries for files to be injected into
+ the server.
+ security_groups: A list of security group dicts.
+ networks: A list of network dicts with UUID and fixed_ip.
+ user_data: User data for instance.
+ availability_zone: Availability zone in which to launch instance.
+ accessIPv4: The IPv4 access address for the server.
+ accessIPv6: The IPv6 access address for the server.
+ min_count: Count of minimum number of instances to launch.
+ max_count: Count of maximum number of instances to launch.
+ disk_config: Determines if user or admin controls disk configuration.
+ """
+ server = Element("server",
+ xmlns=XMLNS_11,
+ imageRef=image_ref,
+ flavorRef=flavor_ref,
+ name=name)
+
+ for attr in ["adminPass", "accessIPv4", "accessIPv6", "key_name",
+ "user_data", "availability_zone", "min_count",
+ "max_count", "return_reservation_id"]:
+ if attr in kwargs:
+ server.add_attr(attr, kwargs[attr])
+
+ if 'disk_config' in kwargs:
+ server.add_attr('xmlns:OS-DCF', "http://docs.openstack.org/"
+ "compute/ext/disk_config/api/v1.1")
+ server.add_attr('OS-DCF:diskConfig', kwargs['disk_config'])
+
+ if 'security_groups' in kwargs:
+ secgroups = Element("security_groups")
+ server.append(secgroups)
+ for secgroup in kwargs['security_groups']:
+ s = Element("security_group", name=secgroup['name'])
+ secgroups.append(s)
+
+ if 'networks' in kwargs:
+ networks = Element("networks")
+ server.append(networks)
+ for network in kwargs['networks']:
+ s = Element("network", uuid=network['uuid'],
+ fixed_ip=network['fixed_ip'])
+ networks.append(s)
+
+ if 'meta' in kwargs:
+ metadata = Element("metadata")
+ server.append(metadata)
+ for k, v in kwargs['meta'].items():
+ meta = Element("meta", key=k)
+ meta.append(Text(v))
+ metadata.append(meta)
+
+ if 'personality' in kwargs:
+ personality = Element('personality')
+ server.append(personality)
+ for k in kwargs['personality']:
+ temp = Element('file', path=k['path'])
+ temp.append(Text(k['contents']))
+ personality.append(temp)
+
+ resp, body = self.post('servers', str(Document(server)), self.headers)
+ server = self._parse_server(etree.fromstring(body))
+ return resp, server
+
+ def wait_for_server_status(self, server_id, status):
+ """Waits for a server to reach a given status."""
+ return waiters.wait_for_server_status(self, server_id, status)
+
+ def wait_for_server_termination(self, server_id, ignore_error=False):
+ """Waits for server to reach termination."""
+ start_time = int(time.time())
+ while True:
+ try:
+ resp, body = self.get_server(server_id)
+ except exceptions.NotFound:
+ return
+
+ server_status = body['status']
+ if server_status == 'ERROR' and not ignore_error:
+ raise exceptions.BuildErrorException
+
+ if int(time.time()) - start_time >= self.build_timeout:
+ raise exceptions.TimeoutException
+
+ time.sleep(self.build_interval)
+
+ def _parse_network(self, node):
+ addrs = []
+ for child in node.getchildren():
+ addrs.append({'version': int(child.get('version')),
+ 'addr': child.get('addr')})
+ return {node.get('id'): addrs}
+
+ def list_addresses(self, server_id):
+ """Lists all addresses for a server."""
+ resp, body = self.get("servers/%s/ips" % str(server_id), self.headers)
+
+ networks = {}
+ xml_list = etree.fromstring(body)
+ for child in xml_list.getchildren():
+ network = self._parse_network(child)
+ networks.update(**network)
+
+ return resp, networks
+
+ def list_addresses_by_network(self, server_id, network_id):
+ """Lists all addresses of a specific network type for a server."""
+ resp, body = self.get("servers/%s/ips/%s" % (str(server_id),
+ network_id),
+ self.headers)
+ network = self._parse_network(etree.fromstring(body))
+
+ return resp, network
+
+ def action(self, server_id, action_name, response_key, **kwargs):
+ if 'xmlns' not in kwargs:
+ kwargs['xmlns'] = XMLNS_11
+ doc = Document((Element(action_name, **kwargs)))
+ resp, body = self.post("servers/%s/action" % server_id,
+ str(doc), self.headers)
+ if response_key is not None:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def change_password(self, server_id, password):
+ return self.action(server_id, "changePassword", None,
+ adminPass=password)
+
+ def reboot(self, server_id, reboot_type):
+ return self.action(server_id, "reboot", None, type=reboot_type)
+
+ def rebuild(self, server_id, image_ref, **kwargs):
+ kwargs['imageRef'] = image_ref
+ if 'disk_config' in kwargs:
+ kwargs['OS-DCF:diskConfig'] = kwargs['disk_config']
+ del kwargs['disk_config']
+ kwargs['xmlns:OS-DCF'] = "http://docs.openstack.org/"\
+ "compute/ext/disk_config/api/v1.1"
+ kwargs['xmlns:atom'] = "http://www.w3.org/2005/Atom"
+ if 'xmlns' not in kwargs:
+ kwargs['xmlns'] = XMLNS_11
+
+ attrs = kwargs.copy()
+ if 'metadata' in attrs:
+ del attrs['metadata']
+ rebuild = Element("rebuild",
+ **attrs)
+
+ if 'metadata' in kwargs:
+ metadata = Element("metadata")
+ rebuild.append(metadata)
+ for k, v in kwargs['metadata'].items():
+ meta = Element("meta", key=k)
+ meta.append(Text(v))
+ metadata.append(meta)
+
+ resp, body = self.post('servers/%s/action' % server_id,
+ str(Document(rebuild)), self.headers)
+ server = self._parse_server(etree.fromstring(body))
+ return resp, server
+
+ def resize(self, server_id, flavor_ref, **kwargs):
+ if 'disk_config' in kwargs:
+ kwargs['OS-DCF:diskConfig'] = kwargs['disk_config']
+ del kwargs['disk_config']
+ kwargs['xmlns:OS-DCF'] = "http://docs.openstack.org/"\
+ "compute/ext/disk_config/api/v1.1"
+ kwargs['xmlns:atom'] = "http://www.w3.org/2005/Atom"
+ kwargs['flavorRef'] = flavor_ref
+ return self.action(server_id, 'resize', None, **kwargs)
+
+ def confirm_resize(self, server_id, **kwargs):
+ return self.action(server_id, 'confirmResize', None, **kwargs)
+
+ def revert_resize(self, server_id, **kwargs):
+ return self.action(server_id, 'revertResize', None, **kwargs)
+
+ def stop(self, server_id, **kwargs):
+ return self.action(server_id, 'os-stop', None, **kwargs)
+
+ def start(self, server_id, **kwargs):
+ return self.action(server_id, 'os-start', None, **kwargs)
+
+ def create_image(self, server_id, name):
+ return self.action(server_id, 'createImage', None, name=name)
+
+ def add_security_group(self, server_id, name):
+ return self.action(server_id, 'addSecurityGroup', None, name=name)
+
+ def remove_security_group(self, server_id, name):
+ return self.action(server_id, 'removeSecurityGroup', None, name=name)
+
+ def live_migrate_server(self, server_id, dest_host, use_block_migration):
+ """This should be called with administrator privileges ."""
+
+ req_body = Element("os-migrateLive",
+ xmlns=XMLNS_11,
+ disk_over_commit=False,
+ block_migration=use_block_migration,
+ host=dest_host)
+
+ resp, body = self.post("servers/%s/action" % str(server_id),
+ str(Document(req_body)), self.headers)
+ return resp, body
+
+ def list_server_metadata(self, server_id):
+ resp, body = self.get("servers/%s/metadata" % str(server_id),
+ self.headers)
+ body = self._parse_key_value(etree.fromstring(body))
+ return resp, body
+
+ def set_server_metadata(self, server_id, meta, no_metadata_field=False):
+ doc = Document()
+ if not no_metadata_field:
+ metadata = Element("metadata")
+ doc.append(metadata)
+ for k, v in meta.items():
+ meta_element = Element("meta", key=k)
+ meta_element.append(Text(v))
+ metadata.append(meta_element)
+ resp, body = self.put('servers/%s/metadata' % str(server_id),
+ str(doc), self.headers)
+ return resp, xml_to_json(etree.fromstring(body))
+
+ def update_server_metadata(self, server_id, meta):
+ doc = Document()
+ metadata = Element("metadata")
+ doc.append(metadata)
+ for k, v in meta.items():
+ meta_element = Element("meta", key=k)
+ meta_element.append(Text(v))
+ metadata.append(meta_element)
+ resp, body = self.post("/servers/%s/metadata" % str(server_id),
+ str(doc), headers=self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def get_server_metadata_item(self, server_id, key):
+ resp, body = self.get("servers/%s/metadata/%s" % (str(server_id), key),
+ headers=self.headers)
+ return resp, dict([(etree.fromstring(body).attrib['key'],
+ xml_to_json(etree.fromstring(body)))])
+
+ def set_server_metadata_item(self, server_id, key, meta):
+ doc = Document()
+ for k, v in meta.items():
+ meta_element = Element("meta", key=k)
+ meta_element.append(Text(v))
+ doc.append(meta_element)
+ resp, body = self.put('servers/%s/metadata/%s' % (str(server_id), key),
+ str(doc), self.headers)
+ return resp, xml_to_json(etree.fromstring(body))
+
+ def delete_server_metadata_item(self, server_id, key):
+ resp, body = self.delete("servers/%s/metadata/%s" %
+ (str(server_id), key))
+ return resp, body
+
+ def get_console_output(self, server_id, length):
+ return self.action(server_id, 'os-getConsoleOutput', 'output',
+ length=length)
+
+ def list_virtual_interfaces(self, server_id):
+ """
+ List the virtual interfaces used in an instance.
+ """
+ resp, body = self.get('/'.join(['servers', server_id,
+ 'os-virtual-interfaces']), self.headers)
+ virt_int = self._parse_xml_virtual_interfaces(etree.fromstring(body))
+ return resp, virt_int
+
+ def rescue_server(self, server_id, adminPass=None):
+ """Rescue the provided server."""
+ return self.action(server_id, 'rescue', None, adminPass=adminPass)
+
+ def unrescue_server(self, server_id):
+ """Unrescue the provided server."""
+ return self.action(server_id, 'unrescue', None)
+
+ def attach_volume(self, server_id, volume_id, device='/dev/vdz'):
+ post_body = Element("volumeAttachment", volumeId=volume_id,
+ device=device)
+ resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
+ str(Document(post_body)), self.headers)
+ return resp, body
+
+ def detach_volume(self, server_id, volume_id):
+ headers = {'Content-Type': 'application/xml',
+ 'Accept': 'application/xml'}
+ resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
+ (server_id, volume_id), headers)
+ return resp, body
+
+ def get_server_diagnostics(self, server_id):
+ """Get the usage data for a server."""
+ resp, body = self.get("servers/%s/diagnostics" % server_id,
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def list_instance_actions(self, server_id):
+ """List the provided server action."""
+ resp, body = self.get("servers/%s/os-instance-actions" % server_id,
+ self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def get_instance_action(self, server_id, request_id):
+ """Returns the action details of the provided server."""
+ resp, body = self.get("servers/%s/os-instance-actions/%s" %
+ (server_id, request_id), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body