Add support to XML in images_client and its tests
Change-Id: Ia3fb6eaefc0929c3bc224d7b7f7c7504956a7f78
diff --git a/tempest/manager.py b/tempest/manager.py
index cde0fc0..7a5d6f5 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -40,7 +40,7 @@
from tempest.services.nova.json import console_output_client
NetworkClient = network_client.NetworkClient
-ImagesClient = images_client.ImagesClient
+ImagesClient = images_client.ImagesClientJSON
FlavorsClient = flavors_client.FlavorsClientJSON
ServersClient = servers_client.ServersClientJSON
LimitsClient = limits_client.LimitsClientJSON
diff --git a/tempest/openstack.py b/tempest/openstack.py
index 290b278..ec0c257 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -25,7 +25,7 @@
from tempest.services.nova.json.flavors_client import FlavorsClientJSON
from tempest.services.nova.json.floating_ips_client import \
FloatingIPsClientJSON
-from tempest.services.nova.json.images_client import ImagesClient
+from tempest.services.nova.json.images_client import ImagesClientJSON
from tempest.services.nova.json.limits_client import LimitsClientJSON
from tempest.services.nova.json.servers_client import ServersClientJSON
from tempest.services.nova.json.security_groups_client \
@@ -39,6 +39,7 @@
from tempest.services.nova.xml.flavors_client import FlavorsClientXML
from tempest.services.nova.xml.floating_ips_client import \
FloatingIPsClientXML
+from tempest.services.nova.xml.images_client import ImagesClientXML
from tempest.services.nova.xml.keypairs_client import KeyPairsClientXML
from tempest.services.nova.xml.limits_client import LimitsClientXML
from tempest.services.nova.xml.servers_client import ServersClientXML
@@ -48,6 +49,11 @@
LOG = logging.getLogger(__name__)
+IMAGES_CLIENTS = {
+ "json": ImagesClientJSON,
+ "xml": ImagesClientXML,
+}
+
KEYPAIRS_CLIENTS = {
"json": KeyPairsClientJSON,
"xml": KeyPairsClientXML,
@@ -127,6 +133,7 @@
try:
self.servers_client = SERVERS_CLIENTS[interface](*client_args)
self.limits_client = LIMITS_CLIENTS[interface](*client_args)
+ self.images_client = IMAGES_CLIENTS[interface](*client_args)
self.keypairs_client = KEYPAIRS_CLIENTS[interface](*client_args)
self.flavors_client = FLAVORS_CLIENTS[interface](*client_args)
self.extensions_client = \
@@ -137,7 +144,6 @@
except KeyError:
msg = "Unsupported interface type `%s'" % interface
raise exceptions.InvalidConfiguration(msg)
- self.images_client = ImagesClient(*client_args)
self.security_groups_client = SecurityGroupsClient(*client_args)
self.console_outputs_client = ConsoleOutputsClient(*client_args)
self.network_client = NetworkClient(*client_args)
diff --git a/tempest/services/nova/json/images_client.py b/tempest/services/nova/json/images_client.py
index 87cb403..7a29b2f 100644
--- a/tempest/services/nova/json/images_client.py
+++ b/tempest/services/nova/json/images_client.py
@@ -4,11 +4,11 @@
import time
-class ImagesClient(RestClient):
+class ImagesClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- super(ImagesClient, self).__init__(config, username, password,
- auth_url, tenant_name)
+ super(ImagesClientJSON, self).__init__(config, username, password,
+ auth_url, tenant_name)
self.service = self.config.compute.catalog_type
self.build_interval = self.config.compute.build_interval
self.build_timeout = self.config.compute.build_timeout
diff --git a/tempest/services/nova/xml/images_client.py b/tempest/services/nova/xml/images_client.py
new file mode 100644
index 0000000..0df8dfc
--- /dev/null
+++ b/tempest/services/nova/xml/images_client.py
@@ -0,0 +1,198 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# 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 import exceptions
+from tempest.common.rest_client import RestClientXML
+from tempest.services.nova.xml.common import Document
+from tempest.services.nova.xml.common import Element
+from tempest.services.nova.xml.common import Text
+from tempest.services.nova.xml.common import xml_to_json
+from tempest.services.nova.xml.common import XMLNS_11
+
+
+class ImagesClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(ImagesClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+ self.build_interval = self.config.compute.build_interval
+ self.build_timeout = self.config.compute.build_timeout
+
+ def _parse_server(self, node):
+ json = xml_to_json(node)
+ return self._parse_links(node, json)
+
+ def _parse_image(self, node):
+ """Parses detailed XML image information into dictionary"""
+ json = xml_to_json(node)
+
+ self._parse_links(node, json)
+
+ # parse all metadata
+ if 'metadata' in json:
+ tag = node.find('{%s}metadata' % XMLNS_11)
+ json['metadata'] = dict((x.get('key'), x.text)
+ for x in tag.getchildren())
+
+ # parse server information
+ if 'server' in json:
+ tag = node.find('{%s}server' % XMLNS_11)
+ json['server'] = self._parse_server(tag)
+ return json
+
+ def _parse_links(self, node, json):
+ """Append multiple links under a list"""
+ # look for links
+ if 'link' in json:
+ # remove single link element
+ del json['link']
+ json['links'] = [xml_to_json(x) for x in
+ node.findall('{http://www.w3.org/2005/Atom}link')]
+ return json
+
+ def create_image(self, server_id, name, meta=None):
+ """Creates an image of the original server"""
+ post_body = Element('createImage', name=name)
+
+ if meta:
+ metadata = Element('metadata')
+ post_body.append(metadata)
+ for k, v in meta.items():
+ data = Element('meta', key=k)
+ data.append(Text(v))
+ metadata.append(data)
+ resp, body = self.post('servers/%s/action' % str(server_id),
+ str(Document(post_body)), self.headers)
+ return resp, body
+
+ def list_images(self, params=None):
+ """Returns a list of all images filtered by any parameters"""
+ url = 'images'
+ if params:
+ param_list = urllib.urlencode(params)
+ url += "?" + param_list
+
+ resp, body = self.get(url, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['images']
+
+ def list_images_with_detail(self, params=None):
+ """Returns a detailed list of images filtered by any parameters"""
+ url = 'images/detail'
+ if params:
+ param_list = urllib.urlencode(params)
+
+ url = "images/detail?" + param_list
+
+ resp, body = self.get(url, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['images']
+
+ def get_image(self, image_id):
+ """Returns the details of a single image"""
+ resp, body = self.get("images/%s" % str(image_id), self.headers)
+ body = self._parse_image(etree.fromstring(body))
+ return resp, body
+
+ def delete_image(self, image_id):
+ """Deletes the provided image"""
+ return self.delete("images/%s" % str(image_id), self.headers)
+
+ def wait_for_image_resp_code(self, image_id, code):
+ """
+ Waits until the HTTP response code for the request matches the
+ expected value
+ """
+ resp, body = self.get("images/%s" % str(image_id), self.headers)
+ start = int(time.time())
+
+ while resp.status != code:
+ time.sleep(self.build_interval)
+ resp, body = self.get("images/%s" % str(image_id), self.headers)
+
+ if int(time.time()) - start >= self.build_timeout:
+ raise exceptions.TimeoutException
+
+ def wait_for_image_status(self, image_id, status):
+ """Waits for an image to reach a given status."""
+ resp, image = self.get_image(image_id)
+ start = int(time.time())
+
+ while image['status'] != status:
+ time.sleep(self.build_interval)
+ resp, image = self.get_image(image_id)
+ if image['status'] == 'ERROR':
+ raise exceptions.AddImageException(image_id=image_id)
+
+ if int(time.time()) - start >= self.build_timeout:
+ raise exceptions.TimeoutException
+
+ def list_image_metadata(self, image_id):
+ """Lists all metadata items for an image"""
+ resp, body = self.get("images/%s/metadata" % str(image_id),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['metadata']
+
+ def set_image_metadata(self, image_id, meta):
+ """Sets the metadata for an image"""
+ post_body = json.dumps({'metadata': meta})
+ resp, body = self.put('images/%s/metadata' % str(image_id),
+ post_body, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['metadata']
+
+ def update_image_metadata(self, image_id, meta):
+ """Updates the metadata for an image"""
+ post_body = Element('metadata', meta)
+ for k, v in meta:
+ metadata = Element('meta', key=k)
+ text = Text(v)
+ metadata.append(text)
+ post_body.append(metadata)
+
+ resp, body = self.post('images/%s/metadata' % str(image_id),
+ post_body, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['metadata']
+
+ def get_image_metadata_item(self, image_id, key):
+ """Returns the value for a specific image metadata key"""
+ resp, body = self.get("images/%s/metadata/%s.xml" %
+ (str(image_id), key), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['meta']
+
+ def set_image_metadata_item(self, image_id, key, meta):
+ """Sets the value for a specific image metadata key"""
+ post_body = json.dumps({'meta': meta})
+ resp, body = self.put('images/%s/metadata/%s' % (str(image_id), key),
+ post_body, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['meta']
+
+ def delete_image_metadata_item(self, image_id, key):
+ """Deletes a single image metadata key/value pair"""
+ resp, body = self.delete("images/%s/metadata/%s" % (str(image_id), key,
+ self.headers))
+ return resp, body