port test_images and test_server_actions into v3 part1
This changeset only copies the v2 files into the appropriate v3
directories unchanged. This is being tried in order to make
reviewing of the porting easier as gerrit will display only what
is actually changed for v3 rather than entirely new files. There
is no image api in nova v3 api, so the images_client won't be ported
into v3 test. We will use glance directly.
Partially implements blueprint nova-v3-api-tests
Change-Id: I058407afb7bba4d55c619db27f59a91a46cbc6cf
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