diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index 9498148fd7dc..216f27b86cd3 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -75,9 +75,9 @@ use = call:nova.api.openstack.urlmap:urlmap_factory /v1.1: openstackapi11 [pipeline:openstackapi11] -pipeline = faultwrap noauth ratelimit extensions osapiapp11 +pipeline = faultwrap noauth ratelimit serialize extensions osapiapp11 # NOTE(vish): use the following pipeline for deprecated auth -# pipeline = faultwrap auth ratelimit extensions osapiapp11 +# pipeline = faultwrap auth ratelimit serialize extensions osapiapp11 [filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory @@ -91,6 +91,9 @@ paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory [filter:ratelimit] paste.filter_factory = nova.api.openstack.limits:RateLimitingMiddleware.factory +[filter:serialize] +paste.filter_factory = nova.api.openstack.wsgi:LazySerializationMiddleware.factory + [filter:extensions] paste.filter_factory = nova.api.openstack.extensions:ExtensionMiddleware.factory diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py index a13a758ab275..3a19d5c893fc 100644 --- a/nova/api/openstack/accounts.py +++ b/nova/api/openstack/accounts.py @@ -22,6 +22,7 @@ from nova import log as logging from nova.auth import manager from nova.api.openstack import faults from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil FLAGS = flags.FLAGS @@ -80,15 +81,25 @@ class Controller(object): return dict(account=_translate_keys(account)) -def create_resource(): - metadata = { - "attributes": { - "account": ["id", "name", "description", "manager"], - }, - } +class AccountTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('account', selector='account') + root.set('id', 'id') + root.set('name', 'name') + root.set('description', 'description') + root.set('manager', 'manager') + return xmlutil.MasterTemplate(root, 1) + + +class AccountXMLSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return AccountTemplate() + + +def create_resource(): body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata), + 'application/xml': AccountXMLSerializer(), } serializer = wsgi.ResponseSerializer(body_serializers) return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 30ca434149f4..4a26ee42fee0 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -356,52 +356,51 @@ class MetadataHeadersSerializer(wsgi.ResponseHeadersSerializer): response.status_int = 204 -class MetadataXMLSerializer(wsgi.XMLDictSerializer): +metadata_nsmap = {None: xmlutil.XMLNS_V11} - NSMAP = {None: xmlutil.XMLNS_V11} - def __init__(self, xmlns=wsgi.XMLNS_V11): - super(MetadataXMLSerializer, self).__init__(xmlns=xmlns) +class MetaItemTemplate(xmlutil.TemplateBuilder): + def construct(self): + sel = xmlutil.Selector('meta', xmlutil.get_items, 0) + root = xmlutil.TemplateElement('meta', selector=sel) + root.set('key', 0) + root.text = 1 + return xmlutil.MasterTemplate(root, 1, nsmap=metadata_nsmap) - def populate_metadata(self, metadata_elem, meta_dict): - for (key, value) in meta_dict.items(): - elem = etree.SubElement(metadata_elem, 'meta') - elem.set('key', str(key)) - elem.text = value - def _populate_meta_item(self, meta_elem, meta_item_dict): - """Populate a meta xml element from a dict.""" - (key, value) = meta_item_dict.items()[0] - meta_elem.set('key', str(key)) - meta_elem.text = value +class MetadataTemplateElement(xmlutil.TemplateElement): + def will_render(self, datum): + return True - def index(self, metadata_dict): - metadata = etree.Element('metadata', nsmap=self.NSMAP) - self.populate_metadata(metadata, metadata_dict.get('metadata', {})) - return self._to_xml(metadata) - def create(self, metadata_dict): - metadata = etree.Element('metadata', nsmap=self.NSMAP) - self.populate_metadata(metadata, metadata_dict.get('metadata', {})) - return self._to_xml(metadata) +class MetadataTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = MetadataTemplateElement('metadata', selector='metadata') + elem = xmlutil.SubTemplateElement(root, 'meta', + selector=xmlutil.get_items) + elem.set('key', 0) + elem.text = 1 + return xmlutil.MasterTemplate(root, 1, nsmap=metadata_nsmap) - def update_all(self, metadata_dict): - metadata = etree.Element('metadata', nsmap=self.NSMAP) - self.populate_metadata(metadata, metadata_dict.get('metadata', {})) - return self._to_xml(metadata) - def show(self, meta_item_dict): - meta = etree.Element('meta', nsmap=self.NSMAP) - self._populate_meta_item(meta, meta_item_dict['meta']) - return self._to_xml(meta) +class MetadataXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return MetadataTemplate() - def update(self, meta_item_dict): - meta = etree.Element('meta', nsmap=self.NSMAP) - self._populate_meta_item(meta, meta_item_dict['meta']) - return self._to_xml(meta) + def create(self): + return MetadataTemplate() - def default(self, *args, **kwargs): - return '' + def update_all(self): + return MetadataTemplate() + + def show(self): + return MetaItemTemplate() + + def update(self): + return MetaItemTemplate() + + def default(self): + return xmlutil.MasterTemplate(None, 1) def check_snapshots_enabled(f): diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py index 8f6dbaadfc5e..d42d34069a52 100644 --- a/nova/api/openstack/consoles.py +++ b/nova/api/openstack/consoles.py @@ -21,6 +21,7 @@ import webob from nova import console from nova import exception from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil def _translate_keys(cons): @@ -89,5 +90,51 @@ class Controller(object): return webob.Response(status_int=202) +class ConsoleTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('console', selector='console') + + id_elem = xmlutil.SubTemplateElement(root, 'id', selector='id') + id_elem.text = xmlutil.Selector() + + port_elem = xmlutil.SubTemplateElement(root, 'port', selector='port') + port_elem.text = xmlutil.Selector() + + host_elem = xmlutil.SubTemplateElement(root, 'host', selector='host') + host_elem.text = xmlutil.Selector() + + passwd_elem = xmlutil.SubTemplateElement(root, 'password', + selector='password') + passwd_elem.text = xmlutil.Selector() + + constype_elem = xmlutil.SubTemplateElement(root, 'console_type', + selector='console_type') + constype_elem.text = xmlutil.Selector() + + return xmlutil.MasterTemplate(root, 1) + + +class ConsolesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('consoles') + console = xmlutil.SubTemplateElement(root, 'console', + selector='consoles') + console.append(ConsoleTemplate()) + + return xmlutil.MasterTemplate(root, 1) + + +class ConsoleXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ConsolesTemplate() + + def show(self): + return ConsoleTemplate() + + def create_resource(): - return wsgi.Resource(Controller()) + body_serializers = { + 'application/xml': ConsoleXMLSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 43bc060149ef..6eb085d0912b 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -30,6 +30,7 @@ import webob.exc from nova import exception from nova import flags from nova import log as logging +from nova import utils from nova import wsgi as base_wsgi import nova.api.openstack from nova.api.openstack import common @@ -159,9 +160,20 @@ class RequestExtensionController(object): def process(self, req, *args, **kwargs): res = req.get_response(self.application) + + # Deserialize the response body, if any + body = None + if res.body: + body = utils.loads(res.body) + # currently request handlers are un-ordered for handler in self.handlers: - res = handler(req, res) + res = handler(req, res, body) + + # Reserialize the response body + if body is not None: + res.body = utils.dumps(body) + return res diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index cd6562e4f60a..0727ee258a74 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -81,50 +81,54 @@ class Controller(object): return views.flavors.ViewBuilder(base_url, project_id) -class FlavorXMLSerializer(wsgi.XMLDictSerializer): +def make_flavor(elem, detailed=False): + elem.set('name') + elem.set('id') + if detailed: + elem.set('ram') + elem.set('disk') - NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + for attr in ("vcpus", "swap", "rxtx_quota", "rxtx_cap"): + elem.set(attr, xmlutil.EmptyStringSelector(attr)) - def __init__(self): - super(FlavorXMLSerializer, self).__init__(xmlns=wsgi.XMLNS_V11) + xmlutil.make_links(elem, 'links') - def _populate_flavor(self, flavor_elem, flavor_dict, detailed=False): - """Populate a flavor xml element from a dict.""" - flavor_elem.set('name', flavor_dict['name']) - flavor_elem.set('id', str(flavor_dict['id'])) - if detailed: - flavor_elem.set('ram', str(flavor_dict['ram'])) - flavor_elem.set('disk', str(flavor_dict['disk'])) +flavor_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - for attr in ("vcpus", "swap", "rxtx_quota", "rxtx_cap"): - flavor_elem.set(attr, str(flavor_dict.get(attr, ""))) - for link in flavor_dict.get('links', []): - elem = etree.SubElement(flavor_elem, - '{%s}link' % xmlutil.XMLNS_ATOM) - elem.set('rel', link['rel']) - elem.set('href', link['href']) - return flavor_elem +class FlavorTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavor', selector='flavor') + make_flavor(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) - def show(self, flavor_container): - flavor = etree.Element('flavor', nsmap=self.NSMAP) - self._populate_flavor(flavor, flavor_container['flavor'], True) - return self._to_xml(flavor) - def detail(self, flavors_dict): - flavors = etree.Element('flavors', nsmap=self.NSMAP) - for flavor_dict in flavors_dict['flavors']: - flavor = etree.SubElement(flavors, 'flavor') - self._populate_flavor(flavor, flavor_dict, True) - return self._to_xml(flavors) +class MinimalFlavorsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavors') + elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') + make_flavor(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) - def index(self, flavors_dict): - flavors = etree.Element('flavors', nsmap=self.NSMAP) - for flavor_dict in flavors_dict['flavors']: - flavor = etree.SubElement(flavors, 'flavor') - self._populate_flavor(flavor, flavor_dict, False) - return self._to_xml(flavors) + +class FlavorsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavors') + elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') + make_flavor(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) + + +class FlavorXMLSerializer(xmlutil.XMLTemplateSerializer): + def show(self): + return FlavorTemplate() + + def detail(self): + return FlavorsTemplate() + + def index(self): + return MinimalFlavorsTemplate() def create_resource(): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 11b847266986..b86b48ff5d67 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -148,82 +148,63 @@ class Controller(object): raise webob.exc.HTTPMethodNotAllowed() -class ImageXMLSerializer(wsgi.XMLDictSerializer): +def make_image(elem, detailed=False): + elem.set('name') + elem.set('id') - NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + if detailed: + elem.set('updated') + elem.set('created') + elem.set('status') + elem.set('progress') + elem.set('minRam') + elem.set('minDisk') - def __init__(self): - self.metadata_serializer = common.MetadataXMLSerializer() + server = xmlutil.SubTemplateElement(elem, 'server', selector='server') + server.set('id') + xmlutil.make_links(server, 'links') - def _create_metadata_node(self, metadata_dict): - metadata_elem = etree.Element('metadata', nsmap=self.NSMAP) - self.metadata_serializer.populate_metadata(metadata_elem, - metadata_dict) - return metadata_elem + elem.append(common.MetadataTemplate()) - def _create_server_node(self, server_dict): - server_elem = etree.Element('server', nsmap=self.NSMAP) - server_elem.set('id', str(server_dict['id'])) - for link in server_dict.get('links', []): - elem = etree.SubElement(server_elem, - '{%s}link' % xmlutil.XMLNS_ATOM) - elem.set('rel', link['rel']) - elem.set('href', link['href']) - return server_elem + xmlutil.make_links(elem, 'links') - def _populate_image(self, image_elem, image_dict, detailed=False): - """Populate an image xml element from a dict.""" - image_elem.set('name', image_dict['name']) - image_elem.set('id', str(image_dict['id'])) - if detailed: - image_elem.set('updated', str(image_dict['updated'])) - image_elem.set('created', str(image_dict['created'])) - image_elem.set('status', str(image_dict['status'])) - if 'progress' in image_dict: - image_elem.set('progress', str(image_dict['progress'])) - if 'minRam' in image_dict: - image_elem.set('minRam', str(image_dict['minRam'])) - if 'minDisk' in image_dict: - image_elem.set('minDisk', str(image_dict['minDisk'])) - if 'server' in image_dict: - server_elem = self._create_server_node(image_dict['server']) - image_elem.append(server_elem) +image_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - meta_elem = self._create_metadata_node( - image_dict.get('metadata', {})) - image_elem.append(meta_elem) - self._populate_links(image_elem, image_dict.get('links', [])) +class ImageTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('image', selector='image') + make_image(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - def _populate_links(self, parent, links): - for link in links: - elem = etree.SubElement(parent, '{%s}link' % xmlutil.XMLNS_ATOM) - elem.set('rel', link['rel']) - if 'type' in link: - elem.set('type', link['type']) - elem.set('href', link['href']) - def index(self, images_dict): - images = etree.Element('images', nsmap=self.NSMAP) - for image_dict in images_dict['images']: - image = etree.SubElement(images, 'image') - self._populate_image(image, image_dict, False) +class MinimalImagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + make_image(elem) + xmlutil.make_links(root, 'images_links') + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - self._populate_links(images, images_dict.get('images_links', [])) - return self._to_xml(images) - def detail(self, images_dict): - images = etree.Element('images', nsmap=self.NSMAP) - for image_dict in images_dict['images']: - image = etree.SubElement(images, 'image') - self._populate_image(image, image_dict, True) - return self._to_xml(images) +class ImagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + make_image(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - def show(self, image_dict): - image = etree.Element('image', nsmap=self.NSMAP) - self._populate_image(image, image_dict['image'], True) - return self._to_xml(image) + +class ImageXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return MinimalImagesTemplate() + + def detail(self): + return ImagesTemplate() + + def show(self): + return ImageTemplate() def create_resource(): diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py index 48fd4eeb709f..1ab9a4fdcd2e 100644 --- a/nova/api/openstack/ips.py +++ b/nova/api/openstack/ips.py @@ -74,37 +74,40 @@ class Controller(object): return views_addresses.ViewBuilder() -class IPXMLSerializer(wsgi.XMLDictSerializer): +def make_network(elem): + elem.set('id', 0) - NSMAP = {None: xmlutil.XMLNS_V11} + ip = xmlutil.SubTemplateElement(elem, 'ip', selector=1) + ip.set('version') + ip.set('addr') - def __init__(self, xmlns=wsgi.XMLNS_V11): - super(IPXMLSerializer, self).__init__(xmlns=xmlns) - def populate_addresses_node(self, addresses_elem, addresses_dict): - for (network_id, ip_dicts) in addresses_dict.items(): - network_elem = self._create_network_node(network_id, ip_dicts) - addresses_elem.append(network_elem) +network_nsmap = {None: xmlutil.XMLNS_V11} - def _create_network_node(self, network_id, ip_dicts): - network_elem = etree.Element('network', nsmap=self.NSMAP) - network_elem.set('id', str(network_id)) - for ip_dict in ip_dicts: - ip_elem = etree.SubElement(network_elem, 'ip') - ip_elem.set('version', str(ip_dict['version'])) - ip_elem.set('addr', ip_dict['addr']) - return network_elem - def show(self, network_dict): - (network_id, ip_dicts) = network_dict.items()[0] - network = self._create_network_node(network_id, ip_dicts) - return self._to_xml(network) +class NetworkTemplate(xmlutil.TemplateBuilder): + def construct(self): + sel = xmlutil.Selector(xmlutil.get_items, 0) + root = xmlutil.TemplateElement('network', selector=sel) + make_network(root) + return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) - def index(self, addresses_dict): - addresses = etree.Element('addresses', nsmap=self.NSMAP) - self.populate_addresses_node(addresses, - addresses_dict.get('addresses', {})) - return self._to_xml(addresses) + +class AddressesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('addresses', selector='addresses') + elem = xmlutil.SubTemplateElement(root, 'network', + selector=xmlutil.get_items) + make_network(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) + + +class IPXMLSerializer(xmlutil.XMLTemplateSerializer): + def show(self): + return NetworkTemplate() + + def index(self): + return AddressesTemplate() def create_resource(): diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index 564f62b1227a..0b549de2ecea 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -68,53 +68,37 @@ class LimitsController(object): return limits_views.ViewBuilder() -class LimitsXMLSerializer(wsgi.XMLDictSerializer): +limits_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - xmlns = wsgi.XMLNS_V11 - NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} +class LimitsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('limits', selector='limits') - def __init__(self): - pass + rates = xmlutil.SubTemplateElement(root, 'rates') + rate = xmlutil.SubTemplateElement(rates, 'rate', selector='rate') + rate.set('uri', 'uri') + rate.set('regex', 'regex') + limit = xmlutil.SubTemplateElement(rate, 'limit', selector='limit') + limit.set('value', 'value') + limit.set('verb', 'verb') + limit.set('remaining', 'remaining') + limit.set('unit', 'unit') + limit.set('next-available', 'next-available') - def _create_rates_node(self, rates): - rates_elem = etree.Element('rates', nsmap=self.NSMAP) - for rate in rates: - rate_node = etree.SubElement(rates_elem, 'rate') - rate_node.set('uri', rate['uri']) - rate_node.set('regex', rate['regex']) - for limit in rate['limit']: - limit_elem = etree.SubElement(rate_node, 'limit') - limit_elem.set('value', str(limit['value'])) - limit_elem.set('verb', str(limit['verb'])) - limit_elem.set('remaining', str(limit['remaining'])) - limit_elem.set('unit', str(limit['unit'])) - limit_elem.set('next-available', str(limit['next-available'])) - return rates_elem + absolute = xmlutil.SubTemplateElement(root, 'absolute', + selector='absolute') + limit = xmlutil.SubTemplateElement(absolute, 'limit', + selector=xmlutil.get_items) + limit.set('name', 0) + limit.set('value', 1) - def _create_absolute_node(self, absolute_dict): - absolute_elem = etree.Element('absolute', nsmap=self.NSMAP) - for key, value in absolute_dict.items(): - limit_elem = etree.SubElement(absolute_elem, 'limit') - limit_elem.set('name', str(key)) - limit_elem.set('value', str(value)) - return absolute_elem + return xmlutil.MasterTemplate(root, 1, nsmap=limits_nsmap) - def _populate_limits(self, limits_elem, limits_dict): - """Populate a limits xml element from a dict.""" - rates_elem = self._create_rates_node( - limits_dict.get('rate', [])) - limits_elem.append(rates_elem) - - absolutes_elem = self._create_absolute_node( - limits_dict.get('absolute', {})) - limits_elem.append(absolutes_elem) - - def index(self, limits_dict): - limits = etree.Element('limits', nsmap=self.NSMAP) - self._populate_limits(limits, limits_dict['limits']) - return self._to_xml(limits) +class LimitsXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return LimitsTemplate() def create_resource(): diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 4d55f40f4e9f..83231b11961e 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -988,129 +988,107 @@ class HeadersSerializer(wsgi.ResponseHeadersSerializer): response.status_int = 202 -class ServerXMLSerializer(wsgi.XMLDictSerializer): +class SecurityGroupsTemplateElement(xmlutil.TemplateElement): + def will_render(self, datum): + return 'security_groups' in datum - NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - def __init__(self): - self.metadata_serializer = common.MetadataXMLSerializer() - self.addresses_serializer = ips.IPXMLSerializer() +def make_server(elem, detailed=False): + elem.set('name') + elem.set('id') - def _create_metadata_node(self, metadata_dict): - metadata_elem = etree.Element('metadata', nsmap=self.NSMAP) - self.metadata_serializer.populate_metadata(metadata_elem, - metadata_dict) - return metadata_elem + if detailed: + elem.set('uuid') + elem.set('userId', 'user_id') + elem.set('tenantId', 'tenant_id') + elem.set('updated') + elem.set('created') + elem.set('hostId') + elem.set('accessIPv4') + elem.set('accessIPv6') + elem.set('status') + elem.set('progress') - def _create_image_node(self, image_dict): - image_elem = etree.Element('image', nsmap=self.NSMAP) - image_elem.set('id', str(image_dict['id'])) - for link in image_dict.get('links', []): - elem = etree.SubElement(image_elem, - '{%s}link' % xmlutil.XMLNS_ATOM) - elem.set('rel', link['rel']) - elem.set('href', link['href']) - return image_elem + # Attach image node + image = xmlutil.SubTemplateElement(elem, 'image', selector='image') + image.set('id') + xmlutil.make_links(image, 'links') - def _create_flavor_node(self, flavor_dict): - flavor_elem = etree.Element('flavor', nsmap=self.NSMAP) - flavor_elem.set('id', str(flavor_dict['id'])) - for link in flavor_dict.get('links', []): - elem = etree.SubElement(flavor_elem, - '{%s}link' % xmlutil.XMLNS_ATOM) - elem.set('rel', link['rel']) - elem.set('href', link['href']) - return flavor_elem + # Attach flavor node + flavor = xmlutil.SubTemplateElement(elem, 'flavor', selector='flavor') + flavor.set('id') + xmlutil.make_links(flavor, 'links') - def _create_addresses_node(self, addresses_dict): - addresses_elem = etree.Element('addresses', nsmap=self.NSMAP) - self.addresses_serializer.populate_addresses_node(addresses_elem, - addresses_dict) - return addresses_elem + # Attach metadata node + elem.append(common.MetadataTemplate()) - def _populate_server(self, server_elem, server_dict, detailed=False): - """Populate a server xml element from a dict.""" + # Attach addresses node + elem.append(ips.AddressesTemplate()) - server_elem.set('name', server_dict['name']) - server_elem.set('id', str(server_dict['id'])) - if detailed: - server_elem.set('uuid', str(server_dict['uuid'])) - server_elem.set('userId', str(server_dict['user_id'])) - server_elem.set('tenantId', str(server_dict['tenant_id'])) - server_elem.set('updated', str(server_dict['updated'])) - server_elem.set('created', str(server_dict['created'])) - server_elem.set('hostId', str(server_dict['hostId'])) - server_elem.set('accessIPv4', str(server_dict['accessIPv4'])) - server_elem.set('accessIPv6', str(server_dict['accessIPv6'])) - server_elem.set('status', str(server_dict['status'])) - if 'progress' in server_dict: - server_elem.set('progress', str(server_dict['progress'])) - image_elem = self._create_image_node(server_dict['image']) - server_elem.append(image_elem) + # Attach security groups node + secgrps = SecurityGroupsTemplateElement('security_groups') + elem.append(secgrps) + secgrp = xmlutil.SubTemplateElement(secgrps, 'security_group', + selector='security_groups') + secgrp.set('name') - flavor_elem = self._create_flavor_node(server_dict['flavor']) - server_elem.append(flavor_elem) + xmlutil.make_links(elem, 'links') - meta_elem = self._create_metadata_node( - server_dict.get('metadata', {})) - server_elem.append(meta_elem) - addresses_elem = self._create_addresses_node( - server_dict.get('addresses', {})) - server_elem.append(addresses_elem) - groups = server_dict.get('security_groups') - if groups: - groups_elem = etree.SubElement(server_elem, 'security_groups') - for group in groups: - group_elem = etree.SubElement(groups_elem, - 'security_group') - group_elem.set('name', group['name']) +server_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - self._populate_links(server_elem, server_dict.get('links', [])) - def _populate_links(self, parent, links): - for link in links: - elem = etree.SubElement(parent, - '{%s}link' % xmlutil.XMLNS_ATOM) - elem.set('rel', link['rel']) - elem.set('href', link['href']) +class ServerTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server', selector='server') + make_server(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - def index(self, servers_dict): - servers = etree.Element('servers', nsmap=self.NSMAP) - for server_dict in servers_dict['servers']: - server = etree.SubElement(servers, 'server') - self._populate_server(server, server_dict, False) - self._populate_links(servers, servers_dict.get('servers_links', [])) - return self._to_xml(servers) +class MinimalServersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + make_server(elem) + xmlutil.make_links(root, 'servers_links') + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - def detail(self, servers_dict): - servers = etree.Element('servers', nsmap=self.NSMAP) - for server_dict in servers_dict['servers']: - server = etree.SubElement(servers, 'server') - self._populate_server(server, server_dict, True) - return self._to_xml(servers) - def show(self, server_dict): - server = etree.Element('server', nsmap=self.NSMAP) - self._populate_server(server, server_dict['server'], True) - return self._to_xml(server) +class ServersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + make_server(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - def create(self, server_dict): - server = etree.Element('server', nsmap=self.NSMAP) - self._populate_server(server, server_dict['server'], True) - server.set('adminPass', server_dict['server']['adminPass']) - return self._to_xml(server) - def action(self, server_dict): - #NOTE(bcwaldon): We need a way to serialize actions individually. This - # assumes all actions return a server entity - return self.create(server_dict) +class ServerAdminPassTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server') + root.set('adminPass') + return xmlutil.SlaveTemplate(root, 1, nsmap=server_nsmap) - def update(self, server_dict): - server = etree.Element('server', nsmap=self.NSMAP) - self._populate_server(server, server_dict['server'], True) - return self._to_xml(server) + +class ServerXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return MinimalServersTemplate() + + def detail(self): + return ServersTemplate() + + def show(self): + return ServerTemplate() + + def update(self): + return ServerTemplate() + + def create(self): + master = ServerTemplate() + master.attach(ServerAdminPassTemplate()) + return master + + def action(self): + return self.create() class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer): diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py index 8dd72d559932..9fac45763403 100644 --- a/nova/api/openstack/users.py +++ b/nova/api/openstack/users.py @@ -20,6 +20,7 @@ from nova import flags from nova import log as logging from nova.api.openstack import common from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil from nova.auth import manager @@ -97,15 +98,40 @@ class Controller(object): return dict(user=_translate_keys(self.manager.get_user(id))) -def create_resource(): - metadata = { - "attributes": { - "user": ["id", "name", "access", "secret", "admin"], - }, - } +def make_user(elem): + elem.set('id') + elem.set('name') + elem.set('access') + elem.set('secret') + elem.set('admin') + +class UserTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('user', selector='user') + make_user(root) + return xmlutil.MasterTemplate(root, 1) + + +class UsersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('users') + elem = xmlutil.SubTemplateElement(root, 'user', selector='users') + make_user(elem) + return xmlutil.MasterTemplate(root, 1) + + +class UserXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return UsersTemplate() + + def default(self): + return UserTemplate() + + +def create_resource(): body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata), + 'application/xml': UserXMLSerializer(), } serializer = wsgi.ResponseSerializer(body_serializers) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index aa420953b702..a3ca5cd5b43a 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -449,7 +449,8 @@ class ResponseSerializer(object): self.headers_serializer = headers_serializer or \ ResponseHeadersSerializer() - def serialize(self, response_data, content_type, action='default'): + def serialize(self, request, response_data, content_type, + action='default'): """Serialize a dict into a string and wrap in a wsgi.Request object. :param response_data: dict produced by the Controller @@ -458,17 +459,28 @@ class ResponseSerializer(object): """ response = webob.Response() self.serialize_headers(response, response_data, action) - self.serialize_body(response, response_data, content_type, action) + self.serialize_body(request, response, response_data, content_type, + action) return response def serialize_headers(self, response, data, action): self.headers_serializer.serialize(response, data, action) - def serialize_body(self, response, data, content_type, action): + def serialize_body(self, request, response, data, content_type, action): response.headers['Content-Type'] = content_type if data is not None: serializer = self.get_body_serializer(content_type) - response.body = serializer.serialize(data, action) + lazy_serialize = request.environ.get('nova.lazy_serialize', False) + if lazy_serialize: + response.body = utils.dumps(data) + request.environ['nova.serializer'] = serializer + request.environ['nova.action'] = action + if (hasattr(serializer, 'get_template') and + 'nova.template' not in request.environ): + template = serializer.get_template(action) + request.environ['nova.template'] = template + else: + response.body = serializer.serialize(data, action) def get_body_serializer(self, content_type): try: @@ -478,6 +490,32 @@ class ResponseSerializer(object): raise exception.InvalidContentType(content_type=content_type) +class LazySerializationMiddleware(wsgi.Middleware): + """Lazy serialization middleware.""" + @webob.dec.wsgify(RequestClass=Request) + def __call__(self, req): + # Request lazy serialization + req.environ['nova.lazy_serialize'] = True + + response = req.get_response(self.application) + + # See if there's a serializer... + serializer = req.environ.get('nova.serializer') + if serializer is None: + return response + + # OK, build up the arguments for the serialize() method + kwargs = dict(action=req.environ['nova.action']) + if 'nova.template' in req.environ: + kwargs['template'] = req.environ['nova.template'] + + # Re-serialize the body + response.body = serializer.serialize(utils.loads(response.body), + **kwargs) + + return response + + class Resource(wsgi.Application): """WSGI app that handles (de)serialization and controller dispatch. @@ -531,7 +569,8 @@ class Resource(wsgi.Application): action_result = faults.Fault(ex) if type(action_result) is dict or action_result is None: - response = self.serializer.serialize(action_result, + response = self.serializer.serialize(request, + action_result, accept, action=action) else: diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py index d5eb88a57fd2..5779849b3e90 100644 --- a/nova/api/openstack/xmlutil.py +++ b/nova/api/openstack/xmlutil.py @@ -20,6 +20,7 @@ import os.path from lxml import etree from nova import utils +from nova.api.openstack import wsgi XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0' @@ -38,3 +39,857 @@ def validate_schema(xml, schema_name): schema_doc = etree.parse(schema_path) relaxng = etree.RelaxNG(schema_doc) relaxng.assertValid(xml) + + +class Selector(object): + """Selects datum to operate on from an object.""" + + def __init__(self, *chain): + """Initialize the selector. + + Each argument is a subsequent index into the object. + """ + + self.chain = chain + + def __repr__(self): + """Return a representation of the selector.""" + + return "Selector" + repr(self.chain) + + def __call__(self, obj, do_raise=False): + """Select a datum to operate on. + + Selects the relevant datum within the object. + + :param obj: The object from which to select the object. + :param do_raise: If False (the default), return None if the + indexed datum does not exist. Otherwise, + raise a KeyError. + """ + + # Walk the selector list + for elem in self.chain: + # If it's callable, call it + if callable(elem): + obj = elem(obj) + else: + # Use indexing + try: + obj = obj[elem] + except (KeyError, IndexError): + # No sense going any further + if do_raise: + # Convert to a KeyError, for consistency + raise KeyError(elem) + return None + + # Return the finally-selected object + return obj + + +def get_items(obj): + """Get items in obj.""" + + return list(obj.items()) + + +class EmptyStringSelector(Selector): + """Returns the empty string if Selector would return None.""" + def __call__(self, obj, do_raise=False): + """Returns empty string if the selected value does not exist.""" + + try: + return super(EmptyStringSelector, self).__call__(obj, True) + except KeyError: + return "" + + +class ConstantSelector(object): + """Returns a constant.""" + + def __init__(self, value): + """Initialize the selector. + + :param value: The value to return. + """ + + self.value = value + + def __repr__(self): + """Return a representation of the selector.""" + + return repr(self.value) + + def __call__(self, _obj, _do_raise=False): + """Select a datum to operate on. + + Returns a constant value. Compatible with + Selector.__call__(). + """ + + return self.value + + +class TemplateElement(object): + """Represent an element in the template.""" + + def __init__(self, tag, attrib=None, selector=None, **extra): + """Initialize an element. + + Initializes an element in the template. Keyword arguments + specify attributes to be set on the element; values must be + callables. See TemplateElement.set() for more information. + + :param tag: The name of the tag to create. + :param attrib: An optional dictionary of element attributes. + :param selector: An optional callable taking an object and + optional boolean do_raise indicator and + returning the object bound to the element. + """ + + # Convert selector into a Selector + if selector is None: + selector = Selector() + elif not callable(selector): + selector = Selector(selector) + + self.tag = tag + self.selector = selector + self.attrib = {} + self._text = None + self._children = [] + self._childmap = {} + + # Run the incoming attributes through set() so that they + # become selectorized + if not attrib: + attrib = {} + attrib.update(extra) + for k, v in attrib.items(): + self.set(k, v) + + def __repr__(self): + """Return a representation of the template element.""" + + return ('<%s.%s %r at %#x>' % + (self.__class__.__module__, self.__class__.__name__, + self.tag, id(self))) + + def __len__(self): + """Return the number of child elements.""" + + return len(self._children) + + def __contains__(self, key): + """Determine whether a child node named by key exists.""" + + return key in self._childmap + + def __getitem__(self, idx): + """Retrieve a child node by index or name.""" + + if isinstance(idx, basestring): + # Allow access by node name + return self._childmap[idx] + else: + return self._children[idx] + + def append(self, elem): + """Append a child to the element.""" + + # Unwrap templates... + elem = elem.unwrap() + + # Avoid duplications + if elem.tag in self._childmap: + raise KeyError(elem.tag) + + self._children.append(elem) + self._childmap[elem.tag] = elem + + def extend(self, elems): + """Append children to the element.""" + + # Pre-evaluate the elements + elemmap = {} + elemlist = [] + for elem in elems: + # Unwrap templates... + elem = elem.unwrap() + + # Avoid duplications + if elem.tag in self._childmap or elem.tag in elemmap: + raise KeyError(elem.tag) + + elemmap[elem.tag] = elem + elemlist.append(elem) + + # Update the children + self._children.extend(elemlist) + self._childmap.update(elemmap) + + def insert(self, idx, elem): + """Insert a child element at the given index.""" + + # Unwrap templates... + elem = elem.unwrap() + + # Avoid duplications + if elem.tag in self._childmap: + raise KeyError(elem.tag) + + self._children.insert(idx, elem) + self._childmap[elem.tag] = elem + + def remove(self, elem): + """Remove a child element.""" + + # Unwrap templates... + elem = elem.unwrap() + + # Check if element exists + if elem.tag not in self._childmap or self._childmap[elem.tag] != elem: + raise ValueError(_('element is not a child')) + + self._children.remove(elem) + del self._childmap[elem.tag] + + def get(self, key): + """Get an attribute. + + Returns a callable which performs datum selection. + + :param key: The name of the attribute to get. + """ + + return self.attrib[key] + + def set(self, key, value=None): + """Set an attribute. + + :param key: The name of the attribute to set. + + :param value: A callable taking an object and optional boolean + do_raise indicator and returning the datum bound + to the attribute. If None, a Selector() will be + constructed from the key. If a string, a + Selector() will be constructed from the string. + """ + + # Convert value to a selector + if value is None: + value = Selector(key) + elif not callable(value): + value = Selector(value) + + self.attrib[key] = value + + def keys(self): + """Return the attribute names.""" + + return self.attrib.keys() + + def items(self): + """Return the attribute names and values.""" + + return self.attrib.items() + + def unwrap(self): + """Unwraps a template to return a template element.""" + + # We are a template element + return self + + def wrap(self): + """Wraps a template element to return a template.""" + + # Wrap in a basic Template + return Template(self) + + def apply(self, elem, obj): + """Apply text and attributes to an etree.Element. + + Applies the text and attribute instructions in the template + element to an etree.Element instance. + + :param elem: An etree.Element instance. + :param obj: The base object associated with this template + element. + """ + + # Start with the text... + if self.text is not None: + elem.text = unicode(self.text(obj)) + + # Now set up all the attributes... + for key, value in self.attrib.items(): + try: + elem.set(key, unicode(value(obj, True))) + except KeyError: + # Attribute has no value, so don't include it + pass + + def _render(self, parent, datum, patches, nsmap): + """Internal rendering. + + Renders the template node into an etree.Element object. + Returns the etree.Element object. + + :param parent: The parent etree.Element instance. + :param datum: The datum associated with this template element. + :param patches: A list of other template elements that must + also be applied. + :param nsmap: An optional namespace dictionary to be + associated with the etree.Element instance. + """ + + # Allocate a node + if callable(self.tag): + tagname = self.tag(datum) + else: + tagname = self.tag + elem = etree.Element(tagname, nsmap=nsmap) + + # If we have a parent, append the node to the parent + if parent is not None: + parent.append(elem) + + # If the datum is None, do nothing else + if datum is None: + return elem + + # Apply this template element to the element + self.apply(elem, datum) + + # Additionally, apply the patches + for patch in patches: + patch.apply(elem, datum) + + # We have fully rendered the element; return it + return elem + + def render(self, parent, obj, patches=[], nsmap=None): + """Render an object. + + Renders an object against this template node. Returns a list + of two-item tuples, where the first item is an etree.Element + instance and the second item is the datum associated with that + instance. + + :param parent: The parent for the etree.Element instances. + :param obj: The object to render this template element + against. + :param patches: A list of other template elements to apply + when rendering this template element. + :param nsmap: An optional namespace dictionary to attach to + the etree.Element instances. + """ + + # First, get the datum we're rendering + data = None if obj is None else self.selector(obj) + + # Check if we should render at all + if not self.will_render(data): + return [] + elif data is None: + return [(self._render(parent, None, patches, nsmap), None)] + + # Make the data into a list if it isn't already + if not isinstance(data, list): + data = [data] + elif parent is None: + raise ValueError(_('root element selecting a list')) + + # Render all the elements + elems = [] + for datum in data: + elems.append((self._render(parent, datum, patches, nsmap), datum)) + + # Return all the elements rendered, as well as the + # corresponding datum for the next step down the tree + return elems + + def will_render(self, datum): + """Hook method. + + An overridable hook method to determine whether this template + element will be rendered at all. By default, returns False + (inhibiting rendering) if the datum is None. + + :param datum: The datum associated with this template element. + """ + + # Don't render if datum is None + return datum is not None + + def _text_get(self): + """Template element text. + + Either None or a callable taking an object and optional + boolean do_raise indicator and returning the datum bound to + the text of the template element. + """ + + return self._text + + def _text_set(self, value): + # Convert value to a selector + if value is not None and not callable(value): + value = Selector(value) + + self._text = value + + def _text_del(self): + self._text = None + + text = property(_text_get, _text_set, _text_del) + + def tree(self): + """Return string representation of the template tree. + + Returns a representation of the template rooted at this + element as a string, suitable for inclusion in debug logs. + """ + + # Build the inner contents of the tag... + contents = [self.tag, '!selector=%r' % self.selector] + + # Add the text... + if self.text is not None: + contents.append('!text=%r' % self.text) + + # Add all the other attributes + for key, value in self.attrib.items(): + contents.append('%s=%r' % (key, value)) + + # If there are no children, return it as a closed tag + if len(self) == 0: + return '<%s/>' % ' '.join(contents) + + # OK, recurse to our children + children = [c.tree() for c in self] + + # Return the result + return ('<%s>%s' % + (' '.join(contents), ''.join(children), self.tag)) + + +def SubTemplateElement(parent, tag, attrib=None, selector=None, **extra): + """Create a template element as a child of another. + + Corresponds to the etree.SubElement interface. Parameters are as + for TemplateElement, with the addition of the parent. + """ + + # Convert attributes + attrib = attrib or {} + attrib.update(extra) + + # Get a TemplateElement + elem = TemplateElement(tag, attrib=attrib, selector=selector) + + # Append the parent safely + if parent is not None: + parent.append(elem) + + return elem + + +class Template(object): + """Represent a template.""" + + def __init__(self, root, nsmap=None): + """Initialize a template. + + :param root: The root element of the template. + :param nsmap: An optional namespace dictionary to be + associated with the root element of the + template. + """ + + self.root = root.unwrap() if root is not None else None + self.nsmap = nsmap or {} + + def _serialize(self, parent, obj, siblings, nsmap=None): + """Internal serialization. + + Recursive routine to build a tree of etree.Element instances + from an object based on the template. Returns the first + etree.Element instance rendered, or None. + + :param parent: The parent etree.Element instance. Can be + None. + :param obj: The object to render. + :param siblings: The TemplateElement instances against which + to render the object. + :param nsmap: An optional namespace dictionary to be + associated with the etree.Element instance + rendered. + """ + + # First step, render the element + elems = siblings[0].render(parent, obj, siblings[1:], nsmap) + + # Now, recurse to all child elements + seen = set() + for idx, sibling in enumerate(siblings): + for child in sibling: + # Have we handled this child already? + if child.tag in seen: + continue + seen.add(child.tag) + + # Determine the child's siblings + nieces = [child] + for sib in siblings[idx + 1:]: + if child.tag in sib: + nieces.append(sib[child.tag]) + + # Now we recurse for every data element + for elem, datum in elems: + self._serialize(elem, datum, nieces) + + # Return the first element; at the top level, this will be the + # root element + if elems: + return elems[0][0] + + def serialize(self, obj, *args, **kwargs): + """Serialize an object. + + Serializes an object against the template. Returns a string + with the serialized XML. Positional and keyword arguments are + passed to etree.tostring(). + + :param obj: The object to serialize. + """ + + elem = self.make_tree(obj) + if elem is None: + return '' + + # Serialize it into XML + return etree.tostring(elem, *args, **kwargs) + + def make_tree(self, obj): + """Create a tree. + + Serializes an object against the template. Returns an Element + node with appropriate children. + + :param obj: The object to serialize. + """ + + # If the template is empty, return the empty string + if self.root is None: + return None + + # Get the siblings and nsmap of the root element + siblings = self._siblings() + nsmap = self._nsmap() + + # Form the element tree + return self._serialize(None, obj, siblings, nsmap) + + def _siblings(self): + """Hook method for computing root siblings. + + An overridable hook method to return the siblings of the root + element. By default, this is the root element itself. + """ + + return [self.root] + + def _nsmap(self): + """Hook method for computing the namespace dictionary. + + An overridable hook method to return the namespace dictionary. + """ + + return self.nsmap.copy() + + def unwrap(self): + """Unwraps a template to return a template element.""" + + # Return the root element + return self.root + + def wrap(self): + """Wraps a template element to return a template.""" + + # We are a template + return self + + def apply(self, master): + """Hook method for determining slave applicability. + + An overridable hook method used to determine if this template + is applicable as a slave to a given master template. + + :param master: The master template to test. + """ + + return True + + def tree(self): + """Return string representation of the template tree. + + Returns a representation of the template as a string, suitable + for inclusion in debug logs. + """ + + return "%r: %s" % (self, self.root.tree()) + + +class MasterTemplate(Template): + """Represent a master template. + + Master templates are versioned derivatives of templates that + additionally allow slave templates to be attached. Slave + templates allow modification of the serialized result without + directly changing the master. + """ + + def __init__(self, root, version, nsmap=None): + """Initialize a master template. + + :param root: The root element of the template. + :param version: The version number of the template. + :param nsmap: An optional namespace dictionary to be + associated with the root element of the + template. + """ + + super(MasterTemplate, self).__init__(root, nsmap) + self.version = version + self.slaves = [] + + def __repr__(self): + """Return string representation of the template.""" + + return ("<%s.%s object version %s at %#x>" % + (self.__class__.__module__, self.__class__.__name__, + self.version, id(self))) + + def _siblings(self): + """Hook method for computing root siblings. + + An overridable hook method to return the siblings of the root + element. This is the root element plus the root elements of + all the slave templates. + """ + + return [self.root] + [slave.root for slave in self.slaves] + + def _nsmap(self): + """Hook method for computing the namespace dictionary. + + An overridable hook method to return the namespace dictionary. + The namespace dictionary is computed by taking the master + template's namespace dictionary and updating it from all the + slave templates. + """ + + nsmap = self.nsmap.copy() + for slave in self.slaves: + nsmap.update(slave._nsmap()) + return nsmap + + def attach(self, *slaves): + """Attach one or more slave templates. + + Attaches one or more slave templates to the master template. + Slave templates must have a root element with the same tag as + the master template. The slave template's apply() method will + be called to determine if the slave should be applied to this + master; if it returns False, that slave will be skipped. + (This allows filtering of slaves based on the version of the + master template.) + """ + + slave_list = [] + for slave in slaves: + slave = slave.wrap() + + # Make sure we have a tree match + if slave.root.tag != self.root.tag: + slavetag = slave.root.tag + mastertag = self.root.tag + msg = _("Template tree mismatch; adding slave %(slavetag)s " + "to master %(mastertag)s") % locals() + raise ValueError(msg) + + # Make sure slave applies to this template + if not slave.apply(self): + continue + + slave_list.append(slave) + + # Add the slaves + self.slaves.extend(slave_list) + + def copy(self): + """Return a copy of this master template.""" + + # Return a copy of the MasterTemplate + tmp = self.__class__(self.root, self.version, self.nsmap) + tmp.slaves = self.slaves[:] + return tmp + + +class SlaveTemplate(Template): + """Represent a slave template. + + Slave templates are versioned derivatives of templates. Each + slave has a minimum version and optional maximum version of the + master template to which they can be attached. + """ + + def __init__(self, root, min_vers, max_vers=None, nsmap=None): + """Initialize a slave template. + + :param root: The root element of the template. + :param min_vers: The minimum permissible version of the master + template for this slave template to apply. + :param max_vers: An optional upper bound for the master + template version. + :param nsmap: An optional namespace dictionary to be + associated with the root element of the + template. + """ + + super(SlaveTemplate, self).__init__(root, nsmap) + self.min_vers = min_vers + self.max_vers = max_vers + + def __repr__(self): + """Return string representation of the template.""" + + return ("<%s.%s object versions %s-%s at %#x>" % + (self.__class__.__module__, self.__class__.__name__, + self.min_vers, self.max_vers, id(self))) + + def apply(self, master): + """Hook method for determining slave applicability. + + An overridable hook method used to determine if this template + is applicable as a slave to a given master template. This + version requires the master template to have a version number + between min_vers and max_vers. + + :param master: The master template to test. + """ + + # Does the master meet our minimum version requirement? + if master.version < self.min_vers: + return False + + # How about our maximum version requirement? + if self.max_vers is not None and master.version > self.max_vers: + return False + + return True + + +class TemplateBuilder(object): + """Template builder. + + This class exists to allow templates to be lazily built without + having to build them each time they are needed. It must be + subclassed, and the subclass must implement the construct() + method, which must return a Template (or subclass) instance. The + constructor will always return the template returned by + construct(), or, if it has a copy() method, a copy of that + template. + """ + + _tmpl = None + + def __new__(cls, copy=True): + """Construct and return a template. + + :param copy: If True (the default), a copy of the template + will be constructed and returned, if possible. + """ + + # Do we need to construct the template? + if cls._tmpl is None: + tmp = super(TemplateBuilder, cls).__new__(cls) + + # Construct the template + cls._tmpl = tmp.construct() + + # If the template has a copy attribute, return the result of + # calling it + if copy and hasattr(cls._tmpl, 'copy'): + return cls._tmpl.copy() + + # Return the template + return cls._tmpl + + def construct(self): + """Construct a template. + + Called to construct a template instance, which it must return. + Only called once. + """ + + raise NotImplementedError(_("subclasses must implement construct()!")) + + +class XMLTemplateSerializer(wsgi.ActionDispatcher): + """Template-based XML serializer. + + Data serializer that uses templates to perform its serialization. + """ + + def get_template(self, action='default'): + """Retrieve the template to use for serialization.""" + + return self.dispatch(action=action) + + def serialize(self, data, action='default', template=None): + """Serialize data. + + :param data: The data to serialize. + :param action: The action, for identifying the template to + use. If no template is provided, + get_template() will be called with this action + to retrieve the template. + :param template: The template to use in serialization. + """ + + # No template provided, look one up + if template is None: + template = self.get_template(action) + + # Still couldn't find a template; try the base + # XMLDictSerializer + if template is None: + serial = wsgi.XMLDictSerializer() + return serial.serialize(data, action=action) + + # Serialize the template + return template.serialize(data, encoding='UTF-8', + xml_declaration=True) + + def default(self): + """Retrieve the default template to use.""" + + return None + + +def make_links(parent, selector=None): + """ + Attach an Atom element to the parent. + """ + + elem = SubTemplateElement(parent, '{%s}link' % XMLNS_ATOM, + selector=selector) + elem.set('rel') + elem.set('type') + elem.set('href') + + # Just for completeness... + return elem diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index be1644df87e0..2a95cd0a46bc 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -27,6 +27,7 @@ from nova.scheduler import api from nova.api.openstack import common from nova.api.openstack import servers +from nova.api.openstack import xmlutil from nova.api.openstack import wsgi @@ -143,16 +144,70 @@ class Controller(object): return cooked -def create_resource(): - metadata = { - "attributes": { - "zone": ["id", "api_url", "name", "capabilities"], - }, - } +class CapabilitySelector(object): + def __call__(self, obj, do_raise=False): + return [(k, v) for k, v in obj.items() + if k not in ('id', 'api_url', 'name', 'capabilities')] + +def make_zone(elem): + #elem = xmlutil.SubTemplateElement(parent, 'zone', selector=selector) + elem.set('id') + elem.set('api_url') + elem.set('name') + elem.set('capabilities') + + cap = xmlutil.SubTemplateElement(elem, xmlutil.Selector(0), + selector=CapabilitySelector()) + cap.text = 1 + + +zone_nsmap = {None: wsgi.XMLNS_V10} + + +class ZoneTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zone', selector='zone') + make_zone(root) + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +class ZonesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zones') + elem = xmlutil.SubTemplateElement(root, 'zone', selector='zones') + make_zone(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +class WeightsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('weights') + weight = xmlutil.SubTemplateElement(root, 'weight', selector='weights') + blob = xmlutil.SubTemplateElement(weight, 'blob') + blob.text = 'blob' + inner_weight = xmlutil.SubTemplateElement(weight, 'weight') + inner_weight.text = 'weight' + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +class ZonesXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ZonesTemplate() + + def detail(self): + return ZonesTemplate() + + def select(self): + return WeightsTemplate() + + def default(self): + return ZoneTemplate() + + +def create_resource(): body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11, - metadata=metadata), + 'application/xml': ZonesXMLSerializer(), } serializer = wsgi.ResponseSerializer(body_serializers) diff --git a/nova/tests/api/openstack/extensions/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks.py index 2d8313cf6044..70556a28df19 100644 --- a/nova/tests/api/openstack/extensions/foxinsocks.py +++ b/nova/tests/api/openstack/extensions/foxinsocks.py @@ -64,12 +64,10 @@ class Foxinsocks(object): def get_request_extensions(self): request_exts = [] - def _goose_handler(req, res): + def _goose_handler(req, res, body): #NOTE: This only handles JSON responses. # You can use content type header to test for XML. - data = json.loads(res.body) - data['flavor']['googoose'] = req.GET.get('chewing') - res.body = json.dumps(data) + body['flavor']['googoose'] = req.GET.get('chewing') return res req_ext1 = extensions.RequestExtension('GET', @@ -77,12 +75,10 @@ class Foxinsocks(object): _goose_handler) request_exts.append(req_ext1) - def _bands_handler(req, res): + def _bands_handler(req, res, body): #NOTE: This only handles JSON responses. # You can use content type header to test for XML. - data = json.loads(res.body) - data['big_bands'] = 'Pig Bands!' - res.body = json.dumps(data) + body['big_bands'] = 'Pig Bands!' return res req_ext2 = extensions.RequestExtension('GET', diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 1c6359ff1032..88cfb8d7a902 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -29,9 +29,10 @@ from nova.api import openstack from nova.api import auth as api_auth from nova.api.openstack import auth from nova.api.openstack import extensions -from nova.api.openstack import versions from nova.api.openstack import limits from nova.api.openstack import urlmap +from nova.api.openstack import versions +from nova.api.openstack import wsgi as os_wsgi from nova.auth.manager import User, Project import nova.image.fake from nova.tests.glance import stubs as glance_stubs @@ -65,7 +66,8 @@ def fake_wsgi(self, req): return self.application -def wsgi_app(inner_app11=None, fake_auth=True, fake_auth_context=None): +def wsgi_app(inner_app11=None, fake_auth=True, fake_auth_context=None, + serialization=os_wsgi.LazySerializationMiddleware): if not inner_app11: inner_app11 = openstack.APIRouter() @@ -76,11 +78,13 @@ def wsgi_app(inner_app11=None, fake_auth=True, fake_auth_context=None): ctxt = context.RequestContext('fake', 'fake', auth_token=True) api11 = openstack.FaultWrapper(api_auth.InjectContext(ctxt, limits.RateLimitingMiddleware( - extensions.ExtensionMiddleware(inner_app11)))) + serialization( + extensions.ExtensionMiddleware(inner_app11))))) else: api11 = openstack.FaultWrapper(auth.AuthMiddleware( limits.RateLimitingMiddleware( - extensions.ExtensionMiddleware(inner_app11)))) + serialization( + extensions.ExtensionMiddleware(inner_app11))))) Auth = auth mapper = urlmap.URLMap() mapper['/v1.1'] = api11 diff --git a/nova/tests/api/openstack/test_accounts.py b/nova/tests/api/openstack/test_accounts.py index 125ab8ea0e22..ea96e1348d77 100644 --- a/nova/tests/api/openstack/test_accounts.py +++ b/nova/tests/api/openstack/test_accounts.py @@ -16,6 +16,7 @@ import json +from lxml import etree import webob from nova import test @@ -59,10 +60,21 @@ class AccountsTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) + self.assertEqual(res.status_int, 200) self.assertEqual(res_dict['account']['id'], 'test1') self.assertEqual(res_dict['account']['name'], 'test1') self.assertEqual(res_dict['account']['manager'], 'id1') + + def test_get_account_xml(self): + req = webob.Request.blank('/v1.1/fake/accounts/test1.xml') + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual('account', res_tree.tag) + self.assertEqual('test1', res_tree.get('id')) + self.assertEqual('test1', res_tree.get('name')) + self.assertEqual('id1', res_tree.get('manager')) def test_account_delete(self): req = webob.Request.blank('/v1.1/fake/accounts/test1') @@ -91,6 +103,27 @@ class AccountsTest(test.TestCase): fakes.FakeAuthManager.projects) self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3) + def test_account_create_xml(self): + body = dict(account=dict(description='test account', + manager='id1')) + req = webob.Request.blank('/v1.1/fake/accounts/newacct.xml') + req.headers["Content-Type"] = "application/json" + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, 'account') + self.assertEqual(res_tree.get('id'), 'newacct') + self.assertEqual(res_tree.get('name'), 'newacct') + self.assertEqual(res_tree.get('description'), 'test account') + self.assertEqual(res_tree.get('manager'), 'id1') + self.assertTrue('newacct' in + fakes.FakeAuthManager.projects) + self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3) + def test_account_update(self): body = dict(account=dict(description='test account', manager='id2')) @@ -108,3 +141,22 @@ class AccountsTest(test.TestCase): self.assertEqual(res_dict['account']['description'], 'test account') self.assertEqual(res_dict['account']['manager'], 'id2') self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2) + + def test_account_update_xml(self): + body = dict(account=dict(description='test account', + manager='id2')) + req = webob.Request.blank('/v1.1/fake/accounts/test1.xml') + req.headers["Content-Type"] = "application/json" + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, 'account') + self.assertEqual(res_tree.get('id'), 'test1') + self.assertEqual(res_tree.get('name'), 'test1') + self.assertEqual(res_tree.get('description'), 'test account') + self.assertEqual(res_tree.get('manager'), 'id2') + self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2) diff --git a/nova/tests/api/openstack/test_consoles.py b/nova/tests/api/openstack/test_consoles.py index 25414bc7d438..75b55942b17b 100644 --- a/nova/tests/api/openstack/test_consoles.py +++ b/nova/tests/api/openstack/test_consoles.py @@ -18,6 +18,8 @@ import datetime import json + +from lxml import etree import webob from nova.api.openstack import consoles @@ -142,6 +144,30 @@ class ConsolesTest(test.TestCase): res_dict = json.loads(res.body) self.assertDictMatch(res_dict, expected) + def test_show_console_xml(self): + def fake_get_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, 10) + self.assertEqual(console_id, 20) + pool = dict(console_type='fake_type', + public_hostname='fake_hostname') + return dict(id=console_id, password='fake_password', + port='fake_port', pool=pool) + + self.stubs.Set(console.API, 'get_console', fake_get_console) + + req = webob.Request.blank('/v1.1/fake/servers/10/consoles/20.xml') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + + res_tree = etree.fromstring(res.body) + self.assertEqual(res_tree.tag, 'console') + self.assertEqual(res_tree.xpath('id')[0].text, '20') + self.assertEqual(res_tree.xpath('port')[0].text, 'fake_port') + self.assertEqual(res_tree.xpath('host')[0].text, 'fake_hostname') + self.assertEqual(res_tree.xpath('password')[0].text, 'fake_password') + self.assertEqual(res_tree.xpath('console_type')[0].text, + 'fake_type') + def test_show_console_unknown_console(self): def fake_get_console(cons_self, context, instance_id, console_id): raise exception.ConsoleNotFound(console_id=console_id) @@ -188,6 +214,46 @@ class ConsolesTest(test.TestCase): res_dict = json.loads(res.body) self.assertDictMatch(res_dict, expected) + def test_list_consoles_xml(self): + def fake_get_consoles(cons_self, context, instance_id): + self.assertEqual(instance_id, 10) + + pool1 = dict(console_type='fake_type', + public_hostname='fake_hostname') + cons1 = dict(id=10, password='fake_password', + port='fake_port', pool=pool1) + pool2 = dict(console_type='fake_type2', + public_hostname='fake_hostname2') + cons2 = dict(id=11, password='fake_password2', + port='fake_port2', pool=pool2) + return [cons1, cons2] + + expected = {'consoles': + [{'console': {'id': 10, 'console_type': 'fake_type'}}, + {'console': {'id': 11, 'console_type': 'fake_type2'}}]} + + self.stubs.Set(console.API, 'get_consoles', fake_get_consoles) + + req = webob.Request.blank('/v1.1/fake/servers/10/consoles.xml') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + + res_tree = etree.fromstring(res.body) + self.assertEqual(res_tree.tag, 'consoles') + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, 'console') + self.assertEqual(res_tree[1].tag, 'console') + self.assertEqual(len(res_tree[0]), 1) + self.assertEqual(res_tree[0][0].tag, 'console') + self.assertEqual(len(res_tree[1]), 1) + self.assertEqual(res_tree[1][0].tag, 'console') + self.assertEqual(res_tree[0][0].xpath('id')[0].text, '10') + self.assertEqual(res_tree[1][0].xpath('id')[0].text, '11') + self.assertEqual(res_tree[0][0].xpath('console_type')[0].text, + 'fake_type') + self.assertEqual(res_tree[1][0].xpath('console_type')[0].text, + 'fake_type2') + def test_delete_console(self): def fake_get_console(cons_self, context, instance_id, console_id): self.assertEqual(instance_id, 10) diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index e3fe0e878c96..92e74e545936 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -22,6 +22,7 @@ from lxml import etree from nova import context from nova import test +from nova import wsgi as base_wsgi from nova.api import openstack from nova.api.openstack import extensions from nova.api.openstack import flavors @@ -111,8 +112,9 @@ class ExtensionControllerTest(test.TestCase): def test_list_extensions_json(self): app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/123/extensions") - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) # Make sure we have all the extensions. @@ -137,8 +139,9 @@ class ExtensionControllerTest(test.TestCase): def test_get_extension_json(self): app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/123/extensions/FOXNSOX") - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) data = json.loads(response.body) @@ -160,9 +163,10 @@ class ExtensionControllerTest(test.TestCase): def test_list_extensions_xml(self): app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/123/extensions") request.accept = "application/xml" - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) print response.body @@ -187,9 +191,10 @@ class ExtensionControllerTest(test.TestCase): def test_get_extension_xml(self): app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/123/extensions/FOXNSOX") request.accept = "application/xml" - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) xml = response.body print xml @@ -218,8 +223,9 @@ class ResourceExtensionTest(test.TestCase): manager = StubExtensionManager(None) app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/blah") - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(404, response.status_int) def test_get_resources(self): @@ -228,8 +234,9 @@ class ResourceExtensionTest(test.TestCase): manager = StubExtensionManager(res_ext) app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/123/tweedles") - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) self.assertEqual(response_body, response.body) @@ -239,8 +246,9 @@ class ResourceExtensionTest(test.TestCase): manager = StubExtensionManager(res_ext) app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/123/tweedles") - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) self.assertEqual(response_body, response.body) @@ -263,12 +271,15 @@ class ExtensionManagerTest(test.TestCase): def test_get_resources(self): app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/123/foxnsocks") - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) self.assertEqual(response_body, response.body) def test_invalid_extensions(self): + # Don't need the serialization middleware here because we're + # not testing any serialization app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app) ext_mgr = ext_midware.ext_mgr @@ -287,11 +298,12 @@ class ActionExtensionTest(test.TestCase): def _send_server_action_request(self, url, body): app = openstack.APIRouter() ext_midware = extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank(url) request.method = 'POST' request.content_type = 'application/json' request.body = json.dumps(body) - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) return response def test_extended_action(self): @@ -328,11 +340,9 @@ class RequestExtensionTest(test.TestCase): def test_get_resources_with_stub_mgr(self): - def _req_handler(req, res): + def _req_handler(req, res, body): # only handle JSON responses - data = json.loads(res.body) - data['flavor']['googoose'] = req.GET.get('chewing') - res.body = json.dumps(data) + body['flavor']['googoose'] = req.GET.get('chewing') return res req_ext = extensions.RequestExtension('GET', @@ -340,22 +350,24 @@ class RequestExtensionTest(test.TestCase): _req_handler) manager = StubExtensionManager(None, None, req_ext) - app = fakes.wsgi_app() + app = fakes.wsgi_app(serialization=base_wsgi.Middleware) ext_midware = extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/v1.1/123/flavors/1?chewing=bluegoo") request.environ['api.version'] = '1.1' - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) response_data = json.loads(response.body) self.assertEqual('bluegoo', response_data['flavor']['googoose']) def test_get_resources_with_mgr(self): - app = fakes.wsgi_app() + app = fakes.wsgi_app(serialization=base_wsgi.Middleware) ext_midware = extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) request = webob.Request.blank("/v1.1/123/flavors/1?chewing=newblue") request.environ['api.version'] = '1.1' - response = request.get_response(ext_midware) + response = request.get_response(ser_midware) self.assertEqual(200, response.status_int) response_data = json.loads(response.body) self.assertEqual('newblue', response_data['flavor']['googoose']) diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index 51a97f9204f4..96e30f7568aa 100644 --- a/nova/tests/api/openstack/test_limits.py +++ b/nova/tests/api/openstack/test_limits.py @@ -30,6 +30,7 @@ from xml.dom import minidom import nova.context from nova.api.openstack import limits from nova.api.openstack import views +from nova.api.openstack import wsgi from nova.api.openstack import xmlutil from nova import test @@ -80,7 +81,8 @@ class LimitsControllerTest(BaseLimitTestSuite): def setUp(self): """Run before each test.""" BaseLimitTestSuite.setUp(self) - self.controller = limits.create_resource() + self.controller = wsgi.LazySerializationMiddleware( + limits.create_resource()) self.maxDiff = None def _get_index_request(self, accept_header="application/json"): diff --git a/nova/tests/api/openstack/test_users.py b/nova/tests/api/openstack/test_users.py index c9af530cf8d3..cc77d7d26316 100644 --- a/nova/tests/api/openstack/test_users.py +++ b/nova/tests/api/openstack/test_users.py @@ -15,6 +15,7 @@ import json +from lxml import etree import webob from nova import test @@ -63,6 +64,19 @@ class UsersTest(test.TestCase): self.assertEqual(res.status_int, 200) self.assertEqual(len(res_dict['users']), 2) + def test_get_user_list_xml(self): + req = webob.Request.blank('/v1.1/fake/users.xml') + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, 'users') + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, 'user') + self.assertEqual(res_tree[0].get('id'), 'id1') + self.assertEqual(res_tree[1].tag, 'user') + self.assertEqual(res_tree[1].get('id'), 'id2') + def test_get_user_by_id(self): req = webob.Request.blank('/v1.1/fake/users/id2') res = req.get_response(fakes.wsgi_app()) @@ -74,6 +88,18 @@ class UsersTest(test.TestCase): self.assertEqual(res_dict['user']['admin'], True) self.assertEqual(res.status_int, 200) + def test_get_user_by_id_xml(self): + req = webob.Request.blank('/v1.1/fake/users/id2.xml') + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, 'user') + self.assertEqual(res_tree.get('id'), 'id2') + self.assertEqual(res_tree.get('name'), 'guy2') + self.assertEqual(res_tree.get('secret'), 'secret2') + self.assertEqual(res_tree.get('admin'), 'True') + def test_user_delete(self): # Check the user exists req = webob.Request.blank('/v1.1/fake/users/id1') @@ -125,6 +151,35 @@ class UsersTest(test.TestCase): fakes.FakeAuthManager.auth_data]) self.assertEqual(len(fakes.FakeAuthManager.auth_data), 3) + def test_user_create_xml(self): + secret = utils.generate_password() + body = dict(user=dict(name='test_guy', + access='acc3', + secret=secret, + admin=True)) + req = webob.Request.blank('/v1.1/fake/users.xml') + req.headers["Content-Type"] = "application/json" + req.method = 'POST' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + + # NOTE(justinsb): This is a questionable assertion in general + # fake sets id=name, but others might not... + self.assertEqual(res_tree.tag, 'user') + self.assertEqual(res_tree.get('id'), 'test_guy') + + self.assertEqual(res_tree.get('name'), 'test_guy') + self.assertEqual(res_tree.get('access'), 'acc3') + self.assertEqual(res_tree.get('secret'), secret) + self.assertEqual(res_tree.get('admin'), 'True') + self.assertTrue('test_guy' in [u.id for u in + fakes.FakeAuthManager.auth_data]) + self.assertEqual(len(fakes.FakeAuthManager.auth_data), 3) + def test_user_update(self): new_secret = utils.generate_password() body = dict(user=dict(name='guy2', @@ -144,3 +199,24 @@ class UsersTest(test.TestCase): self.assertEqual(res_dict['user']['access'], 'acc2') self.assertEqual(res_dict['user']['secret'], new_secret) self.assertEqual(res_dict['user']['admin'], True) + + def test_user_update_xml(self): + new_secret = utils.generate_password() + body = dict(user=dict(name='guy2', + access='acc2', + secret=new_secret)) + req = webob.Request.blank('/v1.1/fake/users/id2.xml') + req.headers["Content-Type"] = "application/json" + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, 'user') + self.assertEqual(res_tree.get('id'), 'id2') + self.assertEqual(res_tree.get('name'), 'guy2') + self.assertEqual(res_tree.get('access'), 'acc2') + self.assertEqual(res_tree.get('secret'), new_secret) + self.assertEqual(res_tree.get('admin'), 'True') diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py index 74b9ce8531ec..5aea8275d026 100644 --- a/nova/tests/api/openstack/test_wsgi.py +++ b/nova/tests/api/openstack/test_wsgi.py @@ -215,20 +215,23 @@ class RequestHeadersDeserializerTest(test.TestCase): self.assertEqual(deserializer.deserialize(req, 'update'), {'a': 'b'}) +class JSONSerializer(object): + def serialize(self, data, action='default'): + return 'pew_json' + + +class XMLSerializer(object): + def serialize(self, data, action='default'): + return 'pew_xml' + + +class HeadersSerializer(object): + def serialize(self, response, data, action): + response.status_int = 404 + + class ResponseSerializerTest(test.TestCase): def setUp(self): - class JSONSerializer(object): - def serialize(self, data, action='default'): - return 'pew_json' - - class XMLSerializer(object): - def serialize(self, data, action='default'): - return 'pew_xml' - - class HeadersSerializer(object): - def serialize(self, response, data, action): - response.status_int = 404 - self.body_serializers = { 'application/json': JSONSerializer(), 'application/xml': XMLSerializer(), @@ -253,7 +256,8 @@ class ResponseSerializerTest(test.TestCase): def test_serialize_response_json(self): for content_type in ('application/json', 'application/vnd.openstack.compute+json'): - response = self.serializer.serialize({}, content_type) + request = wsgi.Request.blank('/') + response = self.serializer.serialize(request, {}, content_type) self.assertEqual(response.headers['Content-Type'], content_type) self.assertEqual(response.body, 'pew_json') self.assertEqual(response.status_int, 404) @@ -261,21 +265,72 @@ class ResponseSerializerTest(test.TestCase): def test_serialize_response_xml(self): for content_type in ('application/xml', 'application/vnd.openstack.compute+xml'): - response = self.serializer.serialize({}, content_type) + request = wsgi.Request.blank('/') + response = self.serializer.serialize(request, {}, content_type) self.assertEqual(response.headers['Content-Type'], content_type) self.assertEqual(response.body, 'pew_xml') self.assertEqual(response.status_int, 404) def test_serialize_response_None(self): - response = self.serializer.serialize(None, 'application/json') + request = wsgi.Request.blank('/') + response = self.serializer.serialize(request, None, 'application/json') self.assertEqual(response.headers['Content-Type'], 'application/json') self.assertEqual(response.body, '') self.assertEqual(response.status_int, 404) def test_serialize_response_dict_to_unknown_content_type(self): + request = wsgi.Request.blank('/') self.assertRaises(exception.InvalidContentType, self.serializer.serialize, - {}, 'application/unknown') + request, {}, 'application/unknown') + + +class LazySerializationTest(test.TestCase): + def setUp(self): + self.body_serializers = { + 'application/json': JSONSerializer(), + 'application/xml': XMLSerializer(), + } + + self.serializer = wsgi.ResponseSerializer(self.body_serializers, + HeadersSerializer()) + + def tearDown(self): + pass + + def test_serialize_response_json(self): + for content_type in ('application/json', + 'application/vnd.openstack.compute+json'): + request = wsgi.Request.blank('/') + request.environ['nova.lazy_serialize'] = True + response = self.serializer.serialize(request, {}, content_type) + self.assertEqual(response.headers['Content-Type'], content_type) + self.assertEqual(response.status_int, 404) + body = json.loads(response.body) + self.assertEqual(body, {}) + serializer = request.environ['nova.serializer'] + self.assertEqual(serializer.serialize(body), 'pew_json') + + def test_serialize_response_xml(self): + for content_type in ('application/xml', + 'application/vnd.openstack.compute+xml'): + request = wsgi.Request.blank('/') + request.environ['nova.lazy_serialize'] = True + response = self.serializer.serialize(request, {}, content_type) + self.assertEqual(response.headers['Content-Type'], content_type) + self.assertEqual(response.status_int, 404) + body = json.loads(response.body) + self.assertEqual(body, {}) + serializer = request.environ['nova.serializer'] + self.assertEqual(serializer.serialize(body), 'pew_xml') + + def test_serialize_response_None(self): + request = wsgi.Request.blank('/') + request.environ['nova.lazy_serialize'] = True + response = self.serializer.serialize(request, None, 'application/json') + self.assertEqual(response.headers['Content-Type'], 'application/json') + self.assertEqual(response.status_int, 404) + self.assertEqual(response.body, '') class RequestDeserializerTest(test.TestCase): diff --git a/nova/tests/api/openstack/test_xmlutil.py b/nova/tests/api/openstack/test_xmlutil.py new file mode 100644 index 000000000000..6d224967f053 --- /dev/null +++ b/nova/tests/api/openstack/test_xmlutil.py @@ -0,0 +1,763 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +from lxml import etree + +from nova import test +from nova.api.openstack import xmlutil + + +class SelectorTest(test.TestCase): + obj_for_test = { + 'test': { + 'name': 'test', + 'values': [1, 2, 3], + 'attrs': { + 'foo': 1, + 'bar': 2, + 'baz': 3, + }, + }, + } + + def test_empty_selector(self): + sel = xmlutil.Selector() + self.assertEqual(len(sel.chain), 0) + self.assertEqual(sel(self.obj_for_test), self.obj_for_test) + + def test_dict_selector(self): + sel = xmlutil.Selector('test') + self.assertEqual(len(sel.chain), 1) + self.assertEqual(sel.chain[0], 'test') + self.assertEqual(sel(self.obj_for_test), + self.obj_for_test['test']) + + def test_datum_selector(self): + sel = xmlutil.Selector('test', 'name') + self.assertEqual(len(sel.chain), 2) + self.assertEqual(sel.chain[0], 'test') + self.assertEqual(sel.chain[1], 'name') + self.assertEqual(sel(self.obj_for_test), 'test') + + def test_list_selector(self): + sel = xmlutil.Selector('test', 'values', 0) + self.assertEqual(len(sel.chain), 3) + self.assertEqual(sel.chain[0], 'test') + self.assertEqual(sel.chain[1], 'values') + self.assertEqual(sel.chain[2], 0) + self.assertEqual(sel(self.obj_for_test), 1) + + def test_items_selector(self): + sel = xmlutil.Selector('test', 'attrs', xmlutil.get_items) + self.assertEqual(len(sel.chain), 3) + self.assertEqual(sel.chain[2], xmlutil.get_items) + for key, val in sel(self.obj_for_test): + self.assertEqual(self.obj_for_test['test']['attrs'][key], val) + + def test_missing_key_selector(self): + sel = xmlutil.Selector('test2', 'attrs') + self.assertEqual(sel(self.obj_for_test), None) + self.assertRaises(KeyError, sel, self.obj_for_test, True) + + def test_constant_selector(self): + sel = xmlutil.ConstantSelector('Foobar') + self.assertEqual(sel.value, 'Foobar') + self.assertEqual(sel(self.obj_for_test), 'Foobar') + + +class TemplateElementTest(test.TestCase): + def test_element_initial_attributes(self): + # Create a template element with some attributes + elem = xmlutil.TemplateElement('test', attrib=dict(a=1, b=2, c=3), + c=4, d=5, e=6) + + # Verify all the attributes are as expected + expected = dict(a=1, b=2, c=4, d=5, e=6) + for k, v in expected.items(): + self.assertEqual(elem.attrib[k].chain[0], v) + + def test_element_get_attributes(self): + expected = dict(a=1, b=2, c=3) + + # Create a template element with some attributes + elem = xmlutil.TemplateElement('test', attrib=expected) + + # Verify that get() retrieves the attributes + for k, v in expected.items(): + self.assertEqual(elem.get(k).chain[0], v) + + def test_element_set_attributes(self): + attrs = dict(a=None, b='foo', c=xmlutil.Selector('foo', 'bar')) + + # Create a bare template element with no attributes + elem = xmlutil.TemplateElement('test') + + # Set the attribute values + for k, v in attrs.items(): + elem.set(k, v) + + # Now verify what got set + self.assertEqual(len(elem.attrib['a'].chain), 1) + self.assertEqual(elem.attrib['a'].chain[0], 'a') + self.assertEqual(len(elem.attrib['b'].chain), 1) + self.assertEqual(elem.attrib['b'].chain[0], 'foo') + self.assertEqual(elem.attrib['c'], attrs['c']) + + def test_element_attribute_keys(self): + attrs = dict(a=1, b=2, c=3, d=4) + expected = set(attrs.keys()) + + # Create a template element with some attributes + elem = xmlutil.TemplateElement('test', attrib=attrs) + + # Now verify keys + self.assertEqual(set(elem.keys()), expected) + + def test_element_attribute_items(self): + expected = dict(a=xmlutil.Selector(1), + b=xmlutil.Selector(2), + c=xmlutil.Selector(3)) + keys = set(expected.keys()) + + # Create a template element with some attributes + elem = xmlutil.TemplateElement('test', attrib=expected) + + # Now verify items + for k, v in elem.items(): + self.assertEqual(expected[k], v) + keys.remove(k) + + # Did we visit all keys? + self.assertEqual(len(keys), 0) + + def test_element_selector_none(self): + # Create a template element with no selector + elem = xmlutil.TemplateElement('test') + + self.assertEqual(len(elem.selector.chain), 0) + + def test_element_selector_string(self): + # Create a template element with a string selector + elem = xmlutil.TemplateElement('test', selector='test') + + self.assertEqual(len(elem.selector.chain), 1) + self.assertEqual(elem.selector.chain[0], 'test') + + def test_element_selector(self): + sel = xmlutil.Selector('a', 'b') + + # Create a template element with an explicit selector + elem = xmlutil.TemplateElement('test', selector=sel) + + self.assertEqual(elem.selector, sel) + + def test_element_append_child(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Make sure the element starts off empty + self.assertEqual(len(elem), 0) + + # Create a child element + child = xmlutil.TemplateElement('child') + + # Append the child to the parent + elem.append(child) + + # Verify that the child was added + self.assertEqual(len(elem), 1) + self.assertEqual(elem[0], child) + self.assertEqual('child' in elem, True) + self.assertEqual(elem['child'], child) + + # Ensure that multiple children of the same name are rejected + child2 = xmlutil.TemplateElement('child') + self.assertRaises(KeyError, elem.append, child2) + + def test_element_extend_children(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Make sure the element starts off empty + self.assertEqual(len(elem), 0) + + # Create a few children + children = [ + xmlutil.TemplateElement('child1'), + xmlutil.TemplateElement('child2'), + xmlutil.TemplateElement('child3'), + ] + + # Extend the parent by those children + elem.extend(children) + + # Verify that the children were added + self.assertEqual(len(elem), 3) + for idx in range(len(elem)): + self.assertEqual(children[idx], elem[idx]) + self.assertEqual(children[idx].tag in elem, True) + self.assertEqual(elem[children[idx].tag], children[idx]) + + # Ensure that multiple children of the same name are rejected + children2 = [ + xmlutil.TemplateElement('child4'), + xmlutil.TemplateElement('child1'), + ] + self.assertRaises(KeyError, elem.extend, children2) + + # Also ensure that child4 was not added + self.assertEqual(len(elem), 3) + self.assertEqual(elem[-1].tag, 'child3') + + def test_element_insert_child(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Make sure the element starts off empty + self.assertEqual(len(elem), 0) + + # Create a few children + children = [ + xmlutil.TemplateElement('child1'), + xmlutil.TemplateElement('child2'), + xmlutil.TemplateElement('child3'), + ] + + # Extend the parent by those children + elem.extend(children) + + # Create a child to insert + child = xmlutil.TemplateElement('child4') + + # Insert it + elem.insert(1, child) + + # Ensure the child was inserted in the right place + self.assertEqual(len(elem), 4) + children.insert(1, child) + for idx in range(len(elem)): + self.assertEqual(children[idx], elem[idx]) + self.assertEqual(children[idx].tag in elem, True) + self.assertEqual(elem[children[idx].tag], children[idx]) + + # Ensure that multiple children of the same name are rejected + child2 = xmlutil.TemplateElement('child2') + self.assertRaises(KeyError, elem.insert, 2, child2) + + def test_element_remove_child(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Make sure the element starts off empty + self.assertEqual(len(elem), 0) + + # Create a few children + children = [ + xmlutil.TemplateElement('child1'), + xmlutil.TemplateElement('child2'), + xmlutil.TemplateElement('child3'), + ] + + # Extend the parent by those children + elem.extend(children) + + # Create a test child to remove + child = xmlutil.TemplateElement('child2') + + # Try to remove it + self.assertRaises(ValueError, elem.remove, child) + + # Ensure that no child was removed + self.assertEqual(len(elem), 3) + + # Now remove a legitimate child + elem.remove(children[1]) + + # Ensure that the child was removed + self.assertEqual(len(elem), 2) + self.assertEqual(elem[0], children[0]) + self.assertEqual(elem[1], children[2]) + self.assertEqual('child2' in elem, False) + + # Ensure the child cannot be retrieved by name + def get_key(elem, key): + return elem[key] + self.assertRaises(KeyError, get_key, elem, 'child2') + + def test_element_text(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Ensure that it has no text + self.assertEqual(elem.text, None) + + # Try setting it to a string and ensure it becomes a selector + elem.text = 'test' + self.assertEqual(hasattr(elem.text, 'chain'), True) + self.assertEqual(len(elem.text.chain), 1) + self.assertEqual(elem.text.chain[0], 'test') + + # Try resetting the text to None + elem.text = None + self.assertEqual(elem.text, None) + + # Now make up a selector and try setting the text to that + sel = xmlutil.Selector() + elem.text = sel + self.assertEqual(elem.text, sel) + + # Finally, try deleting the text and see what happens + del elem.text + self.assertEqual(elem.text, None) + + def test_apply_attrs(self): + # Create a template element + attrs = dict(attr1=xmlutil.ConstantSelector(1), + attr2=xmlutil.ConstantSelector(2)) + tmpl_elem = xmlutil.TemplateElement('test', attrib=attrs) + + # Create an etree element + elem = etree.Element('test') + + # Apply the template to the element + tmpl_elem.apply(elem, None) + + # Now, verify the correct attributes were set + for k, v in elem.items(): + self.assertEqual(str(attrs[k].value), v) + + def test_apply_text(self): + # Create a template element + tmpl_elem = xmlutil.TemplateElement('test') + tmpl_elem.text = xmlutil.ConstantSelector(1) + + # Create an etree element + elem = etree.Element('test') + + # Apply the template to the element + tmpl_elem.apply(elem, None) + + # Now, verify the text was set + self.assertEqual(str(tmpl_elem.text.value), elem.text) + + def test__render(self): + attrs = dict(attr1=xmlutil.ConstantSelector(1), + attr2=xmlutil.ConstantSelector(2), + attr3=xmlutil.ConstantSelector(3)) + + # Create a master template element + master_elem = xmlutil.TemplateElement('test', attr1=attrs['attr1']) + + # Create a couple of slave template element + slave_elems = [ + xmlutil.TemplateElement('test', attr2=attrs['attr2']), + xmlutil.TemplateElement('test', attr3=attrs['attr3']), + ] + + # Try the render + elem = master_elem._render(None, None, slave_elems, None) + + # Verify the particulars of the render + self.assertEqual(elem.tag, 'test') + self.assertEqual(len(elem.nsmap), 0) + for k, v in elem.items(): + self.assertEqual(str(attrs[k].value), v) + + # Create a parent for the element to be rendered + parent = etree.Element('parent') + + # Try the render again... + elem = master_elem._render(parent, None, slave_elems, dict(a='foo')) + + # Verify the particulars of the render + self.assertEqual(len(parent), 1) + self.assertEqual(parent[0], elem) + self.assertEqual(len(elem.nsmap), 1) + self.assertEqual(elem.nsmap['a'], 'foo') + + def test_render(self): + # Create a template element + tmpl_elem = xmlutil.TemplateElement('test') + tmpl_elem.text = xmlutil.Selector() + + # Create the object we're going to render + obj = ['elem1', 'elem2', 'elem3', 'elem4'] + + # Try a render with no object + elems = tmpl_elem.render(None, None) + self.assertEqual(len(elems), 0) + + # Try a render with one object + elems = tmpl_elem.render(None, 'foo') + self.assertEqual(len(elems), 1) + self.assertEqual(elems[0][0].text, 'foo') + self.assertEqual(elems[0][1], 'foo') + + # Now, try rendering an object with multiple entries + parent = etree.Element('parent') + elems = tmpl_elem.render(parent, obj) + self.assertEqual(len(elems), 4) + + # Check the results + for idx in range(len(obj)): + self.assertEqual(elems[idx][0].text, obj[idx]) + self.assertEqual(elems[idx][1], obj[idx]) + + def test_subelement(self): + # Try the SubTemplateElement constructor + parent = xmlutil.SubTemplateElement(None, 'parent') + self.assertEqual(parent.tag, 'parent') + self.assertEqual(len(parent), 0) + + # Now try it with a parent element + child = xmlutil.SubTemplateElement(parent, 'child') + self.assertEqual(child.tag, 'child') + self.assertEqual(len(parent), 1) + self.assertEqual(parent[0], child) + + def test_wrap(self): + # These are strange methods, but they make things easier + elem = xmlutil.TemplateElement('test') + self.assertEqual(elem.unwrap(), elem) + self.assertEqual(elem.wrap().root, elem) + + def test_dyntag(self): + obj = ['a', 'b', 'c'] + + # Create a template element with a dynamic tag + tmpl_elem = xmlutil.TemplateElement(xmlutil.Selector()) + + # Try the render + parent = etree.Element('parent') + elems = tmpl_elem.render(parent, obj) + + # Verify the particulars of the render + self.assertEqual(len(elems), len(obj)) + for idx in range(len(obj)): + self.assertEqual(elems[idx][0].tag, obj[idx]) + + +class TemplateTest(test.TestCase): + def test_wrap(self): + # These are strange methods, but they make things easier + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.Template(elem) + self.assertEqual(tmpl.unwrap(), elem) + self.assertEqual(tmpl.wrap(), tmpl) + + def test__siblings(self): + # Set up a basic template + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.Template(elem) + + # Check that we get the right siblings + siblings = tmpl._siblings() + self.assertEqual(len(siblings), 1) + self.assertEqual(siblings[0], elem) + + def test__nsmap(self): + # Set up a basic template + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.Template(elem, nsmap=dict(a="foo")) + + # Check out that we get the right namespace dictionary + nsmap = tmpl._nsmap() + self.assertNotEqual(id(nsmap), id(tmpl.nsmap)) + self.assertEqual(len(nsmap), 1) + self.assertEqual(nsmap['a'], 'foo') + + def test_master_attach(self): + # Set up a master template + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.MasterTemplate(elem, 1) + + # Make sure it has a root but no slaves + self.assertEqual(tmpl.root, elem) + self.assertEqual(len(tmpl.slaves), 0) + + # Try to attach an invalid slave + bad_elem = xmlutil.TemplateElement('test2') + self.assertRaises(ValueError, tmpl.attach, bad_elem) + self.assertEqual(len(tmpl.slaves), 0) + + # Try to attach an invalid and a valid slave + good_elem = xmlutil.TemplateElement('test') + self.assertRaises(ValueError, tmpl.attach, good_elem, bad_elem) + self.assertEqual(len(tmpl.slaves), 0) + + # Try to attach an inapplicable template + class InapplicableTemplate(xmlutil.Template): + def apply(self, master): + return False + inapp_tmpl = InapplicableTemplate(good_elem) + tmpl.attach(inapp_tmpl) + self.assertEqual(len(tmpl.slaves), 0) + + # Now try attaching an applicable template + tmpl.attach(good_elem) + self.assertEqual(len(tmpl.slaves), 1) + self.assertEqual(tmpl.slaves[0].root, good_elem) + + def test_master_copy(self): + # Construct a master template + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.MasterTemplate(elem, 1, nsmap=dict(a='foo')) + + # Give it a slave + slave = xmlutil.TemplateElement('test') + tmpl.attach(slave) + + # Construct a copy + copy = tmpl.copy() + + # Check to see if we actually managed a copy + self.assertNotEqual(tmpl, copy) + self.assertEqual(tmpl.root, copy.root) + self.assertEqual(tmpl.version, copy.version) + self.assertEqual(id(tmpl.nsmap), id(copy.nsmap)) + self.assertNotEqual(id(tmpl.slaves), id(copy.slaves)) + self.assertEqual(len(tmpl.slaves), len(copy.slaves)) + self.assertEqual(tmpl.slaves[0], copy.slaves[0]) + + def test_slave_apply(self): + # Construct a master template + elem = xmlutil.TemplateElement('test') + master = xmlutil.MasterTemplate(elem, 3) + + # Construct a slave template with applicable minimum version + slave = xmlutil.SlaveTemplate(elem, 2) + self.assertEqual(slave.apply(master), True) + + # Construct a slave template with equal minimum version + slave = xmlutil.SlaveTemplate(elem, 3) + self.assertEqual(slave.apply(master), True) + + # Construct a slave template with inapplicable minimum version + slave = xmlutil.SlaveTemplate(elem, 4) + self.assertEqual(slave.apply(master), False) + + # Construct a slave template with applicable version range + slave = xmlutil.SlaveTemplate(elem, 2, 4) + self.assertEqual(slave.apply(master), True) + + # Construct a slave template with low version range + slave = xmlutil.SlaveTemplate(elem, 1, 2) + self.assertEqual(slave.apply(master), False) + + # Construct a slave template with high version range + slave = xmlutil.SlaveTemplate(elem, 4, 5) + self.assertEqual(slave.apply(master), False) + + # Construct a slave template with matching version range + slave = xmlutil.SlaveTemplate(elem, 3, 3) + self.assertEqual(slave.apply(master), True) + + def test__serialize(self): + # Our test object to serialize + obj = { + 'test': { + 'name': 'foobar', + 'values': [1, 2, 3, 4], + 'attrs': { + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + }, + 'image': { + 'name': 'image_foobar', + 'id': 42, + }, + }, + } + + # Set up our master template + root = xmlutil.TemplateElement('test', selector='test', + name='name') + value = xmlutil.SubTemplateElement(root, 'value', selector='values') + value.text = xmlutil.Selector() + attrs = xmlutil.SubTemplateElement(root, 'attrs', selector='attrs') + xmlutil.SubTemplateElement(attrs, 'attr', selector=xmlutil.get_items, + key=0, value=1) + master = xmlutil.MasterTemplate(root, 1, nsmap=dict(f='foo')) + + # Set up our slave template + root_slave = xmlutil.TemplateElement('test', selector='test') + image = xmlutil.SubTemplateElement(root_slave, 'image', + selector='image', id='id') + image.text = xmlutil.Selector('name') + slave = xmlutil.SlaveTemplate(root_slave, 1, nsmap=dict(b='bar')) + + # Attach the slave to the master... + master.attach(slave) + + # Try serializing our object + siblings = master._siblings() + nsmap = master._nsmap() + result = master._serialize(None, obj, siblings, nsmap) + + # Now we get to manually walk the element tree... + self.assertEqual(result.tag, 'test') + self.assertEqual(len(result.nsmap), 2) + self.assertEqual(result.nsmap['f'], 'foo') + self.assertEqual(result.nsmap['b'], 'bar') + self.assertEqual(result.get('name'), obj['test']['name']) + for idx, val in enumerate(obj['test']['values']): + self.assertEqual(result[idx].tag, 'value') + self.assertEqual(result[idx].text, str(val)) + idx += 1 + self.assertEqual(result[idx].tag, 'attrs') + for attr in result[idx]: + self.assertEqual(attr.tag, 'attr') + self.assertEqual(attr.get('value'), + str(obj['test']['attrs'][attr.get('key')])) + idx += 1 + self.assertEqual(result[idx].tag, 'image') + self.assertEqual(result[idx].get('id'), + str(obj['test']['image']['id'])) + self.assertEqual(result[idx].text, obj['test']['image']['name']) + + +class MasterTemplateBuilder(xmlutil.TemplateBuilder): + def construct(self): + elem = xmlutil.TemplateElement('test') + return xmlutil.MasterTemplate(elem, 1) + + +class SlaveTemplateBuilder(xmlutil.TemplateBuilder): + def construct(self): + elem = xmlutil.TemplateElement('test') + return xmlutil.SlaveTemplate(elem, 1) + + +class TemplateBuilderTest(test.TestCase): + def test_master_template_builder(self): + # Make sure the template hasn't been built yet + self.assertEqual(MasterTemplateBuilder._tmpl, None) + + # Now, construct the template + tmpl1 = MasterTemplateBuilder() + + # Make sure that there is a template cached... + self.assertNotEqual(MasterTemplateBuilder._tmpl, None) + + # Make sure it wasn't what was returned... + self.assertNotEqual(MasterTemplateBuilder._tmpl, tmpl1) + + # Make sure it doesn't get rebuilt + cached = MasterTemplateBuilder._tmpl + tmpl2 = MasterTemplateBuilder() + self.assertEqual(MasterTemplateBuilder._tmpl, cached) + + # Make sure we're always getting fresh copies + self.assertNotEqual(tmpl1, tmpl2) + + # Make sure we can override the copying behavior + tmpl3 = MasterTemplateBuilder(False) + self.assertEqual(MasterTemplateBuilder._tmpl, tmpl3) + + def test_slave_template_builder(self): + # Make sure the template hasn't been built yet + self.assertEqual(SlaveTemplateBuilder._tmpl, None) + + # Now, construct the template + tmpl1 = SlaveTemplateBuilder() + + # Make sure there is a template cached... + self.assertNotEqual(SlaveTemplateBuilder._tmpl, None) + + # Make sure it was what was returned... + self.assertEqual(SlaveTemplateBuilder._tmpl, tmpl1) + + # Make sure it doesn't get rebuilt + tmpl2 = SlaveTemplateBuilder() + self.assertEqual(SlaveTemplateBuilder._tmpl, tmpl1) + + # Make sure we're always getting the cached copy + self.assertEqual(tmpl1, tmpl2) + + +class SerializerTest(xmlutil.XMLTemplateSerializer): + def test(self): + root = xmlutil.TemplateElement('servers') + a = xmlutil.SubTemplateElement(root, 'a', selector='servers') + a.text = xmlutil.Selector('a') + return xmlutil.MasterTemplate(root, 1, nsmap={None: "asdf"}) + + +class XMLTemplateSerializerTest(test.TestCase): + def setUp(self): + self.tmpl_serializer = SerializerTest() + self.data = dict(servers=dict(a=(2, 3))) + self.data_multi = dict(servers=[dict(a=(2, 3)), dict(a=(3, 4))]) + super(XMLTemplateSerializerTest, self).setUp() + + def test_get_template(self): + # First, check what happens when we fall back on the default + # option + self.assertEqual(self.tmpl_serializer.get_template(), None) + self.assertEqual(self.tmpl_serializer.get_template('nosuch'), None) + + # Now, check that we get back a template + tmpl = self.tmpl_serializer.get_template('test') + self.assertNotEqual(tmpl, None) + self.assertEqual(tmpl.root.tag, 'servers') + + def test_serialize_default(self): + expected_xml = '(2,3)' + result = self.tmpl_serializer.serialize(self.data) + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(result, expected_xml) + + def test_serialize_multi_default(self): + expected_xml = ('(2,3)' + '(3,4)') + result = self.tmpl_serializer.serialize(self.data_multi) + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(result, expected_xml) + + def test_serialize_explicit(self): + expected_xml = ("" + '(2,3)') + tmpl = self.tmpl_serializer.get_template('test') + result = self.tmpl_serializer.serialize(self.data, template=tmpl) + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(result, expected_xml) + + def test_serialize_multi_explicit(self): + expected_xml = ("" + '(2,3)' + '(3,4)') + tmpl = self.tmpl_serializer.get_template('test') + result = self.tmpl_serializer.serialize(self.data_multi, template=tmpl) + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(result, expected_xml) + + def test_serialize(self): + expected_xml = ("" + '(2,3)') + result = self.tmpl_serializer.serialize(self.data, 'test') + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(result, expected_xml) + + def test_serialize_multi(self): + expected_xml = ("" + '(2,3)' + '(3,4)') + result = self.tmpl_serializer.serialize(self.data_multi, 'test') + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(result, expected_xml) diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py index 869f13183764..af762d3d6f05 100644 --- a/nova/tests/api/openstack/test_zones.py +++ b/nova/tests/api/openstack/test_zones.py @@ -14,15 +14,18 @@ # under the License. +import json + +from lxml import etree import stubout import webob -import json import nova.db from nova import context from nova import crypto from nova import flags from nova import test +from nova.api.openstack import xmlutil from nova.api.openstack import zones from nova.tests.api.openstack import fakes from nova.scheduler import api @@ -112,6 +115,18 @@ class ZonesTest(test.TestCase): self.assertEqual(res.status_int, 200) self.assertEqual(len(res_dict['zones']), 2) + def test_get_zone_list_scheduler_xml(self): + self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler) + req = webob.Request.blank('/v1.1/fake/zones.xml') + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, '{%s}zones' % xmlutil.XMLNS_V10) + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, '{%s}zone' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree[1].tag, '{%s}zone' % xmlutil.XMLNS_V10) + def test_get_zone_list_db(self): self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler_empty) self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db) @@ -123,6 +138,20 @@ class ZonesTest(test.TestCase): res_dict = json.loads(res.body) self.assertEqual(len(res_dict['zones']), 2) + def test_get_zone_list_db_xml(self): + self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler_empty) + self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db) + req = webob.Request.blank('/v1.1/fake/zones.xml') + req.headers["Content-Type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 200) + res_tree = etree.fromstring(res.body) + self.assertEqual(res_tree.tag, '{%s}zones' % xmlutil.XMLNS_V10) + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, '{%s}zone' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree[1].tag, '{%s}zone' % xmlutil.XMLNS_V10) + def test_get_zone_by_id(self): req = webob.Request.blank('/v1.1/fake/zones/1') req.headers["Content-Type"] = "application/json" @@ -134,6 +163,18 @@ class ZonesTest(test.TestCase): self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') self.assertFalse('password' in res_dict['zone']) + def test_get_zone_by_id_xml(self): + req = webob.Request.blank('/v1.1/fake/zones/1.xml') + req.headers["Content-Type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, '{%s}zone' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('id'), '1') + self.assertEqual(res_tree.get('api_url'), 'http://example.com') + self.assertEqual(res_tree.get('password'), None) + def test_zone_delete(self): req = webob.Request.blank('/v1.1/fake/zones/1') req.headers["Content-Type"] = "application/json" @@ -157,6 +198,23 @@ class ZonesTest(test.TestCase): self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') self.assertFalse('username' in res_dict['zone']) + def test_zone_create_xml(self): + body = dict(zone=dict(api_url='http://example.com', username='fred', + password='fubar')) + req = webob.Request.blank('/v1.1/fake/zones.xml') + req.headers["Content-Type"] = "application/json" + req.method = 'POST' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 200) + res_tree = etree.fromstring(res.body) + self.assertEqual(res_tree.tag, '{%s}zone' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('id'), '1') + self.assertEqual(res_tree.get('api_url'), 'http://example.com') + self.assertEqual(res_tree.get('username'), None) + def test_zone_update(self): body = dict(zone=dict(username='zeb', password='sneaky')) req = webob.Request.blank('/v1.1/fake/zones/1') @@ -172,6 +230,22 @@ class ZonesTest(test.TestCase): self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') self.assertFalse('username' in res_dict['zone']) + def test_zone_update_xml(self): + body = dict(zone=dict(username='zeb', password='sneaky')) + req = webob.Request.blank('/v1.1/fake/zones/1.xml') + req.headers["Content-Type"] = "application/json" + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 200) + res_tree = etree.fromstring(res.body) + self.assertEqual(res_tree.tag, '{%s}zone' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('id'), '1') + self.assertEqual(res_tree.get('api_url'), 'http://example.com') + self.assertEqual(res_tree.get('username'), None) + def test_zone_info(self): caps = ['cap1=a;b', 'cap2=c;d'] self.flags(zone_name='darksecret', zone_capabilities=caps) @@ -187,6 +261,28 @@ class ZonesTest(test.TestCase): self.assertEqual(res_dict['zone']['cap1'], 'a;b') self.assertEqual(res_dict['zone']['cap2'], 'c;d') + def test_zone_info_xml(self): + caps = ['cap1=a;b', 'cap2=c;d'] + self.flags(zone_name='darksecret', zone_capabilities=caps) + self.stubs.Set(api, '_call_scheduler', zone_capabilities) + + body = dict(zone=dict(username='zeb', password='sneaky')) + req = webob.Request.blank('/v1.1/fake/zones/info.xml') + + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, '{%s}zone' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('name'), 'darksecret') + for elem in res_tree: + self.assertEqual(elem.tag in ('{%s}cap1' % xmlutil.XMLNS_V10, + '{%s}cap2' % xmlutil.XMLNS_V10), + True) + if elem.tag == '{%s}cap1' % xmlutil.XMLNS_V10: + self.assertEqual(elem.text, 'a;b') + elif elem.tag == '{%s}cap2' % xmlutil.XMLNS_V10: + self.assertEqual(elem.text, 'c;d') + def test_zone_select(self): key = 'c286696d887c9aa0611bbb3e2025a45a' self.flags(build_plan_encryption_key=key) @@ -220,3 +316,45 @@ class ZonesTest(test.TestCase): self.assertTrue(found) self.assertEqual(len(item), 2) self.assertTrue('weight' in item) + + def test_zone_select_xml(self): + key = 'c286696d887c9aa0611bbb3e2025a45a' + self.flags(build_plan_encryption_key=key) + self.stubs.Set(api, 'select', zone_select) + + req = webob.Request.blank('/v1.1/fake/zones/select.xml') + req.method = 'POST' + req.headers["Content-Type"] = "application/json" + # Select queries end up being JSON encoded twice. + # Once to a string and again as an HTTP POST Body + req.body = json.dumps(json.dumps({})) + + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + self.assertEqual(res.status_int, 200) + + self.assertEqual(res_tree.tag, '{%s}weights' % xmlutil.XMLNS_V10) + + for item in res_tree: + self.assertEqual(item.tag, '{%s}weight' % xmlutil.XMLNS_V10) + blob = None + weight = None + for chld in item: + if chld.tag.endswith('blob'): + blob = chld.text + elif chld.tag.endswith('weight'): + weight = chld.text + + decrypt = crypto.decryptor(FLAGS.build_plan_encryption_key) + secret_item = json.loads(decrypt(blob)) + found = False + for original_item in GLOBAL_BUILD_PLAN: + if original_item['name'] != secret_item['name']: + continue + found = True + for key in ('weight', 'ip', 'zone'): + self.assertEqual(secret_item[key], original_item[key]) + + self.assertTrue(found) + self.assertEqual(len(item), 2) + self.assertTrue(weight)