diff options
| author | Soren Hansen <soren@linux2go.dk> | 2011-09-16 17:17:34 +0200 |
|---|---|---|
| committer | Soren Hansen <soren@linux2go.dk> | 2011-09-16 17:17:34 +0200 |
| commit | 322f5458236a64bc6d12ba29cee6750d452f1ecd (patch) | |
| tree | 9549137acf4c12c7ebeb7e0a0b5fa7a1449ec7ed | |
| parent | 1d2a5bd706736bff2e2d82ebeb813438493cea41 (diff) | |
| parent | 23fefd17727966b0a1397f40b25bedcd668206d7 (diff) | |
| download | nova-322f5458236a64bc6d12ba29cee6750d452f1ecd.tar.gz nova-322f5458236a64bc6d12ba29cee6750d452f1ecd.tar.xz nova-322f5458236a64bc6d12ba29cee6750d452f1ecd.zip | |
Merge trunk
132 files changed, 6401 insertions, 4543 deletions
@@ -15,6 +15,7 @@ <code@term.ie> <termie@preciousroy.local> <corywright@gmail.com> <cory.wright@rackspace.com> <dan@nicira.com> <danwent@dan-xs3-cs> +<dan@nicira.com> danwent@gmail.com <devin.carlen@gmail.com> <devcamcar@illian.local> <ewan.mellor@citrix.com> <emellor@silver> <itoumsn@nttdata.co.jp> <itoumsn@shayol> @@ -11,6 +11,7 @@ Antony Messerli <ant@openstack.org> Armando Migliaccio <Armando.Migliaccio@eu.citrix.com> Arvind Somya <asomya@cisco.com> Bilal Akhtar <bilalakhtar@ubuntu.com> +Brad Hall <brad@nicira.com> Brian Lamar <brian.lamar@rackspace.com> Brian Schott <bschott@isi.edu> Brian Waldon <brian.waldon@rackspace.com> @@ -30,6 +31,7 @@ Devendra Modium <dmodium@isi.edu> Devin Carlen <devin.carlen@gmail.com> Donal Lafferty <donal.lafferty@citrix.com> Ed Leafe <ed@leafe.com> +Edouard Thuleau <thuleau@gmail.com> Eldar Nugaev <reldan@oscloud.ru> Eric Day <eday@oddments.org> Eric Windisch <eric@cloudscaling.com> @@ -60,6 +62,7 @@ Joshua McKenty <jmckenty@gmail.com> Justin Santa Barbara <justin@fathomdb.com> Justin Shepherd <jshepher@rackspace.com> Kei Masumoto <masumotok@nttdata.co.jp> +Keisuke Tagami <tagami.keisuke@lab.ntt.co.jp> masumoto<masumotok@nttdata.co.jp> Ken Pepple <ken.pepple@gmail.com> Kevin Bringard <kbringard@attinteractive.com> diff --git a/MANIFEST.in b/MANIFEST.in index 883aba8a1..5451ace4b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -37,7 +37,7 @@ include nova/tests/bundle/1mb.manifest.xml include nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml include nova/tests/bundle/1mb.part.0 include nova/tests/bundle/1mb.part.1 -include nova/tests/public_key/* +include nova/tests/api/ec2/public_key/* include nova/tests/db/nova.austin.sqlite include plugins/xenapi/README include plugins/xenapi/etc/xapi.d/plugins/objectstore diff --git a/bin/nova-manage b/bin/nova-manage index c3b2c71ce..089b2eeae 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -59,11 +59,11 @@ import glob import json import math import netaddr +from optparse import OptionParser import os import sys import time -from optparse import OptionParser # If ../nova/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... @@ -685,10 +685,17 @@ class NetworkCommands(object): help='Multi host') @args('--dns1', dest="dns1", metavar="<DNS Address>", help='First DNS') @args('--dns2', dest="dns2", metavar="<DNS Address>", help='Second DNS') + @args('--uuid', dest="net_uuid", metavar="<network uuid>", + help='Network UUID') + @args('--project_id', dest="project_id", metavar="<project id>", + help='Project id') + @args('--priority', dest="priority", metavar="<number>", + help='Network interface priority') def create(self, label=None, fixed_range_v4=None, num_networks=None, network_size=None, multi_host=None, vlan_start=None, vpn_start=None, fixed_range_v6=None, gateway_v6=None, - bridge=None, bridge_interface=None, dns1=None, dns2=None): + bridge=None, bridge_interface=None, dns1=None, dns2=None, + project_id=None, priority=None, uuid=None): """Creates fixed ips for host by range""" # check for certain required inputs @@ -717,8 +724,7 @@ class NetworkCommands(object): bridge_interface = bridge_interface or FLAGS.flat_interface or \ FLAGS.vlan_interface if not bridge_interface: - interface_required = ['nova.network.manager.FlatDHCPManager', - 'nova.network.manager.VlanManager'] + interface_required = ['nova.network.manager.VlanManager'] if FLAGS.network_manager in interface_required: raise exception.NetworkNotCreated(req='--bridge_interface') @@ -765,7 +771,10 @@ class NetworkCommands(object): bridge=bridge, bridge_interface=bridge_interface, dns1=dns1, - dns2=dns2) + dns2=dns2, + project_id=project_id, + priority=priority, + uuid=uuid) def list(self): """List all created networks""" @@ -790,16 +799,29 @@ class NetworkCommands(object): network.project_id, network.uuid) + def quantum_list(self): + """List all created networks with Quantum-relevant fields""" + _fmt = "%-32s\t%-10s\t%-10s\t%s , %s" + print _fmt % (_('uuid'), + _('project'), + _('priority'), + _('cidr_v4'), + _('cidr_v6')) + for network in db.network_get_all(context.get_admin_context()): + print _fmt % (network.uuid, + network.project_id, + network.priority, + network.cidr, + network.cidr_v6) + @args('--network', dest="fixed_range", metavar='<x.x.x.x/yy>', help='Network to delete') def delete(self, fixed_range): """Deletes a network""" - network = db.network_get_by_cidr(context.get_admin_context(), \ - fixed_range) - if network.project_id is not None: - raise ValueError(_('Network must be disassociated from project %s' - ' before delete' % network.project_id)) - db.network_delete_safe(context.get_admin_context(), network.id) + + # delete the network + net_manager = utils.import_object(FLAGS.network_manager) + net_manager.delete_network(context.get_admin_context(), fixed_range) @args('--network', dest="fixed_range", metavar='<x.x.x.x/yy>', help='Network to modify') diff --git a/bin/nova-vncproxy b/bin/nova-vncproxy index dc08e2433..8e75451cb 100755 --- a/bin/nova-vncproxy +++ b/bin/nova-vncproxy @@ -107,10 +107,13 @@ if __name__ == "__main__": else: with_auth = auth.VNCNovaAuthMiddleware(with_logging) - server = wsgi.Server("VNC Proxy", - with_auth, - host=FLAGS.vncproxy_host, - port=FLAGS.vncproxy_port) - server.start_tcp(handle_flash_socket_policy, 843, host=FLAGS.vncproxy_host) - service.serve(server) + wsgi_server = wsgi.Server("VNC Proxy", + with_auth, + host=FLAGS.vncproxy_host, + port=FLAGS.vncproxy_port) + wsgi_server.start_tcp(handle_flash_socket_policy, + 843, + host=FLAGS.vncproxy_host) + server = service.Service.create(binary='nova-vncproxy') + service.serve(wsgi_server, server) service.wait() diff --git a/nova/api/auth.py b/nova/api/auth.py index cd0d38b3f..f73cae01e 100644 --- a/nova/api/auth.py +++ b/nova/api/auth.py @@ -70,6 +70,7 @@ class KeystoneContext(wsgi.Middleware): project_id, roles=roles, auth_token=auth_token, + strategy='keystone', remote_address=remote_address) req.environ['nova.context'] = ctx diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 5c876a3b7..23ac30494 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -272,11 +272,23 @@ class CloudController(object): mappings = {} mappings['ami'] = block_device.strip_dev(root_device_name) mappings['root'] = root_device_name - - # 'ephemeralN' and 'swap' + default_local_device = instance_ref.get('default_local_device') + if default_local_device: + mappings['ephemeral0'] = default_local_device + default_swap_device = instance_ref.get('default_swap_device') + if default_swap_device: + mappings['swap'] = default_swap_device + ebs_devices = [] + + # 'ephemeralN', 'swap' and ebs for bdm in db.block_device_mapping_get_all_by_instance( ctxt, instance_ref['id']): - if (bdm['volume_id'] or bdm['snapshot_id'] or bdm['no_device']): + if bdm['no_device']: + continue + + # ebs volume case + if (bdm['volume_id'] or bdm['snapshot_id']): + ebs_devices.append(bdm['device_name']) continue virtual_name = bdm['virtual_name'] @@ -286,6 +298,16 @@ class CloudController(object): if block_device.is_swap_or_ephemeral(virtual_name): mappings[virtual_name] = bdm['device_name'] + # NOTE(yamahata): I'm not sure how ebs device should be numbered. + # Right now sort by device name for deterministic + # result. + if ebs_devices: + nebs = 0 + ebs_devices.sort() + for ebs in ebs_devices: + mappings['ebs%d' % nebs] = ebs + nebs += 1 + return mappings def get_metadata(self, address): @@ -304,11 +326,6 @@ class CloudController(object): instance_ref = db.instance_get(ctxt, instance_ref[0]['id']) mpi = self._get_mpi_data(ctxt, instance_ref['project_id']) - if instance_ref['key_name']: - keys = {'0': {'_name': instance_ref['key_name'], - 'openssh-key': instance_ref['key_data']}} - else: - keys = '' hostname = instance_ref['hostname'] host = instance_ref['host'] availability_zone = self._get_availability_zone_by_host(ctxt, host) @@ -336,11 +353,16 @@ class CloudController(object): 'placement': {'availability-zone': availability_zone}, 'public-hostname': hostname, 'public-ipv4': floating_ip or '', - 'public-keys': keys, 'reservation-id': instance_ref['reservation_id'], 'security-groups': security_groups, 'mpi': mpi}} + # public-keys should be in meta-data only if user specified one + if instance_ref['key_name']: + data['meta-data']['public-keys'] = { + '0': {'_name': instance_ref['key_name'], + 'openssh-key': instance_ref['key_data']}} + for image_type in ['kernel', 'ramdisk']: if instance_ref.get('%s_id' % image_type): ec2_id = self.image_ec2_id(instance_ref['%s_id' % image_type], @@ -1033,14 +1055,6 @@ class CloudController(object): 'status': volume['attach_status'], 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)} - @staticmethod - def _convert_to_set(lst, label): - if lst is None or lst == []: - return None - if not isinstance(lst, list): - lst = [lst] - return [{label: x} for x in lst] - def _format_kernel_id(self, instance_ref, result, key): kernel_id = instance_ref['kernel_id'] if kernel_id is None: @@ -1199,7 +1213,7 @@ class CloudController(object): if instance.get('security_groups'): for security_group in instance['security_groups']: security_group_names.append(security_group['name']) - result['groupSet'] = CloudController._convert_to_set( + result['groupSet'] = utils.convert_to_list_dict( security_group_names, 'groupId') def _format_instances(self, context, instance_id=None, use_v6=False, @@ -1221,8 +1235,10 @@ class CloudController(object): instances.append(instance) else: try: + # always filter out deleted instances + search_opts['deleted'] = False instances = self.compute_api.get_all(context, - search_opts=search_opts) + search_opts=search_opts) except exception.NotFound: instances = [] for instance in instances: @@ -1263,7 +1279,8 @@ class CloudController(object): i['keyName'] = '%s (%s, %s)' % (i['keyName'], instance['project_id'], instance['host']) - i['productCodesSet'] = self._convert_to_set([], 'product_codes') + i['productCodesSet'] = utils.convert_to_list_dict([], + 'product_codes') self._format_instance_type(instance, i) i['launchTime'] = instance['created_at'] i['amiLaunchIndex'] = instance['launch_index'] @@ -1485,7 +1502,7 @@ class CloudController(object): return image def _format_image(self, image): - """Convert from format defined by BaseImageService to S3 format.""" + """Convert from format defined by GlanceImageService to S3 format.""" i = {} image_type = self._image_type(image.get('container_format')) ec2_id = self.image_ec2_id(image.get('id'), image_type) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index d743a66ef..ca7848678 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -16,6 +16,7 @@ # under the License. import functools +from lxml import etree import re import urlparse from xml.dom import minidom @@ -27,6 +28,7 @@ from nova import flags from nova import log as logging from nova import quota from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil from nova.compute import vm_states from nova.compute import task_states @@ -185,30 +187,16 @@ def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit): def get_id_from_href(href): - """Return the id portion of a url as an int. + """Return the id or uuid portion of a url. Given: 'http://www.foo.com/bar/123?q=4' - Returns: 123 + Returns: '123' - In order to support local hrefs, the href argument can be just an id: - Given: '123' - Returns: 123 + Given: 'http://www.foo.com/bar/abc123?q=4' + Returns: 'abc123' """ - LOG.debug(_("Attempting to treat %(href)s as an integer ID.") % locals()) - - try: - return int(href) - except ValueError: - pass - - LOG.debug(_("Attempting to treat %(href)s as a URL.") % locals()) - - try: - return int(urlparse.urlsplit(href).path.split('/')[-1]) - except ValueError as error: - LOG.debug(_("Failed to parse ID from %(href)s: %(error)s") % locals()) - raise + return urlparse.urlsplit("%s" % href).path.split('/')[-1] def remove_version_from_href(href): @@ -308,54 +296,48 @@ class MetadataHeadersSerializer(wsgi.ResponseHeadersSerializer): class MetadataXMLSerializer(wsgi.XMLDictSerializer): + + NSMAP = {None: xmlutil.XMLNS_V11} + def __init__(self, xmlns=wsgi.XMLNS_V11): super(MetadataXMLSerializer, self).__init__(xmlns=xmlns) - def _meta_item_to_xml(self, doc, key, value): - node = doc.createElement('meta') - doc.appendChild(node) - node.setAttribute('key', '%s' % key) - text = doc.createTextNode('%s' % value) - node.appendChild(text) - return node - - def meta_list_to_xml(self, xml_doc, meta_items): - container_node = xml_doc.createElement('metadata') - for (key, value) in meta_items: - item_node = self._meta_item_to_xml(xml_doc, key, value) - container_node.appendChild(item_node) - return container_node - - def _meta_list_to_xml_string(self, metadata_dict): - xml_doc = minidom.Document() - items = metadata_dict['metadata'].items() - container_node = self.meta_list_to_xml(xml_doc, items) - xml_doc.appendChild(container_node) - self._add_xmlns(container_node) - return xml_doc.toxml('UTF-8') + 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 def index(self, metadata_dict): - return self._meta_list_to_xml_string(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): - return self._meta_list_to_xml_string(metadata_dict) + metadata = etree.Element('metadata', nsmap=self.NSMAP) + self.populate_metadata(metadata, metadata_dict.get('metadata', {})) + return self._to_xml(metadata) def update_all(self, metadata_dict): - return self._meta_list_to_xml_string(metadata_dict) - - def _meta_item_to_xml_string(self, meta_item_dict): - xml_doc = minidom.Document() - item_key, item_value = meta_item_dict.items()[0] - item_node = self._meta_item_to_xml(xml_doc, item_key, item_value) - xml_doc.appendChild(item_node) - self._add_xmlns(item_node) - return xml_doc.toxml('UTF-8') + 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): - return self._meta_item_to_xml_string(meta_item_dict['meta']) + meta = etree.Element('meta', nsmap=self.NSMAP) + self._populate_meta_item(meta, meta_item_dict['meta']) + return self._to_xml(meta) def update(self, meta_item_dict): - return self._meta_item_to_xml_string(meta_item_dict['meta']) + meta = etree.Element('meta', nsmap=self.NSMAP) + self._populate_meta_item(meta, meta_item_dict['meta']) + return self._to_xml(meta) def default(self, *args, **kwargs): return '' diff --git a/nova/api/openstack/contrib/createserverext.py b/nova/api/openstack/contrib/createserverext.py index ba72fdb0b..af7f37f13 100644 --- a/nova/api/openstack/contrib/createserverext.py +++ b/nova/api/openstack/contrib/createserverext.py @@ -14,18 +14,34 @@ # License for the specific language governing permissions and limitations # under the License +from nova import utils from nova.api.openstack import create_instance_helper as helper from nova.api.openstack import extensions from nova.api.openstack import servers from nova.api.openstack import wsgi -class Createserverext(extensions.ExtensionDescriptor): - """The servers create ext +class CreateServerController(servers.ControllerV11): + def _build_view(self, req, instance, is_detail=False): + server = super(CreateServerController, self)._build_view(req, + instance, + is_detail) + if is_detail: + self._build_security_groups(server['server'], instance) + return server + + def _build_security_groups(self, response, inst): + sg_names = [] + sec_groups = inst.get('security_groups') + if sec_groups: + sg_names = [sec_group['name'] for sec_group in sec_groups] - Exposes addFixedIp and removeFixedIp actions on servers. + response['security_groups'] = utils.convert_to_list_dict(sg_names, + 'name') - """ + +class Createserverext(extensions.ExtensionDescriptor): + """The servers create ext""" def get_name(self): return "Createserverext" @@ -58,7 +74,7 @@ class Createserverext(extensions.ExtensionDescriptor): deserializer = wsgi.RequestDeserializer(body_deserializers) res = extensions.ResourceExtension('os-create-server-ext', - controller=servers.ControllerV11(), + controller=CreateServerController(), deserializer=deserializer, serializer=serializer) resources.append(res) diff --git a/nova/api/openstack/contrib/flavorextradata.py b/nova/api/openstack/contrib/flavorextradata.py new file mode 100644 index 000000000..d0554c7b4 --- /dev/null +++ b/nova/api/openstack/contrib/flavorextradata.py @@ -0,0 +1,46 @@ +# Copyright 2011 Canonical Ltd. +# 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. + +""" +The Flavor extra data extension +Openstack API version 1.1 lists "name", "ram", "disk", "vcpus" as flavor +attributes. This extension adds to that list: + rxtx_cap + rxtx_quota + swap +""" + +from nova.api.openstack import extensions + + +class Flavorextradata(extensions.ExtensionDescriptor): + """The Flavor extra data extension for the OpenStack API.""" + + def get_name(self): + return "FlavorExtraData" + + def get_alias(self): + return "os-flavor-extra-data" + + def get_description(self): + return "Provide additional data for flavors" + + def get_namespace(self): + return "http://docs.openstack.org/ext/flavor_extra_data/api/v1.1" + + def get_updated(self): + return "2011-09-14T00:00:00+00:00" + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index d1add8f83..d078b26c6 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -107,7 +107,7 @@ class FloatingIPController(object): context = req.environ['nova.context'] floating_ip = self.network_api.get_floating_ip(context, id) - if 'fixed_ip' in floating_ip: + if floating_ip.get('fixed_ip'): self.network_api.disassociate_floating_ip(context, floating_ip['address']) @@ -161,7 +161,7 @@ class Floating_ips(extensions.ExtensionDescriptor): raise webob.exc.HTTPBadRequest(explanation=msg) floating_ip = self.network_api.get_floating_ip_by_ip(context, address) - if 'fixed_ip' in floating_ip: + if floating_ip.get('fixed_ip'): self.network_api.disassociate_floating_ip(context, address) return webob.Response(status_int=202) diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py index d62225e58..9d4254f1f 100644 --- a/nova/api/openstack/contrib/volumes.py +++ b/nova/api/openstack/contrib/volumes.py @@ -372,8 +372,7 @@ class BootFromVolumeController(servers.ControllerV11): for key in ['instance_type', 'image_ref']: inst[key] = extra_values[key] - builder = self._get_view_builder(req) - server = builder.build(inst, is_detail=True) + server = self._build_view(req, inst, is_detail=True) server['server']['adminPass'] = extra_values['password'] return server diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 29e071609..e27ddf78b 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -92,7 +92,8 @@ class CreateInstanceHelper(object): if str(image_href).startswith(req.application_url): image_href = image_href.split('/').pop() try: - image_service, image_id = nova.image.get_image_service(image_href) + image_service, image_id = nova.image.get_image_service(context, + image_href) kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( req, image_service, image_id) images = set([str(x['id']) for x in image_service.index(context)]) @@ -282,7 +283,7 @@ class CreateInstanceHelper(object): try: ramdisk_id = image_meta['properties']['ramdisk_id'] except KeyError: - raise exception.RamdiskNotFoundForImage(image_id=image_id) + ramdisk_id = None return kernel_id, ramdisk_id diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index fd36060da..8a310c900 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -16,12 +16,13 @@ # under the License. import webob -import xml.dom.minidom as minidom +from lxml import etree from nova import db from nova import exception from nova.api.openstack import views from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil class Controller(object): @@ -78,48 +79,48 @@ class ControllerV11(Controller): class FlavorXMLSerializer(wsgi.XMLDictSerializer): + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + def __init__(self): super(FlavorXMLSerializer, self).__init__(xmlns=wsgi.XMLNS_V11) - def _flavor_to_xml(self, xml_doc, flavor, detailed): - flavor_node = xml_doc.createElement('flavor') - flavor_node.setAttribute('id', str(flavor['id'])) - flavor_node.setAttribute('name', flavor['name']) + 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_node.setAttribute('ram', str(flavor['ram'])) - flavor_node.setAttribute('disk', str(flavor['disk'])) - - link_nodes = self._create_link_nodes(xml_doc, flavor['links']) - for link_node in link_nodes: - flavor_node.appendChild(link_node) - return flavor_node + flavor_elem.set('ram', str(flavor_dict['ram'])) + flavor_elem.set('disk', str(flavor_dict['disk'])) - def _flavors_list_to_xml(self, xml_doc, flavors, detailed): - container_node = xml_doc.createElement('flavors') + for attr in ("vcpus", "swap", "rxtx_quota", "rxtx_cap"): + flavor_elem.set(attr, str(flavor_dict.get(attr, ""))) - for flavor in flavors: - item_node = self._flavor_to_xml(xml_doc, flavor, detailed) - container_node.appendChild(item_node) - return container_node + 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 def show(self, flavor_container): - xml_doc = minidom.Document() - flavor = flavor_container['flavor'] - node = self._flavor_to_xml(xml_doc, flavor, True) - return self.to_xml_string(node, True) - - def detail(self, flavors_container): - xml_doc = minidom.Document() - flavors = flavors_container['flavors'] - node = self._flavors_list_to_xml(xml_doc, flavors, True) - return self.to_xml_string(node, True) - - def index(self, flavors_container): - xml_doc = minidom.Document() - flavors = flavors_container['flavors'] - node = self._flavors_list_to_xml(xml_doc, flavors, False) - return self.to_xml_string(node, True) + 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) + + 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) def create_resource(version='1.0'): diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index 4d615ea96..adb6bee4b 100644 --- a/nova/api/openstack/image_metadata.py +++ b/nova/api/openstack/image_metadata.py @@ -17,6 +17,7 @@ from webob import exc +from nova import exception from nova import flags from nova import image from nova import utils @@ -33,21 +34,22 @@ class Controller(object): def __init__(self): self.image_service = image.get_default_image_service() - def _get_metadata(self, context, image_id, image=None): - if not image: - image = self.image_service.show(context, image_id) - metadata = image.get('properties', {}) - return metadata + def _get_image(self, context, image_id): + try: + return self.image_service.show(context, image_id) + except exception.NotFound: + msg = _("Image not found.") + raise exc.HTTPNotFound(explanation=msg) def index(self, req, image_id): """Returns the list of metadata for a given instance""" context = req.environ['nova.context'] - metadata = self._get_metadata(context, image_id) + metadata = self._get_image(context, image_id)['properties'] return dict(metadata=metadata) def show(self, req, image_id, id): context = req.environ['nova.context'] - metadata = self._get_metadata(context, image_id) + metadata = self._get_image(context, image_id)['properties'] if id in metadata: return {'meta': {id: metadata[id]}} else: @@ -55,15 +57,13 @@ class Controller(object): def create(self, req, image_id, body): context = req.environ['nova.context'] - img = self.image_service.show(context, image_id) - metadata = self._get_metadata(context, image_id, img) + image = self._get_image(context, image_id) if 'metadata' in body: for key, value in body['metadata'].iteritems(): - metadata[key] = value - common.check_img_metadata_quota_limit(context, metadata) - img['properties'] = metadata - self.image_service.update(context, image_id, img, None) - return dict(metadata=metadata) + image['properties'][key] = value + common.check_img_metadata_quota_limit(context, image['properties']) + self.image_service.update(context, image_id, image, None) + return dict(metadata=image['properties']) def update(self, req, image_id, id, body): context = req.environ['nova.context'] @@ -80,32 +80,30 @@ class Controller(object): if len(meta) > 1: expl = _('Request body contains too many items') raise exc.HTTPBadRequest(explanation=expl) - img = self.image_service.show(context, image_id) - metadata = self._get_metadata(context, image_id, img) - metadata[id] = meta[id] - common.check_img_metadata_quota_limit(context, metadata) - img['properties'] = metadata - self.image_service.update(context, image_id, img, None) + + image = self._get_image(context, image_id) + image['properties'][id] = meta[id] + common.check_img_metadata_quota_limit(context, image['properties']) + self.image_service.update(context, image_id, image, None) return dict(meta=meta) def update_all(self, req, image_id, body): context = req.environ['nova.context'] - img = self.image_service.show(context, image_id) + image = self._get_image(context, image_id) metadata = body.get('metadata', {}) common.check_img_metadata_quota_limit(context, metadata) - img['properties'] = metadata - self.image_service.update(context, image_id, img, None) + image['properties'] = metadata + self.image_service.update(context, image_id, image, None) return dict(metadata=metadata) def delete(self, req, image_id, id): context = req.environ['nova.context'] - img = self.image_service.show(context, image_id) - metadata = self._get_metadata(context, image_id) - if not id in metadata: - raise exc.HTTPNotFound() - metadata.pop(id) - img['properties'] = metadata - self.image_service.update(context, image_id, img, None) + image = self._get_image(context, image_id) + if not id in image['properties']: + msg = _("Invalid metadata key") + raise exc.HTTPNotFound(explanation=msg) + image['properties'].pop(id) + self.image_service.update(context, image_id, image, None) def create_resource(): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 1c8fc10c9..4340cbe3e 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -16,8 +16,8 @@ import urlparse import os.path +from lxml import etree import webob.exc -from xml.dom import minidom from nova import compute from nova import exception @@ -29,6 +29,7 @@ from nova.api.openstack import image_metadata from nova.api.openstack import servers from nova.api.openstack.views import images as images_view from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil LOG = log.getLogger('nova.api.openstack.images') @@ -50,7 +51,7 @@ class Controller(object): """Initialize new `ImageController`. :param compute_service: `nova.compute.api:API` - :param image_service: `nova.image.service:BaseImageService` + :param image_service: `nova.image.glance:GlancemageService` """ self._compute_service = compute_service or compute.API() @@ -206,93 +207,71 @@ class ControllerV11(Controller): class ImageXMLSerializer(wsgi.XMLDictSerializer): - xmlns = wsgi.XMLNS_V11 + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} def __init__(self): self.metadata_serializer = common.MetadataXMLSerializer() - def _image_to_xml(self, xml_doc, image): - image_node = xml_doc.createElement('image') - image_node.setAttribute('id', str(image['id'])) - image_node.setAttribute('name', image['name']) - link_nodes = self._create_link_nodes(xml_doc, - image['links']) - for link_node in link_nodes: - image_node.appendChild(link_node) - return image_node - - def _image_to_xml_detailed(self, xml_doc, image): - image_node = xml_doc.createElement('image') - self._add_image_attributes(image_node, image) - - if 'server' in image: - server_node = self._create_server_node(xml_doc, image['server']) - image_node.appendChild(server_node) - - metadata = image.get('metadata', {}).items() - if len(metadata) > 0: - metadata_node = self._create_metadata_node(xml_doc, metadata) - image_node.appendChild(metadata_node) - - link_nodes = self._create_link_nodes(xml_doc, - image['links']) - for link_node in link_nodes: - image_node.appendChild(link_node) - - return image_node - - def _add_image_attributes(self, node, image): - node.setAttribute('id', str(image['id'])) - node.setAttribute('name', image['name']) - node.setAttribute('created', image['created']) - node.setAttribute('updated', image['updated']) - node.setAttribute('status', image['status']) - if 'progress' in image: - node.setAttribute('progress', str(image['progress'])) - - def _create_metadata_node(self, xml_doc, metadata): - return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata) - - def _create_server_node(self, xml_doc, server): - server_node = xml_doc.createElement('server') - server_node.setAttribute('id', str(server['id'])) - link_nodes = self._create_link_nodes(xml_doc, - server['links']) - for link_node in link_nodes: - server_node.appendChild(link_node) - return server_node - - def _image_list_to_xml(self, xml_doc, images, detailed): - container_node = xml_doc.createElement('images') + 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 + + 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 + + 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_to_xml = self._image_to_xml_detailed - else: - image_to_xml = self._image_to_xml - - for image in images: - item_node = image_to_xml(xml_doc, image) - container_node.appendChild(item_node) - return container_node + 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 'server' in image_dict: + server_elem = self._create_server_node(image_dict['server']) + image_elem.append(server_elem) + + meta_elem = self._create_metadata_node( + image_dict.get('metadata', {})) + image_elem.append(meta_elem) + + 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 def index(self, images_dict): - xml_doc = minidom.Document() - node = self._image_list_to_xml(xml_doc, - images_dict['images'], - detailed=False) - return self.to_xml_string(node, True) + 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) + return self._to_xml(images) def detail(self, images_dict): - xml_doc = minidom.Document() - node = self._image_list_to_xml(xml_doc, - images_dict['images'], - detailed=True) - return self.to_xml_string(node, True) + 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) def show(self, image_dict): - xml_doc = minidom.Document() - node = self._image_to_xml_detailed(xml_doc, - image_dict['image']) - return self.to_xml_string(node, True) + image = etree.Element('image', nsmap=self.NSMAP) + self._populate_image(image, image_dict['image'], True) + return self._to_xml(image) def create_resource(version='1.0'): diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py index a74fae487..7e644ba04 100644 --- a/nova/api/openstack/ips.py +++ b/nova/api/openstack/ips.py @@ -15,14 +15,15 @@ # License for the specific language governing permissions and limitations # under the License. +from lxml import etree import time -from xml.dom import minidom from webob import exc import nova import nova.api.openstack.views.addresses from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil from nova import db @@ -102,42 +103,36 @@ class ControllerV11(Controller): class IPXMLSerializer(wsgi.XMLDictSerializer): + + NSMAP = {None: xmlutil.XMLNS_V11} + def __init__(self, xmlns=wsgi.XMLNS_V11): super(IPXMLSerializer, self).__init__(xmlns=xmlns) - def _ip_to_xml(self, xml_doc, ip_dict): - ip_node = xml_doc.createElement('ip') - ip_node.setAttribute('addr', ip_dict['addr']) - ip_node.setAttribute('version', str(ip_dict['version'])) - return ip_node - - def _network_to_xml(self, xml_doc, network_id, ip_dicts): - network_node = xml_doc.createElement('network') - network_node.setAttribute('id', network_id) + 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) + 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_node = self._ip_to_xml(xml_doc, ip_dict) - network_node.appendChild(ip_node) - - return network_node - - def networks_to_xml(self, xml_doc, networks_container): - addresses_node = xml_doc.createElement('addresses') - for (network_id, ip_dicts) in networks_container.items(): - network_node = self._network_to_xml(xml_doc, network_id, ip_dicts) - addresses_node.appendChild(network_node) - return addresses_node - - def show(self, network_container): - (network_id, ip_dicts) = network_container.items()[0] - xml_doc = minidom.Document() - node = self._network_to_xml(xml_doc, network_id, ip_dicts) - return self.to_xml_string(node, False) - - def index(self, addresses_container): - xml_doc = minidom.Document() - node = self.networks_to_xml(xml_doc, addresses_container['addresses']) - return self.to_xml_string(node, False) + 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) + + 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) def create_resource(version): diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index 86afa3b62..f6df94eea 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -20,12 +20,12 @@ Module dedicated functions/classes dealing with rate limiting requests. import copy import httplib import json +from lxml import etree import math import re import time import urllib import webob.exc -from xml.dom import minidom from collections import defaultdict @@ -38,6 +38,7 @@ from nova.api.openstack import common from nova.api.openstack import faults from nova.api.openstack.views import limits as limits_views from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil # Convenience constants for the limits dictionary passed to Limiter(). @@ -81,52 +82,49 @@ class LimitsXMLSerializer(wsgi.XMLDictSerializer): xmlns = wsgi.XMLNS_V11 + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + def __init__(self): pass - def _create_rates_node(self, xml_doc, rates): - rates_node = xml_doc.createElement('rates') + def _create_rates_node(self, rates): + rates_elem = etree.Element('rates', nsmap=self.NSMAP) for rate in rates: - rate_node = xml_doc.createElement('rate') - rate_node.setAttribute('uri', rate['uri']) - rate_node.setAttribute('regex', rate['regex']) - + 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_node = xml_doc.createElement('limit') - limit_node.setAttribute('value', str(limit['value'])) - limit_node.setAttribute('verb', limit['verb']) - limit_node.setAttribute('remaining', str(limit['remaining'])) - limit_node.setAttribute('unit', limit['unit']) - limit_node.setAttribute('next-available', - str(limit['next-available'])) - rate_node.appendChild(limit_node) - - rates_node.appendChild(rate_node) - return rates_node - - def _create_absolute_node(self, xml_doc, absolutes): - absolute_node = xml_doc.createElement('absolute') - for key, value in absolutes.iteritems(): - limit_node = xml_doc.createElement('limit') - limit_node.setAttribute('name', key) - limit_node.setAttribute('value', str(value)) - absolute_node.appendChild(limit_node) - return absolute_node - - def _limits_to_xml(self, xml_doc, limits): - limits_node = xml_doc.createElement('limits') - rates_node = self._create_rates_node(xml_doc, limits['rate']) - limits_node.appendChild(rates_node) - - absolute_node = self._create_absolute_node(xml_doc, limits['absolute']) - limits_node.appendChild(absolute_node) - - return limits_node + 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 + + 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 + + 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): - xml_doc = minidom.Document() - node = self._limits_to_xml(xml_doc, limits_dict['limits']) - return self.to_xml_string(node, False) + limits = etree.Element('limits', nsmap=self.NSMAP) + self._populate_limits(limits, limits_dict['limits']) + return self._to_xml(limits) def create_resource(version='1.0'): diff --git a/nova/api/openstack/schemas/v1.1/addresses.rng b/nova/api/openstack/schemas/v1.1/addresses.rng new file mode 100644 index 000000000..b498e8a63 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/addresses.rng @@ -0,0 +1,14 @@ +<element name="addresses" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <zeroOrMore> + <element name="network"> + <attribute name="id"> <text/> </attribute> + <zeroOrMore> + <element name="ip"> + <attribute name="version"> <text/> </attribute> + <attribute name="addr"> <text/> </attribute> + </element> + </zeroOrMore> + </element> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/flavor.rng b/nova/api/openstack/schemas/v1.1/flavor.rng new file mode 100644 index 000000000..6d3adc8dc --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/flavor.rng @@ -0,0 +1,14 @@ +<element name="flavor" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <attribute name="name"> <text/> </attribute> + <attribute name="id"> <text/> </attribute> + <attribute name="ram"> <text/> </attribute> + <attribute name="disk"> <text/> </attribute> + <attribute name="rxtx_cap"> <text/> </attribute> + <attribute name="rxtx_quota"> <text/> </attribute> + <attribute name="swap"> <text/> </attribute> + <attribute name="vcpus"> <text/> </attribute> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/flavors.rng b/nova/api/openstack/schemas/v1.1/flavors.rng new file mode 100644 index 000000000..b7a3acc01 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/flavors.rng @@ -0,0 +1,6 @@ +<element name="flavors" xmlns="http://relaxng.org/ns/structure/1.0" + ns="http://docs.openstack.org/compute/api/v1.1"> + <zeroOrMore> + <externalRef href="flavor.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/flavors_index.rng b/nova/api/openstack/schemas/v1.1/flavors_index.rng new file mode 100644 index 000000000..d1a4fedb1 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/flavors_index.rng @@ -0,0 +1,12 @@ +<element name="flavors" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <zeroOrMore> + <element name="flavor"> + <attribute name="name"> <text/> </attribute> + <attribute name="id"> <text/> </attribute> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> + </element> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/image.rng b/nova/api/openstack/schemas/v1.1/image.rng new file mode 100644 index 000000000..887f76751 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/image.rng @@ -0,0 +1,30 @@ +<element name="image" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <attribute name="name"> <text/> </attribute> + <attribute name="id"> <text/> </attribute> + <attribute name="updated"> <text/> </attribute> + <attribute name="created"> <text/> </attribute> + <attribute name="status"> <text/> </attribute> + <optional> + <attribute name="progress"> <text/> </attribute> + </optional> + <optional> + <element name="server"> + <attribute name="id"> <text/> </attribute> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> + </element> + </optional> + <element name="metadata"> + <zeroOrMore> + <element name="meta"> + <attribute name="key"> <text/> </attribute> + <text/> + </element> + </zeroOrMore> + </element> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/images.rng b/nova/api/openstack/schemas/v1.1/images.rng new file mode 100644 index 000000000..064d4d9cc --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/images.rng @@ -0,0 +1,6 @@ +<element name="images" xmlns="http://relaxng.org/ns/structure/1.0" + ns="http://docs.openstack.org/compute/api/v1.1"> + <zeroOrMore> + <externalRef href="image.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/images_index.rng b/nova/api/openstack/schemas/v1.1/images_index.rng new file mode 100644 index 000000000..81af19cb5 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/images_index.rng @@ -0,0 +1,12 @@ +<element name="images" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <zeroOrMore> + <element name="image"> + <attribute name="name"> <text/> </attribute> + <attribute name="id"> <text/> </attribute> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> + </element> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/limits.rng b/nova/api/openstack/schemas/v1.1/limits.rng new file mode 100644 index 000000000..1af8108ec --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/limits.rng @@ -0,0 +1,28 @@ +<element name="limits" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <element name="rates"> + <zeroOrMore> + <element name="rate"> + <attribute name="uri"> <text/> </attribute> + <attribute name="regex"> <text/> </attribute> + <zeroOrMore> + <element name="limit"> + <attribute name="value"> <text/> </attribute> + <attribute name="verb"> <text/> </attribute> + <attribute name="remaining"> <text/> </attribute> + <attribute name="unit"> <text/> </attribute> + <attribute name="next-available"> <text/> </attribute> + </element> + </zeroOrMore> + </element> + </zeroOrMore> + </element> + <element name="absolute"> + <zeroOrMore> + <element name="limit"> + <attribute name="name"> <text/> </attribute> + <attribute name="value"> <text/> </attribute> + </element> + </zeroOrMore> + </element> +</element> diff --git a/nova/api/openstack/schemas/v1.1/metadata.rng b/nova/api/openstack/schemas/v1.1/metadata.rng new file mode 100644 index 000000000..b2f5d702a --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/metadata.rng @@ -0,0 +1,9 @@ + <element name="metadata" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <zeroOrMore> + <element name="meta"> + <attribute name="key"> <text/> </attribute> + <text/> + </element> + </zeroOrMore> + </element> diff --git a/nova/api/openstack/schemas/v1.1/server.rng b/nova/api/openstack/schemas/v1.1/server.rng index dbd169a83..4eb1a0b85 100644 --- a/nova/api/openstack/schemas/v1.1/server.rng +++ b/nova/api/openstack/schemas/v1.1/server.rng @@ -1,6 +1,8 @@ <element name="server" ns="http://docs.openstack.org/compute/api/v1.1" xmlns="http://relaxng.org/ns/structure/1.0"> <attribute name="name"> <text/> </attribute> + <attribute name="userId"> <text/> </attribute> + <attribute name="tenantId"> <text/> </attribute> <attribute name="id"> <text/> </attribute> <attribute name="uuid"> <text/> </attribute> <attribute name="updated"> <text/> </attribute> @@ -15,9 +17,6 @@ <optional> <attribute name="adminPass"> <text/> </attribute> </optional> - <zeroOrMore> - <externalRef href="../atom-link.rng"/> - </zeroOrMore> <element name="image"> <attribute name="id"> <text/> </attribute> <externalRef href="../atom-link.rng"/> @@ -47,4 +46,7 @@ </element> </zeroOrMore> </element> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> </element> diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 977958f5d..5affd1f33 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -17,8 +17,8 @@ import base64 import os import traceback +from lxml import etree from webob import exc -from xml.dom import minidom import webob from nova import compute @@ -38,6 +38,7 @@ import nova.api.openstack.views.addresses import nova.api.openstack.views.flavors import nova.api.openstack.views.images import nova.api.openstack.views.servers +from nova.api.openstack import xmlutil LOG = logging.getLogger('nova.api.openstack.servers') @@ -334,9 +335,8 @@ class Controller(object): LOG.exception(msg) raise exc.HTTPBadRequest(explanation=msg) try: - # TODO(gundlach): pass reboot_type, support soft reboot in - # virt driver - self.compute_api.reboot(req.environ['nova.context'], id) + self.compute_api.reboot(req.environ['nova.context'], id, + reboot_type) except Exception, e: LOG.exception(_("Error in reboot %s"), e) raise exc.HTTPUnprocessableEntity() @@ -851,123 +851,113 @@ class HeadersSerializer(wsgi.ResponseHeadersSerializer): class ServerXMLSerializer(wsgi.XMLDictSerializer): - xmlns = wsgi.XMLNS_V11 + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} def __init__(self): self.metadata_serializer = common.MetadataXMLSerializer() self.addresses_serializer = ips.IPXMLSerializer() - def _create_basic_entity_node(self, xml_doc, id, links, name): - basic_node = xml_doc.createElement(name) - basic_node.setAttribute('id', str(id)) - link_nodes = self._create_link_nodes(xml_doc, links) - for link_node in link_nodes: - basic_node.appendChild(link_node) - return basic_node - - def _create_metadata_node(self, xml_doc, metadata): - return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata) - - def _create_addresses_node(self, xml_doc, addresses): - return self.addresses_serializer.networks_to_xml(xml_doc, addresses) - - def _add_server_attributes(self, node, server): - node.setAttribute('id', str(server['id'])) - node.setAttribute('uuid', str(server['uuid'])) - node.setAttribute('hostId', str(server['hostId'])) - node.setAttribute('name', server['name']) - node.setAttribute('created', str(server['created'])) - node.setAttribute('updated', str(server['updated'])) - node.setAttribute('status', server['status']) - if 'accessIPv4' in server: - node.setAttribute('accessIPv4', str(server['accessIPv4'])) - if 'accessIPv6' in server: - node.setAttribute('accessIPv6', str(server['accessIPv6'])) - if 'progress' in server: - node.setAttribute('progress', str(server['progress'])) - - def _server_to_xml(self, xml_doc, server): - server_node = xml_doc.createElement('server') - server_node.setAttribute('id', str(server['id'])) - server_node.setAttribute('name', server['name']) - link_nodes = self._create_link_nodes(xml_doc, - server['links']) - for link_node in link_nodes: - server_node.appendChild(link_node) - return server_node - - def _server_to_xml_detailed(self, xml_doc, server): - server_node = xml_doc.createElement('server') - self._add_server_attributes(server_node, server) - - link_nodes = self._create_link_nodes(xml_doc, - server['links']) - for link_node in link_nodes: - server_node.appendChild(link_node) - - if 'image' in server: - image_node = self._create_basic_entity_node(xml_doc, - server['image']['id'], - server['image']['links'], - 'image') - server_node.appendChild(image_node) - - if 'flavor' in server: - flavor_node = self._create_basic_entity_node(xml_doc, - server['flavor']['id'], - server['flavor']['links'], - 'flavor') - server_node.appendChild(flavor_node) - - metadata = server.get('metadata', {}).items() - if len(metadata) > 0: - metadata_node = self._create_metadata_node(xml_doc, metadata) - server_node.appendChild(metadata_node) - - addresses_node = self._create_addresses_node(xml_doc, - server['addresses']) - server_node.appendChild(addresses_node) - - return server_node - - def _server_list_to_xml(self, xml_doc, servers, detailed): - container_node = xml_doc.createElement('servers') + 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 + + 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 + + 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 + + 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 + + def _populate_server(self, server_elem, server_dict, detailed=False): + """Populate a server xml element from a dict.""" + + server_elem.set('name', server_dict['name']) + server_elem.set('id', str(server_dict['id'])) if detailed: - server_to_xml = self._server_to_xml_detailed - else: - server_to_xml = self._server_to_xml - - for server in servers: - item_node = server_to_xml(xml_doc, server) - container_node.appendChild(item_node) - return container_node + 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) + + flavor_elem = self._create_flavor_node(server_dict['flavor']) + server_elem.append(flavor_elem) + + 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']) + + 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 def index(self, servers_dict): - xml_doc = minidom.Document() - node = self._server_list_to_xml(xml_doc, - servers_dict['servers'], - detailed=False) - return self.to_xml_string(node, True) + 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) + return self._to_xml(servers) def detail(self, servers_dict): - xml_doc = minidom.Document() - node = self._server_list_to_xml(xml_doc, - servers_dict['servers'], - detailed=True) - return self.to_xml_string(node, True) + 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): - xml_doc = minidom.Document() - node = self._server_to_xml_detailed(xml_doc, - server_dict['server']) - return self.to_xml_string(node, True) + server = etree.Element('server', nsmap=self.NSMAP) + self._populate_server(server, server_dict['server'], True) + return self._to_xml(server) def create(self, server_dict): - xml_doc = minidom.Document() - node = self._server_to_xml_detailed(xml_doc, - server_dict['server']) - node.setAttribute('adminPass', server_dict['server']['adminPass']) - return self.to_xml_string(node, True) + 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 @@ -975,10 +965,9 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer): return self.create(server_dict) def update(self, server_dict): - xml_doc = minidom.Document() - node = self._server_to_xml_detailed(xml_doc, - server_dict['server']) - return self.to_xml_string(node, True) + server = etree.Element('server', nsmap=self.NSMAP) + self._populate_server(server, server_dict['server'], True) + return self._to_xml(server) def create_resource(version='1.0'): @@ -991,7 +980,7 @@ def create_resource(version='1.0'): "attributes": { "server": ["id", "imageId", "name", "flavorId", "hostId", "status", "progress", "adminPass", "flavorRef", - "imageRef"], + "imageRef", "userId", "tenantId"], "link": ["rel", "type", "href"], }, "dict_collections": { diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index e2f892fb6..75a1d0ba4 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -16,12 +16,13 @@ # under the License. from datetime import datetime +from lxml import etree import webob import webob.dec -from xml.dom import minidom import nova.api.openstack.views.versions from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil VERSIONS = { @@ -106,7 +107,9 @@ class Versions(wsgi.Resource): headers_serializer=headers_serializer) supported_content_types = ('application/json', + 'application/vnd.openstack.compute+json', 'application/xml', + 'application/vnd.openstack.compute+xml', 'application/atom+xml') deserializer = VersionsRequestDeserializer( supported_content_types=supported_content_types) @@ -159,83 +162,51 @@ class VersionsRequestDeserializer(wsgi.RequestDeserializer): class VersionsXMLSerializer(wsgi.XMLDictSerializer): - #TODO(wwolf): this is temporary until we get rid of toprettyxml - # in the base class (XMLDictSerializer), which I plan to do in - # another branch - def to_xml_string(self, node, has_atom=False): - self._add_xmlns(node, has_atom) - return node.toxml(encoding='UTF-8') - - def _versions_to_xml(self, versions, name="versions", xmlns=None): - root = self._xml_doc.createElement(name) - root.setAttribute("xmlns", wsgi.XMLNS_V11) - root.setAttribute("xmlns:atom", wsgi.XMLNS_ATOM) - for version in versions: - root.appendChild(self._create_version_node(version)) - - return root - - def _create_media_types(self, media_types): - base = self._xml_doc.createElement('media-types') - for type in media_types: - node = self._xml_doc.createElement('media-type') - node.setAttribute('base', type['base']) - node.setAttribute('type', type['type']) - base.appendChild(node) - - return base - - def _create_version_node(self, version, create_ns=False): - version_node = self._xml_doc.createElement('version') - if create_ns: - xmlns = wsgi.XMLNS_V11 - xmlns_atom = wsgi.XMLNS_ATOM - version_node.setAttribute('xmlns', xmlns) - version_node.setAttribute('xmlns:atom', xmlns_atom) - - version_node.setAttribute('id', version['id']) - version_node.setAttribute('status', version['status']) + def _populate_version(self, version_node, version): + version_node.set('id', version['id']) + version_node.set('status', version['status']) if 'updated' in version: - version_node.setAttribute('updated', version['updated']) - + version_node.set('updated', version['updated']) if 'media-types' in version: - media_types = self._create_media_types(version['media-types']) - version_node.appendChild(media_types) - - link_nodes = self._create_link_nodes(self._xml_doc, version['links']) - for link in link_nodes: - version_node.appendChild(link) - - return version_node + media_types = etree.SubElement(version_node, 'media-types') + for mtype in version['media-types']: + elem = etree.SubElement(media_types, 'media-type') + elem.set('base', mtype['base']) + elem.set('type', mtype['type']) + for link in version.get('links', []): + elem = etree.SubElement(version_node, + '{%s}link' % xmlutil.XMLNS_ATOM) + elem.set('rel', link['rel']) + elem.set('href', link['href']) + if 'type' in link: + elem.set('type', link['type']) + + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} def index(self, data): - self._xml_doc = minidom.Document() - node = self._versions_to_xml(data['versions']) - - return self.to_xml_string(node) + root = etree.Element('versions', nsmap=self.NSMAP) + for version in data['versions']: + version_elem = etree.SubElement(root, 'version') + self._populate_version(version_elem, version) + return self._to_xml(root) def show(self, data): - self._xml_doc = minidom.Document() - node = self._create_version_node(data['version'], True) - - return self.to_xml_string(node) + root = etree.Element('version', nsmap=self.NSMAP) + self._populate_version(root, data['version']) + return self._to_xml(root) def multi(self, data): - self._xml_doc = minidom.Document() - node = self._versions_to_xml(data['choices'], 'choices', - xmlns=wsgi.XMLNS_V11) - - return self.to_xml_string(node) + root = etree.Element('choices', nsmap=self.NSMAP) + for version in data['choices']: + version_elem = etree.SubElement(root, 'version') + self._populate_version(version_elem, version) + return self._to_xml(root) class VersionsAtomSerializer(wsgi.XMLDictSerializer): - #TODO(wwolf): this is temporary until we get rid of toprettyxml - # in the base class (XMLDictSerializer), which I plan to do in - # another branch - def to_xml_string(self, node, has_atom=False): - self._add_xmlns(node, has_atom) - return node.toxml(encoding='UTF-8') + + NSMAP = {None: xmlutil.XMLNS_ATOM} def __init__(self, metadata=None, xmlns=None): self.metadata = metadata or {} @@ -244,14 +215,6 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): else: self.xmlns = xmlns - def _create_text_elem(self, name, text, type=None): - elem = self._xml_doc.createElement(name) - if type: - elem.setAttribute('type', type) - elem_text = self._xml_doc.createTextNode(text) - elem.appendChild(elem_text) - return elem - def _get_most_recent_update(self, versions): recent = None for version in versions: @@ -269,105 +232,64 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): link_href = link_href.rstrip('/') return link_href.rsplit('/', 1)[0] + '/' - def _create_detail_meta(self, root, version): - title = self._create_text_elem('title', "About This Version", - type='text') - - updated = self._create_text_elem('updated', version['updated']) - - uri = version['links'][0]['href'] - id = self._create_text_elem('id', uri) - - link = self._xml_doc.createElement('link') - link.setAttribute('rel', 'self') - link.setAttribute('href', uri) + def _create_feed(self, versions, feed_title, feed_id): + feed = etree.Element('feed', nsmap=self.NSMAP) + title = etree.SubElement(feed, 'title') + title.set('type', 'text') + title.text = feed_title - author = self._xml_doc.createElement('author') - author_name = self._create_text_elem('name', 'Rackspace') - author_uri = self._create_text_elem('uri', 'http://www.rackspace.com/') - author.appendChild(author_name) - author.appendChild(author_uri) - - root.appendChild(title) - root.appendChild(updated) - root.appendChild(id) - root.appendChild(author) - root.appendChild(link) - - def _create_list_meta(self, root, versions): - title = self._create_text_elem('title', "Available API Versions", - type='text') # Set this updated to the most recently updated version recent = self._get_most_recent_update(versions) - updated = self._create_text_elem('updated', recent) - - base_url = self._get_base_url(versions[0]['links'][0]['href']) - id = self._create_text_elem('id', base_url) + etree.SubElement(feed, 'updated').text = recent - link = self._xml_doc.createElement('link') - link.setAttribute('rel', 'self') - link.setAttribute('href', base_url) + etree.SubElement(feed, 'id').text = feed_id - author = self._xml_doc.createElement('author') - author_name = self._create_text_elem('name', 'Rackspace') - author_uri = self._create_text_elem('uri', 'http://www.rackspace.com/') - author.appendChild(author_name) - author.appendChild(author_uri) + link = etree.SubElement(feed, 'link') + link.set('rel', 'self') + link.set('href', feed_id) - root.appendChild(title) - root.appendChild(updated) - root.appendChild(id) - root.appendChild(author) - root.appendChild(link) + author = etree.SubElement(feed, 'author') + etree.SubElement(author, 'name').text = 'Rackspace' + etree.SubElement(author, 'uri').text = 'http://www.rackspace.com/' - def _create_version_entries(self, root, versions): for version in versions: - entry = self._xml_doc.createElement('entry') - - id = self._create_text_elem('id', version['links'][0]['href']) - title = self._create_text_elem('title', - 'Version %s' % version['id'], - type='text') - updated = self._create_text_elem('updated', version['updated']) - - entry.appendChild(id) - entry.appendChild(title) - entry.appendChild(updated) - - for link in version['links']: - link_node = self._xml_doc.createElement('link') - link_node.setAttribute('rel', link['rel']) - link_node.setAttribute('href', link['href']) - if 'type' in link: - link_node.setAttribute('type', link['type']) - - entry.appendChild(link_node) - - content = self._create_text_elem('content', - 'Version %s %s (%s)' % - (version['id'], - version['status'], - version['updated']), - type='text') - - entry.appendChild(content) - root.appendChild(entry) + feed.append(self._create_version_entry(version)) + + return feed + + def _create_version_entry(self, version): + entry = etree.Element('entry') + etree.SubElement(entry, 'id').text = version['links'][0]['href'] + title = etree.SubElement(entry, 'title') + title.set('type', 'text') + title.text = 'Version %s' % version['id'] + etree.SubElement(entry, 'updated').text = version['updated'] + + for link in version['links']: + link_elem = etree.SubElement(entry, 'link') + link_elem.set('rel', link['rel']) + link_elem.set('href', link['href']) + if 'type' in link: + link_elem.set('type', link['type']) + + content = etree.SubElement(entry, 'content') + content.set('type', 'text') + content.text = 'Version %s %s (%s)' % (version['id'], + version['status'], + version['updated']) + return entry def index(self, data): - self._xml_doc = minidom.Document() - node = self._xml_doc.createElementNS(self.xmlns, 'feed') - self._create_list_meta(node, data['versions']) - self._create_version_entries(node, data['versions']) - - return self.to_xml_string(node) + versions = data['versions'] + feed_id = self._get_base_url(versions[0]['links'][0]['href']) + feed = self._create_feed(versions, 'Available API Versions', feed_id) + return self._to_xml(feed) def show(self, data): - self._xml_doc = minidom.Document() - node = self._xml_doc.createElementNS(self.xmlns, 'feed') - self._create_detail_meta(node, data['version']) - self._create_version_entries(node, [data['version']]) - - return self.to_xml_string(node) + version = data['version'] + feed_id = version['links'][0]['href'] + feed = self._create_feed([version], 'About This Version', feed_id) + return self._to_xml(feed) class VersionsHeadersSerializer(wsgi.ResponseHeadersSerializer): @@ -388,7 +310,9 @@ def create_resource(version='1.0'): serializer = wsgi.ResponseSerializer(body_serializers) supported_content_types = ('application/json', + 'application/vnd.openstack.compute+json', 'application/xml', + 'application/vnd.openstack.compute+xml', 'application/atom+xml') deserializer = wsgi.RequestDeserializer( supported_content_types=supported_content_types) diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index 8f07a2289..8d38bc9c3 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -88,7 +88,6 @@ class ViewBuilderV11(ViewBuilder): try: return interface['network']['label'] except (TypeError, KeyError) as exc: - LOG.exception(exc) raise TypeError def _extract_ipv4_addresses(self, interface): diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index aea34b424..def969a6c 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -50,6 +50,9 @@ class ViewBuilder(object): "disk": flavor_obj["local_gb"], } + for key in ("vcpus", "swap", "rxtx_quota", "rxtx_cap"): + detail[key] = flavor_obj.get(key, "") + detail.update(simple) return detail diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 21f1b2d3e..86e8d7f3a 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -37,17 +37,18 @@ class ViewBuilder(object): def _format_status(self, image): """Update the status field to standardize format.""" status_mapping = { - 'pending': 'QUEUED', - 'decrypting': 'PREPARING', - 'untarring': 'SAVING', - 'available': 'ACTIVE', - 'killed': 'FAILED', + 'active': 'ACTIVE', + 'queued': 'SAVING', + 'saving': 'SAVING', + 'deleted': 'DELETED', + 'pending_delete': 'DELETED', + 'killed': 'ERROR', } try: - image['status'] = status_mapping[image['status']].upper() + image['status'] = status_mapping[image['status']] except KeyError: - image['status'] = image['status'].upper() + image['status'] = 'UNKNOWN' def _build_server(self, image, image_obj): """Indicates that you must use a ViewBuilder subclass.""" @@ -70,6 +71,7 @@ class ViewBuilder(object): } self._build_server(image, image_obj) + self._build_image_id(image, image_obj) if detail: image.update({ @@ -95,6 +97,12 @@ class ViewBuilderV10(ViewBuilder): except (KeyError, ValueError): pass + def _build_image_id(self, image, image_obj): + try: + image['id'] = int(image_obj['id']) + except ValueError: + pass + class ViewBuilderV11(ViewBuilder): """OpenStack API v1.1 Image Builder""" @@ -118,6 +126,9 @@ class ViewBuilderV11(ViewBuilder): except KeyError: return + def _build_image_id(self, image, image_obj): + image['id'] = "%s" % image_obj['id'] + def generate_href(self, image_id): """Return an href string pointing to this object.""" return os.path.join(self.base_url, self.project_id, diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 3a13d15f1..473dc9e7e 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -66,6 +66,8 @@ class ViewBuilder(object): inst_dict = { 'id': inst['id'], 'name': inst['display_name'], + 'user_id': inst.get('user_id', ''), + 'tenant_id': inst.get('project_id', ''), 'status': common.status_from_state(vm_state, task_state)} # Return the metadata as a dictionary @@ -143,6 +145,8 @@ class ViewBuilderV11(ViewBuilder): response['server']['accessIPv4'] = inst.get('access_ip_v4') or "" response['server']['accessIPv6'] = inst.get('access_ip_v6') or "" + response['server']['key_name'] = inst.get('key_name', '') + response['server']['config_drive'] = inst.get('config_drive') return response @@ -183,8 +187,6 @@ class ViewBuilderV11(ViewBuilder): def _build_extra(self, response, inst): self._build_links(response, inst) response['uuid'] = inst['uuid'] - response['key_name'] = inst.get('key_name', '') - self._build_config_drive(response, inst) def _build_links(self, response, inst): href = self.generate_href(inst["id"]) @@ -203,9 +205,6 @@ class ViewBuilderV11(ViewBuilder): response["links"] = links - def _build_config_drive(self, response, inst): - response['config_drive'] = inst.get('config_drive') - def generate_href(self, server_id): """Create an url that refers to a specific server id.""" return os.path.join(self.base_url, self.project_id, diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py index 03da80818..1ac398706 100644 --- a/nova/api/openstack/views/versions.py +++ b/nova/api/openstack/views/versions.py @@ -52,7 +52,7 @@ class ViewBuilder(object): def build_versions(self, versions): version_objs = [] - for version in versions: + for version in sorted(versions.keys()): version = versions[version] version_objs.append({ "id": version['id'], diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 8641e960a..180f328b9 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -1,5 +1,22 @@ +# 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. import json +from lxml import etree import webob from xml.dom import minidom from xml.parsers import expat @@ -18,6 +35,21 @@ XMLNS_ATOM = 'http://www.w3.org/2005/Atom' LOG = logging.getLogger('nova.api.openstack.wsgi') +# The vendor content types should serialize identically to the non-vendor +# content types. So to avoid littering the code with both options, we +# map the vendor to the other when looking up the type +_CONTENT_TYPE_MAP = { + 'application/vnd.openstack.compute+json': 'application/json', + 'application/vnd.openstack.compute+xml': 'application/xml', +} + +_SUPPORTED_CONTENT_TYPES = ( + 'application/json', + 'application/vnd.openstack.compute+json', + 'application/xml', + 'application/vnd.openstack.compute+xml', +) + class Request(webob.Request): """Add some Openstack API-specific logic to the base webob.Request.""" @@ -29,7 +61,7 @@ class Request(webob.Request): """ supported_content_types = supported_content_types or \ - ('application/json', 'application/xml') + _SUPPORTED_CONTENT_TYPES parts = self.path.rsplit('.', 1) if len(parts) > 1: @@ -51,7 +83,7 @@ class Request(webob.Request): if not "Content-Type" in self.headers: return None - allowed_types = ("application/xml", "application/json") + allowed_types = _SUPPORTED_CONTENT_TYPES content_type = self.content_type if content_type not in allowed_types: @@ -191,7 +223,7 @@ class RequestDeserializer(object): supported_content_types=None): self.supported_content_types = supported_content_types or \ - ('application/json', 'application/xml') + _SUPPORTED_CONTENT_TYPES self.body_deserializers = { 'application/xml': XMLDeserializer(), @@ -249,7 +281,8 @@ class RequestDeserializer(object): def get_body_deserializer(self, content_type): try: - return self.body_deserializers[content_type] + ctype = _CONTENT_TYPE_MAP.get(content_type, content_type) + return self.body_deserializers[ctype] except (KeyError, TypeError): raise exception.InvalidContentType(content_type=content_type) @@ -315,7 +348,7 @@ class XMLDictSerializer(DictSerializer): def to_xml_string(self, node, has_atom=False): self._add_xmlns(node, has_atom) - return node.toprettyxml(indent=' ', encoding='UTF-8') + return node.toxml('UTF-8') #NOTE (ameade): the has_atom should be removed after all of the # xml serializers and view builders have been updated to the current @@ -392,6 +425,10 @@ class XMLDictSerializer(DictSerializer): link_nodes.append(link_node) return link_nodes + def _to_xml(self, root): + """Convert the xml object to an xml string.""" + return etree.tostring(root, encoding='UTF-8', xml_declaration=True) + class ResponseHeadersSerializer(ActionDispatcher): """Default response headers serialization""" @@ -439,7 +476,8 @@ class ResponseSerializer(object): def get_body_serializer(self, content_type): try: - return self.body_serializers[content_type] + ctype = _CONTENT_TYPE_MAP.get(content_type, content_type) + return self.body_serializers[ctype] except (KeyError, TypeError): raise exception.InvalidContentType(content_type=content_type) diff --git a/nova/compute/api.py b/nova/compute/api.py index e045ef3de..4220f47ae 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -202,7 +202,8 @@ class API(base.Base): self._check_injected_file_quota(context, injected_files) self._check_requested_networks(context, requested_networks) - (image_service, image_id) = nova.image.get_image_service(image_href) + (image_service, image_id) = nova.image.get_image_service(context, + image_href) image = image_service.show(context, image_id) config_drive_id = None @@ -383,10 +384,6 @@ class API(base.Base): If you are changing this method, be sure to update both call paths. """ - instance = dict(launch_index=num, **base_options) - instance = self.db.instance_create(context, instance) - instance_id = instance['id'] - elevated = context.elevated() if security_group is None: security_group = ['default'] @@ -400,6 +397,10 @@ class API(base.Base): security_group_name) security_groups.append(group['id']) + instance = dict(launch_index=num, **base_options) + instance = self.db.instance_create(context, instance) + instance_id = instance['id'] + for security_group_id in security_groups: self.db.instance_add_security_group(elevated, instance_id, @@ -877,6 +878,7 @@ class API(base.Base): 'image': 'image_ref', 'name': 'display_name', 'instance_name': 'name', + 'tenant_id': 'project_id', 'recurse_zones': None, 'flavor': _remap_flavor_filter, 'fixed_ip': _remap_fixed_ip_filter} @@ -1041,13 +1043,14 @@ class API(base.Base): return recv_meta @scheduler_api.reroute_compute("reboot") - def reboot(self, context, instance_id): + def reboot(self, context, instance_id, reboot_type): """Reboot the given instance.""" self.update(context, instance_id, vm_state=vm_states.ACTIVE, task_state=task_states.REBOOTING) - self._cast_compute_message('reboot_instance', context, instance_id) + self._cast_compute_message('reboot_instance', context, instance_id, + params={'reboot_type': reboot_type}) @scheduler_api.reroute_compute("rebuild") def rebuild(self, context, instance_id, image_href, admin_password, diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 0477db745..7915830ec 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -322,7 +322,8 @@ class ComputeManager(manager.SchedulerDependentManager): # used by the image service. This should be refactored to be # consistent. image_href = instance['image_ref'] - image_service, image_id = nova.image.get_image_service(image_href) + image_service, image_id = nova.image.get_image_service(context, + image_href) image_meta = image_service.show(context, image_id) try: @@ -579,7 +580,7 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock - def reboot_instance(self, context, instance_id): + def reboot_instance(self, context, instance_id, reboot_type="SOFT"): """Reboot an instance on this host.""" LOG.audit(_("Rebooting instance %s"), instance_id, context=context) context = context.elevated() @@ -601,7 +602,7 @@ class ComputeManager(manager.SchedulerDependentManager): context=context) network_info = self._get_instance_nw_info(context, instance_ref) - self.driver.reboot(instance_ref, network_info) + self.driver.reboot(instance_ref, network_info, reboot_type) current_power_state = self._get_power_state(context, instance_ref) self._instance_update(context, diff --git a/nova/context.py b/nova/context.py index 5c22641a0..de5b791c4 100644 --- a/nova/context.py +++ b/nova/context.py @@ -32,7 +32,7 @@ class RequestContext(object): def __init__(self, user_id, project_id, is_admin=None, read_deleted=False, roles=None, remote_address=None, timestamp=None, - request_id=None, auth_token=None): + request_id=None, auth_token=None, strategy='noauth'): self.user_id = user_id self.project_id = project_id self.roles = roles or [] @@ -50,6 +50,7 @@ class RequestContext(object): request_id = unicode(uuid.uuid4()) self.request_id = request_id self.auth_token = auth_token + self.strategy = strategy def to_dict(self): return {'user_id': self.user_id, @@ -60,7 +61,8 @@ class RequestContext(object): 'remote_address': self.remote_address, 'timestamp': utils.strtime(self.timestamp), 'request_id': self.request_id, - 'auth_token': self.auth_token} + 'auth_token': self.auth_token, + 'strategy': self.strategy} @classmethod def from_dict(cls, values): @@ -77,7 +79,8 @@ class RequestContext(object): remote_address=self.remote_address, timestamp=self.timestamp, request_id=self.request_id, - auth_token=self.auth_token) + auth_token=self.auth_token, + strategy=self.strategy) def get_admin_context(read_deleted=False): diff --git a/nova/db/api.py b/nova/db/api.py index 148887635..05d81d8b2 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -261,11 +261,13 @@ def floating_ip_disassociate(context, address): return IMPL.floating_ip_disassociate(context, address) -def floating_ip_fixed_ip_associate(context, floating_address, fixed_address): +def floating_ip_fixed_ip_associate(context, floating_address, + fixed_address, host): """Associate an floating ip to a fixed_ip by address.""" return IMPL.floating_ip_fixed_ip_associate(context, floating_address, - fixed_address) + fixed_address, + host) def floating_ip_get_all(context): @@ -324,13 +326,15 @@ def migration_get_by_instance_and_status(context, instance_uuid, status): #################### -def fixed_ip_associate(context, address, instance_id, network_id=None): +def fixed_ip_associate(context, address, instance_id, network_id=None, + reserved=False): """Associate fixed ip to instance. Raises if fixed ip is not available. """ - return IMPL.fixed_ip_associate(context, address, instance_id, network_id) + return IMPL.fixed_ip_associate(context, address, instance_id, network_id, + reserved) def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): @@ -365,7 +369,7 @@ def fixed_ip_get_all(context): def fixed_ip_get_all_by_instance_host(context, host): """Get all allocated fixed ips filtered by instance host.""" - return IMPL.fixed_ip_get_all_instance_by_host(context, host) + return IMPL.fixed_ip_get_all_by_instance_host(context, host) def fixed_ip_get_by_address(context, address): @@ -420,6 +424,11 @@ def virtual_interface_get_by_address(context, address): return IMPL.virtual_interface_get_by_address(context, address) +def virtual_interface_get_by_uuid(context, vif_uuid): + """Gets a virtual interface from the table filtering on vif uuid.""" + return IMPL.virtual_interface_get_by_uuid(context, vif_uuid) + + def virtual_interface_get_by_fixed_ip(context, fixed_ip_id): """Gets the virtual interface fixed_ip is associated with.""" return IMPL.virtual_interface_get_by_fixed_ip(context, fixed_ip_id) @@ -715,6 +724,11 @@ def network_get_by_bridge(context, bridge): return IMPL.network_get_by_bridge(context, bridge) +def network_get_by_uuid(context, uuid): + """Get a network by uuid or raise if it does not exist.""" + return IMPL.network_get_by_uuid(context, uuid) + + def network_get_by_cidr(context, cidr): """Get a network by cidr or raise if it does not exist""" return IMPL.network_get_by_cidr(context, cidr) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 6c3c2d038..5889f5fc3 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -529,7 +529,8 @@ def floating_ip_count_by_project(context, project_id): @require_context -def floating_ip_fixed_ip_associate(context, floating_address, fixed_address): +def floating_ip_fixed_ip_associate(context, floating_address, + fixed_address, host): session = get_session() with session.begin(): # TODO(devcamcar): How to ensure floating_id belongs to user? @@ -540,6 +541,7 @@ def floating_ip_fixed_ip_associate(context, floating_address, fixed_address): fixed_address, session=session) floating_ip_ref.fixed_ip = fixed_ip_ref + floating_ip_ref.host = host floating_ip_ref.save(session=session) @@ -583,6 +585,7 @@ def floating_ip_disassociate(context, address): else: fixed_ip_address = None floating_ip_ref.fixed_ip = None + floating_ip_ref.host = None floating_ip_ref.save(session=session) return fixed_ip_address @@ -669,14 +672,19 @@ def floating_ip_update(context, address, values): @require_admin_context -def fixed_ip_associate(context, address, instance_id, network_id=None): +def fixed_ip_associate(context, address, instance_id, network_id=None, + reserved=False): + """Keyword arguments: + reserved -- should be a boolean value(True or False), exact value will be + used to filter on the fixed ip address + """ session = get_session() with session.begin(): network_or_none = or_(models.FixedIp.network_id == network_id, models.FixedIp.network_id == None) fixed_ip_ref = session.query(models.FixedIp).\ filter(network_or_none).\ - filter_by(reserved=False).\ + filter_by(reserved=reserved).\ filter_by(deleted=False).\ filter_by(address=address).\ with_lockmode('update').\ @@ -945,6 +953,22 @@ def virtual_interface_get_by_address(context, address): @require_context +def virtual_interface_get_by_uuid(context, vif_uuid): + """Gets a virtual interface from the table. + + :param vif_uuid: the uuid of the interface you're looking to get + """ + session = get_session() + vif_ref = session.query(models.VirtualInterface).\ + filter_by(uuid=vif_uuid).\ + options(joinedload('network')).\ + options(joinedload('instance')).\ + options(joinedload('fixed_ips')).\ + first() + return vif_ref + + +@require_context def virtual_interface_get_by_fixed_ip(context, fixed_ip_id): """Gets the virtual interface fixed_ip is associated with. @@ -1858,6 +1882,19 @@ def network_get_by_bridge(context, bridge): @require_admin_context +def network_get_by_uuid(context, uuid): + session = get_session() + result = session.query(models.Network).\ + filter_by(uuid=uuid).\ + filter_by(deleted=False).\ + first() + + if not result: + raise exception.NetworkNotFoundForUUID(uuid=uuid) + return result + + +@require_admin_context def network_get_by_cidr(context, cidr): session = get_session() result = session.query(models.Network).\ diff --git a/nova/db/sqlalchemy/migrate_repo/versions/045_add_network_priority.py b/nova/db/sqlalchemy/migrate_repo/versions/045_add_network_priority.py new file mode 100644 index 000000000..b9b0ea37c --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/045_add_network_priority.py @@ -0,0 +1,44 @@ +# Copyright 2011 Nicira, Inc. +# 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 sqlalchemy import * +from migrate import * + +from nova import log as logging +from nova import utils + + +meta = MetaData() + +networks = Table('networks', meta, + Column("id", Integer(), primary_key=True, nullable=False)) + +# Add priority column to networks table +priority = Column('priority', Integer()) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + + try: + networks.create_column(priority) + except Exception: + logging.error(_("priority column not added to networks table")) + raise + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + networks.drop_column(priority) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/046_add_instance_swap.py b/nova/db/sqlalchemy/migrate_repo/versions/046_add_instance_swap.py new file mode 100644 index 000000000..63e7bc4f9 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/046_add_instance_swap.py @@ -0,0 +1,48 @@ +# Copyright 2011 Isaku Yamahata +# +# 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 sqlalchemy import Column, Integer, MetaData, Table, String + +meta = MetaData() + +default_local_device = Column( + 'default_local_device', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + +default_swap_device = Column( + 'default_swap_device', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(default_local_device) + instances.create_column(default_swap_device) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('default_swap_device') + instances.drop_column('default_local_device') diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 854034f12..b5f30a1e3 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -232,6 +232,8 @@ class Instance(BASE, NovaBase): uuid = Column(String(36)) root_device_name = Column(String(255)) + default_local_device = Column(String(255), nullable=True) + default_swap_device = Column(String(255), nullable=True) config_drive = Column(String(255)) # User editable field meant to represent what ip should be used @@ -628,6 +630,7 @@ class Network(BASE, NovaBase): dhcp_start = Column(String(255)) project_id = Column(String(255)) + priority = Column(Integer) host = Column(String(255)) # , ForeignKey('hosts.id')) uuid = Column(String(36)) diff --git a/nova/exception.py b/nova/exception.py index df6ff25cd..4f25d3721 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -403,10 +403,6 @@ class KernelNotFoundForImage(ImageNotFound): message = _("Kernel not found for image %(image_id)s.") -class RamdiskNotFoundForImage(ImageNotFound): - message = _("Ramdisk not found for image %(image_id)s.") - - class UserNotFound(NotFound): message = _("User %(user_id)s could not be found.") @@ -439,6 +435,10 @@ class NetworkNotFoundForBridge(NetworkNotFound): message = _("Network could not be found for bridge %(bridge)s") +class NetworkNotFoundForUUID(NetworkNotFound): + message = _("Network could not be found for uuid %(uuid)s") + + class NetworkNotFoundForCidr(NetworkNotFound): message = _("Network could not be found with cidr %(cidr)s.") @@ -506,7 +506,7 @@ class FixedIpInvalid(Invalid): message = _("Fixed IP address %(address)s is invalid.") -class NoMoreFixedIps(Error): +class NoMoreFixedIps(NovaException): message = _("Zero fixed ips available.") @@ -810,3 +810,10 @@ class CannotResizeToSmallerSize(NovaException): class ImageTooLarge(NovaException): message = _("Image is larger than instance type allows") + + +class ZoneRequestError(Error): + def __init__(self, message=None): + if message is None: + message = _("1 or more Zones could not complete the request") + super(ZoneRequestError, self).__init__(message=message) diff --git a/nova/image/__init__.py b/nova/image/__init__.py index 5447c8a3a..307b73f01 100644 --- a/nova/image/__init__.py +++ b/nova/image/__init__.py @@ -16,70 +16,20 @@ # under the License. -from urlparse import urlparse - import nova -from nova import exception from nova import utils from nova import flags -from nova.image import glance as glance_image_service +from nova.image import glance FLAGS = flags.FLAGS -GlanceClient = utils.import_class('glance.client.Client') - - -def _parse_image_ref(image_href): - """Parse an image href into composite parts. - - :param image_href: href of an image - :returns: a tuple of the form (image_id, host, port) - :raises ValueError - - """ - o = urlparse(image_href) - port = o.port or 80 - host = o.netloc.split(':', 1)[0] - image_id = int(o.path.split('/')[-1]) - return (image_id, host, port) - - def get_default_image_service(): ImageService = utils.import_class(FLAGS.image_service) return ImageService() -# FIXME(sirp): perhaps this should be moved to nova/images/glance so that we -# keep Glance specific code together for the most part -def get_glance_client(image_href): - """Get the correct glance client and id for the given image_href. - - The image_href param can be an href of the form - http://myglanceserver:9292/images/42, or just an int such as 42. If the - image_href is an int, then flags are used to create the default - glance client. - - :param image_href: image ref/id for an image - :returns: a tuple of the form (glance_client, image_id) - - """ - image_href = image_href or 0 - if str(image_href).isdigit(): - glance_host, glance_port = \ - glance_image_service.pick_glance_api_server() - glance_client = GlanceClient(glance_host, glance_port) - return (glance_client, int(image_href)) - - try: - (image_id, host, port) = _parse_image_ref(image_href) - except ValueError: - raise exception.InvalidImageRef(image_href=image_href) - glance_client = GlanceClient(host, port) - return (glance_client, image_id) - - -def get_image_service(image_href): +def get_image_service(context, image_href): """Get the proper image_service and id for the given image_href. The image_href param can be an href of the form @@ -94,6 +44,6 @@ def get_image_service(image_href): if str(image_href).isdigit(): return (get_default_image_service(), int(image_href)) - (glance_client, image_id) = get_glance_client(image_href) + (glance_client, image_id) = glance.get_glance_client(context, image_href) image_service = nova.image.glance.GlanceImageService(glance_client) return (image_service, image_id) diff --git a/nova/image/fake.py b/nova/image/fake.py index 97af81711..4eceabc11 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -24,7 +24,6 @@ import random from nova import exception from nova import flags from nova import log as logging -from nova.image import service LOG = logging.getLogger('nova.image.fake') @@ -33,7 +32,7 @@ LOG = logging.getLogger('nova.image.fake') FLAGS = flags.FLAGS -class _FakeImageService(service.BaseImageService): +class _FakeImageService(object): """Mock (fake) image service for unit testing.""" def __init__(self): diff --git a/nova/image/glance.py b/nova/image/glance.py index 80abc7384..5ee1d2b8a 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -23,6 +23,7 @@ import copy import datetime import json import random +from urlparse import urlparse from glance.common import exception as glance_exception @@ -30,7 +31,6 @@ from nova import exception from nova import flags from nova import log as logging from nova import utils -from nova.image import service LOG = logging.getLogger('nova.image.glance') @@ -42,6 +42,35 @@ FLAGS = flags.FLAGS GlanceClient = utils.import_class('glance.client.Client') +def _parse_image_ref(image_href): + """Parse an image href into composite parts. + + :param image_href: href of an image + :returns: a tuple of the form (image_id, host, port) + :raises ValueError + + """ + o = urlparse(image_href) + port = o.port or 80 + host = o.netloc.split(':', 1)[0] + image_id = int(o.path.split('/')[-1]) + return (image_id, host, port) + + +def _create_glance_client(context, host, port): + if context.strategy == 'keystone': + # NOTE(dprince): Glance client just needs auth_tok right? Should we + # add username and tenant to the creds below? + creds = {'strategy': 'keystone', + 'username': context.user_id, + 'tenant': context.project_id} + glance_client = GlanceClient(host, port, auth_tok=context.auth_token, + creds=creds) + else: + glance_client = GlanceClient(host, port) + return glance_client + + def pick_glance_api_server(): """Return which Glance API server to use for the request @@ -57,37 +86,47 @@ def pick_glance_api_server(): return host, port -class GlanceImageService(service.BaseImageService): - """Provides storage and retrieval of disk image objects within Glance.""" +def get_glance_client(context, image_href): + """Get the correct glance client and id for the given image_href. + + The image_href param can be an href of the form + http://myglanceserver:9292/images/42, or just an int such as 42. If the + image_href is an int, then flags are used to create the default + glance client. + + :param image_href: image ref/id for an image + :returns: a tuple of the form (glance_client, image_id) + + """ + image_href = image_href or 0 + if str(image_href).isdigit(): + glance_host, glance_port = pick_glance_api_server() + glance_client = _create_glance_client(context, glance_host, + glance_port) + return (glance_client, int(image_href)) + + try: + (image_id, host, port) = _parse_image_ref(image_href) + except ValueError: + raise exception.InvalidImageRef(image_href=image_href) + glance_client = _create_glance_client(context, glance_host, glance_port) + return (glance_client, image_id) - GLANCE_ONLY_ATTRS = ['size', 'location', 'disk_format', - 'container_format', 'checksum'] - # NOTE(sirp): Overriding to use _translate_to_service provided by - # BaseImageService - SERVICE_IMAGE_ATTRS = service.BaseImageService.BASE_IMAGE_ATTRS +\ - GLANCE_ONLY_ATTRS +class GlanceImageService(object): + """Provides storage and retrieval of disk image objects within Glance.""" def __init__(self, client=None): self._client = client - def _get_client(self): + def _get_client(self, context): # NOTE(sirp): we want to load balance each request across glance # servers. Since GlanceImageService is a long-lived object, `client` # is made to choose a new server each time via this property. if self._client is not None: return self._client glance_host, glance_port = pick_glance_api_server() - return GlanceClient(glance_host, glance_port) - - def _set_client(self, client): - self._client = client - - client = property(_get_client, _set_client) - - def _set_client_context(self, context): - """Sets the client's auth token.""" - self.client.set_auth_token(context.auth_token) + return _create_glance_client(context, glance_host, glance_port) def index(self, context, **kwargs): """Calls out to Glance for a list of images available.""" @@ -112,7 +151,7 @@ class GlanceImageService(service.BaseImageService): images = [] for image_meta in image_metas: if self._is_image_available(context, image_meta): - base_image_meta = self._translate_to_base(image_meta) + base_image_meta = self._translate_from_glance(image_meta) images.append(base_image_meta) return images @@ -128,14 +167,14 @@ class GlanceImageService(service.BaseImageService): def _get_images(self, context, **kwargs): """Get image entitites from images service""" - self._set_client_context(context) # ensure filters is a dict kwargs['filters'] = kwargs.get('filters') or {} # NOTE(vish): don't filter out private images kwargs['filters'].setdefault('is_public', 'none') - return self._fetch_images(self.client.get_images_detailed, **kwargs) + client = self._get_client(context) + return self._fetch_images(client.get_images_detailed, **kwargs) def _fetch_images(self, fetch_func, **kwargs): """Paginate through results from glance server""" @@ -168,16 +207,15 @@ class GlanceImageService(service.BaseImageService): def show(self, context, image_id): """Returns a dict with image data for the given opaque image id.""" - self._set_client_context(context) try: - image_meta = self.client.get_image_meta(image_id) + image_meta = self._get_client(context).get_image_meta(image_id) except glance_exception.NotFound: raise exception.ImageNotFound(image_id=image_id) if not self._is_image_available(context, image_meta): raise exception.ImageNotFound(image_id=image_id) - base_image_meta = self._translate_to_base(image_meta) + base_image_meta = self._translate_from_glance(image_meta) return base_image_meta def show_by_name(self, context, name): @@ -192,16 +230,16 @@ class GlanceImageService(service.BaseImageService): def get(self, context, image_id, data): """Calls out to Glance for metadata and data and writes data.""" - self._set_client_context(context) try: - image_meta, image_chunks = self.client.get_image(image_id) + client = self._get_client(context) + image_meta, image_chunks = client.get_image(image_id) except glance_exception.NotFound: raise exception.ImageNotFound(image_id=image_id) for chunk in image_chunks: data.write(chunk) - base_image_meta = self._translate_to_base(image_meta) + base_image_meta = self._translate_from_glance(image_meta) return base_image_meta def create(self, context, image_meta, data=None): @@ -210,19 +248,18 @@ class GlanceImageService(service.BaseImageService): :raises: AlreadyExists if the image already exist. """ - self._set_client_context(context) # Translate Base -> Service LOG.debug(_('Creating image in Glance. Metadata passed in %s'), image_meta) - sent_service_image_meta = self._translate_to_service(image_meta) + sent_service_image_meta = self._translate_to_glance(image_meta) LOG.debug(_('Metadata after formatting for Glance %s'), sent_service_image_meta) - recv_service_image_meta = self.client.add_image( + recv_service_image_meta = self._get_client(context).add_image( sent_service_image_meta, data) # Translate Service -> Base - base_image_meta = self._translate_to_base(recv_service_image_meta) + base_image_meta = self._translate_from_glance(recv_service_image_meta) LOG.debug(_('Metadata returned from Glance formatted for Base %s'), base_image_meta) return base_image_meta @@ -233,16 +270,16 @@ class GlanceImageService(service.BaseImageService): :raises: ImageNotFound if the image does not exist. """ - self._set_client_context(context) # NOTE(vish): show is to check if image is available self.show(context, image_id) - image_meta = _convert_to_string(image_meta) + image_meta = self._translate_to_glance(image_meta) try: - image_meta = self.client.update_image(image_id, image_meta, data) + client = self._get_client(context) + image_meta = client.update_image(image_id, image_meta, data) except glance_exception.NotFound: raise exception.ImageNotFound(image_id=image_id) - base_image_meta = self._translate_to_base(image_meta) + base_image_meta = self._translate_from_glance(image_meta) return base_image_meta def delete(self, context, image_id): @@ -251,11 +288,10 @@ class GlanceImageService(service.BaseImageService): :raises: ImageNotFound if the image does not exist. """ - self._set_client_context(context) # NOTE(vish): show is to check if image is available self.show(context, image_id) try: - result = self.client.delete_image(image_id) + result = self._get_client(context).delete_image(image_id) except glance_exception.NotFound: raise exception.ImageNotFound(image_id=image_id) return result @@ -265,17 +301,14 @@ class GlanceImageService(service.BaseImageService): pass @classmethod - def _translate_to_service(cls, image_meta): - image_meta = super(GlanceImageService, - cls)._translate_to_service(image_meta) + def _translate_to_glance(cls, image_meta): image_meta = _convert_to_string(image_meta) + image_meta = _remove_read_only(image_meta) return image_meta @classmethod - def _translate_to_base(cls, image_meta): - """Override translation to handle conversion to datetime objects.""" - image_meta = service.BaseImageService._propertify_metadata( - image_meta, cls.SERVICE_IMAGE_ATTRS) + def _translate_from_glance(cls, image_meta): + image_meta = _limit_attributes(image_meta) image_meta = _convert_timestamps_to_datetimes(image_meta) image_meta = _convert_from_string(image_meta) return image_meta @@ -285,14 +318,26 @@ class GlanceImageService(service.BaseImageService): """Check image availability. Under Glance, images are always available if the context has - an auth_token. Otherwise, we fall back to the superclass - method. + an auth_token. """ if hasattr(context, 'auth_token') and context.auth_token: return True - return service.BaseImageService._is_image_available(context, - image_meta) + + if image_meta['is_public'] or context.is_admin: + return True + + properties = image_meta['properties'] + + if context.project_id and ('project_id' in properties): + return str(properties['project_id']) == str(context.project_id) + + try: + user_id = properties['user_id'] + except KeyError: + return False + + return str(user_id) == str(context.user_id) # utility functions @@ -352,3 +397,27 @@ def _convert_from_string(metadata): def _convert_to_string(metadata): return _convert(_json_dumps, metadata) + + +def _limit_attributes(image_meta): + IMAGE_ATTRIBUTES = ['size', 'location', 'disk_format', + 'container_format', 'checksum', 'id', + 'name', 'created_at', 'updated_at', + 'deleted_at', 'deleted', 'status', + 'is_public'] + output = {} + for attr in IMAGE_ATTRIBUTES: + output[attr] = image_meta.get(attr) + + output['properties'] = image_meta.get('properties', {}) + + return output + + +def _remove_read_only(image_meta): + IMAGE_ATTRIBUTES = ['updated_at', 'created_at', 'deleted_at'] + output = copy.deepcopy(image_meta) + for attr in IMAGE_ATTRIBUTES: + if attr in output: + del output[attr] + return output diff --git a/nova/image/s3.py b/nova/image/s3.py index abf01a942..343555887 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -34,7 +34,6 @@ from nova import flags from nova import image from nova import log as logging from nova import utils -from nova.image import service from nova.api.ec2 import ec2utils @@ -48,7 +47,7 @@ flags.DEFINE_string('s3_secret_key', 'notchecked', 'secret key to use for s3 server for images') -class S3ImageService(service.BaseImageService): +class S3ImageService(object): """Wraps an existing image service to support s3 based register.""" def __init__(self, service=None, *args, **kwargs): diff --git a/nova/image/service.py b/nova/image/service.py deleted file mode 100644 index 5361cfc89..000000000 --- a/nova/image/service.py +++ /dev/null @@ -1,200 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 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 nova import utils - - -class BaseImageService(object): - """Base class for providing image search and retrieval services. - - ImageService exposes two concepts of metadata: - - 1. First-class attributes: This is metadata that is common to all - ImageService subclasses and is shared across all hypervisors. These - attributes are defined by IMAGE_ATTRS. - - 2. Properties: This is metdata that is specific to an ImageService, - and Image, or a particular hypervisor. Any attribute not present in - BASE_IMAGE_ATTRS should be considered an image property. - - This means that ImageServices will return BASE_IMAGE_ATTRS as keys in the - metadata dict, all other attributes will be returned as keys in the nested - 'properties' dict. - - """ - - BASE_IMAGE_ATTRS = ['id', 'name', 'created_at', 'updated_at', - 'deleted_at', 'deleted', 'status', 'is_public'] - - # NOTE(sirp): ImageService subclasses may override this to aid translation - # between BaseImageService attributes and additional metadata stored by - # the ImageService subclass - SERVICE_IMAGE_ATTRS = [] - - def index(self, context, *args, **kwargs): - """List images. - - :returns: a sequence of mappings with the following signature - {'id': opaque id of image, 'name': name of image} - - """ - raise NotImplementedError - - def detail(self, context, *args, **kwargs): - """Detailed information about an images. - - :returns: a sequence of mappings with the following signature - {'id': opaque id of image, - 'name': name of image, - 'created_at': creation datetime object, - 'updated_at': modification datetime object, - 'deleted_at': deletion datetime object or None, - 'deleted': boolean indicating if image has been deleted, - 'status': string description of image status, - 'is_public': boolean indicating if image is public - } - - If the service does not implement a method that provides a detailed - set of information about images, then the method should raise - NotImplementedError, in which case Nova will emulate this method - with repeated calls to show() for each image received from the - index() method. - - """ - raise NotImplementedError - - def show(self, context, image_id): - """Detailed information about an image. - - :returns: a mapping with the following signature: - {'id': opaque id of image, - 'name': name of image, - 'created_at': creation datetime object, - 'updated_at': modification datetime object, - 'deleted_at': deletion datetime object or None, - 'deleted': boolean indicating if image has been deleted, - 'status': string description of image status, - 'is_public': boolean indicating if image is public - }, ... - - :raises: NotFound if the image does not exist - - """ - raise NotImplementedError - - def get(self, context, data): - """Get an image. - - :param data: a file-like object to hold binary image data - :returns: a dict containing image metadata, writes image data to data. - :raises: NotFound if the image does not exist - - """ - raise NotImplementedError - - def create(self, context, metadata, data=None): - """Store the image metadata and data. - - :returns: the new image metadata. - :raises: AlreadyExists if the image already exist. - - """ - raise NotImplementedError - - def update(self, context, image_id, metadata, data=None): - """Update the given image metadata and data and return the metadata. - - :raises: NotFound if the image does not exist. - - """ - raise NotImplementedError - - def delete(self, context, image_id): - """Delete the given image. - - :raises: NotFound if the image does not exist. - - """ - raise NotImplementedError - - @staticmethod - def _is_image_available(context, image_meta): - """Check image availability. - - Images are always available if they are public or if the user is an - admin. - - Otherwise, we filter by project_id (if present) and then fall-back to - images owned by user. - - """ - # FIXME(sirp): We should be filtering by user_id on the Glance side - # for security; however, we can't do that until we get authn/authz - # sorted out. Until then, filtering in Nova. - if image_meta['is_public'] or context.is_admin: - return True - - properties = image_meta['properties'] - - if context.project_id and ('project_id' in properties): - return str(properties['project_id']) == str(context.project_id) - - try: - user_id = properties['user_id'] - except KeyError: - return False - - return str(user_id) == str(context.user_id) - - @classmethod - def _translate_to_base(cls, metadata): - """Return a metadata dictionary that is BaseImageService compliant. - - This is used by subclasses to expose only a metadata dictionary that - is the same across ImageService implementations. - - """ - return cls._propertify_metadata(metadata, cls.BASE_IMAGE_ATTRS) - - @classmethod - def _translate_to_service(cls, metadata): - """Return a metadata dict that is usable by the ImageService subclass. - - As an example, Glance has additional attributes (like 'location'); the - BaseImageService considers these properties, but we need to translate - these back to first-class attrs for sending to Glance. This method - handles this by allowing you to specify the attributes an ImageService - considers first-class. - - """ - if not cls.SERVICE_IMAGE_ATTRS: - raise NotImplementedError(_('Cannot use this without specifying ' - 'SERVICE_IMAGE_ATTRS for subclass')) - return cls._propertify_metadata(metadata, cls.SERVICE_IMAGE_ATTRS) - - @staticmethod - def _propertify_metadata(metadata, keys): - """Move unknown keys to a nested 'properties' dict. - - :returns: a new dict with the keys moved. - - """ - flattened = utils.flatten_dict(metadata) - attributes, properties = utils.partition_dict(flattened, keys) - attributes['properties'] = properties - return attributes diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 57c1d0c28..7d89b2bcc 100644..100755 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -68,6 +68,9 @@ flags.DEFINE_string('linuxnet_interface_driver', 'Driver used to create ethernet devices.') flags.DEFINE_string('linuxnet_ovs_integration_bridge', 'br-int', 'Name of Open vSwitch bridge used with linuxnet') +flags.DEFINE_bool('use_single_default_gateway', + False, 'Use single default gateway. Only first nic of vm' + ' will get default gateway from dhcp server') binary_name = os.path.basename(inspect.stack()[-1][1]) @@ -511,6 +514,32 @@ def get_dhcp_hosts(context, network_ref): return '\n'.join(hosts) +def get_dhcp_opts(context, network_ref): + """Get network's hosts config in dhcp-opts format.""" + hosts = [] + ips_ref = db.network_get_associated_fixed_ips(context, network_ref['id']) + + if ips_ref: + #set of instance ids + instance_set = set([fixed_ip_ref['instance_id'] + for fixed_ip_ref in ips_ref]) + default_gw_network_node = {} + for instance_id in instance_set: + vifs = db.virtual_interface_get_by_instance(context, instance_id) + if vifs: + #offer a default gateway to the first virtual interface + default_gw_network_node[instance_id] = vifs[0]['network_id'] + + for fixed_ip_ref in ips_ref: + instance_id = fixed_ip_ref['instance_id'] + if instance_id in default_gw_network_node: + target_network_id = default_gw_network_node[instance_id] + # we don't want default gateway for this fixed ip + if target_network_id != fixed_ip_ref['network_id']: + hosts.append(_host_dhcp_opts(fixed_ip_ref)) + return '\n'.join(hosts) + + # NOTE(ja): Sending a HUP only reloads the hostfile, so any # configuration options (like dchp-range, vlan, ...) # aren't reloaded. @@ -526,6 +555,12 @@ def update_dhcp(context, dev, network_ref): with open(conffile, 'w') as f: f.write(get_dhcp_hosts(context, network_ref)) + if FLAGS.use_single_default_gateway: + optsfile = _dhcp_file(dev, 'opts') + with open(optsfile, 'w') as f: + f.write(get_dhcp_opts(context, network_ref)) + os.chmod(optsfile, 0644) + # Make sure dnsmasq can actually read it (it setuid()s to "nobody") os.chmod(conffile, 0644) @@ -563,6 +598,9 @@ def update_dhcp(context, dev, network_ref): if FLAGS.dns_server: cmd += ['-h', '-R', '--server=%s' % FLAGS.dns_server] + if FLAGS.use_single_default_gateway: + cmd += ['--dhcp-optsfile=%s' % _dhcp_file(dev, 'opts')] + _execute(*cmd, run_as_root=True) @@ -625,13 +663,32 @@ def _host_lease(fixed_ip_ref): instance_ref['hostname'] or '*') +def _host_dhcp_network(fixed_ip_ref): + instance_ref = fixed_ip_ref['instance'] + return 'NW-i%08d-%s' % (instance_ref['id'], + fixed_ip_ref['network_id']) + + def _host_dhcp(fixed_ip_ref): """Return a host string for an address in dhcp-host format.""" instance_ref = fixed_ip_ref['instance'] - return '%s,%s.%s,%s' % (fixed_ip_ref['virtual_interface']['address'], - instance_ref['hostname'], - FLAGS.dhcp_domain, - fixed_ip_ref['address']) + vif = fixed_ip_ref['virtual_interface'] + if FLAGS.use_single_default_gateway: + return '%s,%s.%s,%s,%s' % (vif['address'], + instance_ref['hostname'], + FLAGS.dhcp_domain, + fixed_ip_ref['address'], + "net:" + _host_dhcp_network(fixed_ip_ref)) + else: + return '%s,%s.%s,%s' % (vif['address'], + instance_ref['hostname'], + FLAGS.dhcp_domain, + fixed_ip_ref['address']) + + +def _host_dhcp_opts(fixed_ip_ref): + """Return a host string for an address in dhcp-host format.""" + return '%s,%s' % (_host_dhcp_network(fixed_ip_ref), 3) def _execute(*cmd, **kwargs): diff --git a/nova/network/manager.py b/nova/network/manager.py index e6b30d1a0..70e51888f 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -74,7 +74,7 @@ flags.DEFINE_string('flat_network_bridge', None, 'Bridge for simple network instances') flags.DEFINE_string('flat_network_dns', '8.8.4.4', 'Dns for simple network') -flags.DEFINE_bool('flat_injected', True, +flags.DEFINE_bool('flat_injected', False, 'Whether to attempt to inject network setup into guest') flags.DEFINE_string('flat_interface', None, 'FlatDhcp will bridge into this interface if set') @@ -128,8 +128,8 @@ class RPCAllocateFixedIP(object): """Calls allocate_fixed_ip once for each network.""" green_pool = greenpool.GreenPool() - vpn = kwargs.pop('vpn') - requested_networks = kwargs.pop('requested_networks') + vpn = kwargs.get('vpn') + requested_networks = kwargs.get('requested_networks') for network in networks: address = None @@ -289,7 +289,8 @@ class FloatingIP(object): self.db.floating_ip_fixed_ip_associate(context, floating_address, - fixed_address) + fixed_address, + self.host) self.driver.bind_floating_ip(floating_address) self.driver.ensure_floating_forward(floating_address, fixed_address) @@ -448,7 +449,7 @@ class NetworkManager(manager.SchedulerDependentManager): try: fixed_ips = kwargs.get('fixed_ips') or \ self.db.fixed_ip_get_by_instance(context, instance_id) - except exceptions.FixedIpNotFoundForInstance: + except exception.FixedIpNotFoundForInstance: fixed_ips = [] LOG.debug(_("network deallocation for instance |%s|"), instance_id, context=context) @@ -484,6 +485,9 @@ class NetworkManager(manager.SchedulerDependentManager): for vif in vifs: network = vif['network'] + if network is None: + continue + # determine which of the instance's IPs belong to this network network_IPs = [fixed_ip['address'] for fixed_ip in fixed_ips if fixed_ip['network_id'] == network['id']] @@ -546,21 +550,23 @@ class NetworkManager(manager.SchedulerDependentManager): def _allocate_mac_addresses(self, context, instance_id, networks): """Generates mac addresses and creates vif rows in db for them.""" for network in networks: - vif = {'address': self.generate_mac_address(), + self.add_virtual_interface(context, instance_id, network['id']) + + def add_virtual_interface(self, context, instance_id, network_id): + vif = {'address': self.generate_mac_address(), 'instance_id': instance_id, - 'network_id': network['id'], + 'network_id': network_id, 'uuid': str(utils.gen_uuid())} - # try FLAG times to create a vif record with a unique mac_address - for i in range(FLAGS.create_unique_mac_address_attempts): - try: - self.db.virtual_interface_create(context, vif) - break - except exception.VirtualInterfaceCreateException: - vif['address'] = self.generate_mac_address() - else: - self.db.virtual_interface_delete_by_instance(context, + # try FLAG times to create a vif record with a unique mac_address + for _ in xrange(FLAGS.create_unique_mac_address_attempts): + try: + return self.db.virtual_interface_create(context, vif) + except exception.VirtualInterfaceCreateException: + vif['address'] = self.generate_mac_address() + else: + self.db.virtual_interface_delete_by_instance(context, instance_id) - raise exception.VirtualInterfaceMacAddressException() + raise exception.VirtualInterfaceMacAddressException() def generate_mac_address(self): """Generate an Ethernet MAC address.""" @@ -789,6 +795,15 @@ class NetworkManager(manager.SchedulerDependentManager): self._create_fixed_ips(context, network['id']) return networks + def delete_network(self, context, fixed_range, require_disassociated=True): + + network = db.network_get_by_cidr(context, fixed_range) + + if require_disassociated and network.project_id is not None: + raise ValueError(_('Network must be disassociated from project %s' + ' before delete' % network.project_id)) + db.network_delete_safe(context, network.id) + @property def _bottom_reserved_ips(self): # pylint: disable=R0201 """Number of reserved ips at the bottom of the range.""" @@ -890,7 +905,7 @@ class FlatManager(NetworkManager): def _allocate_fixed_ips(self, context, instance_id, host, networks, **kwargs): """Calls allocate_fixed_ip once for each network.""" - requested_networks = kwargs.pop('requested_networks') + requested_networks = kwargs.get('requested_networks') for network in networks: address = None if requested_networks is not None: @@ -991,7 +1006,8 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): address = network['vpn_private_address'] self.db.fixed_ip_associate(context, address, - instance_id) + instance_id, + reserved=True) else: address = kwargs.get('address', None) if address: diff --git a/nova/network/quantum/__init__.py b/nova/network/quantum/__init__.py new file mode 100644 index 000000000..f7fbfb511 --- /dev/null +++ b/nova/network/quantum/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira Networks +# 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. diff --git a/nova/network/quantum/client.py b/nova/network/quantum/client.py new file mode 100644 index 000000000..40c68dfdc --- /dev/null +++ b/nova/network/quantum/client.py @@ -0,0 +1,307 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# 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. +# @author: Tyler Smith, Cisco Systems + +import httplib +import json +import socket +import urllib + + +# FIXME(danwent): All content in this file should be removed once the +# packaging work for the quantum client libraries is complete. +# At that point, we will be able to just install the libraries as a +# dependency and import from quantum.client.* and quantum.common.* +# Until then, we have simplified versions of these classes in this file. + +class JSONSerializer(object): + """This is a simple json-only serializer to use until we can just grab + the standard serializer from the quantum library. + """ + def serialize(self, data, content_type): + try: + return json.dumps(data) + except TypeError: + pass + return json.dumps(to_primitive(data)) + + def deserialize(self, data, content_type): + return json.loads(data) + + +# The full client lib will expose more +# granular exceptions, for now, just try to distinguish +# between the cases we care about. +class QuantumNotFoundException(Exception): + """Indicates that Quantum Server returned 404""" + pass + + +class QuantumServerException(Exception): + """Indicates any non-404 error from Quantum Server""" + pass + + +class QuantumIOException(Exception): + """Indicates network IO trouble reaching Quantum Server""" + pass + + +class api_call(object): + """A Decorator to add support for format and tenant overriding""" + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + def with_params(*args, **kwargs): + """Temporarily set format and tenant for this request""" + (format, tenant) = (instance.format, instance.tenant) + + if 'format' in kwargs: + instance.format = kwargs['format'] + if 'tenant' in kwargs: + instance.tenant = kwargs['tenant'] + + ret = None + try: + ret = self.func(instance, *args) + finally: + (instance.format, instance.tenant) = (format, tenant) + return ret + return with_params + + +class Client(object): + """A base client class - derived from Glance.BaseClient""" + + action_prefix = '/v1.0/tenants/{tenant_id}' + + """Action query strings""" + networks_path = "/networks" + network_path = "/networks/%s" + ports_path = "/networks/%s/ports" + port_path = "/networks/%s/ports/%s" + attachment_path = "/networks/%s/ports/%s/attachment" + + def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None, + format="xml", testing_stub=None, key_file=None, + cert_file=None, logger=None): + """Creates a new client to some service. + + :param host: The host where service resides + :param port: The port where service resides + :param use_ssl: True to use SSL, False to use HTTP + :param tenant: The tenant ID to make requests with + :param format: The format to query the server with + :param testing_stub: A class that stubs basic server methods for tests + :param key_file: The SSL key file to use if use_ssl is true + :param cert_file: The SSL cert file to use if use_ssl is true + """ + self.host = host + self.port = port + self.use_ssl = use_ssl + self.tenant = tenant + self.format = format + self.connection = None + self.testing_stub = testing_stub + self.key_file = key_file + self.cert_file = cert_file + self.logger = logger + + def get_connection_type(self): + """Returns the proper connection type""" + if self.testing_stub: + return self.testing_stub + elif self.use_ssl: + return httplib.HTTPSConnection + else: + return httplib.HTTPConnection + + def do_request(self, method, action, body=None, + headers=None, params=None): + """Connects to the server and issues a request. + Returns the result data, or raises an appropriate exception if + HTTP status code is not 2xx + + :param method: HTTP method ("GET", "POST", "PUT", etc...) + :param body: string of data to send, or None (default) + :param headers: mapping of key/value pairs to add as headers + :param params: dictionary of key/value pairs to add to append + to action + """ + + # Ensure we have a tenant id + if not self.tenant: + raise Exception(_("Tenant ID not set")) + + # Add format and tenant_id + action += ".%s" % self.format + action = Client.action_prefix + action + action = action.replace('{tenant_id}', self.tenant) + + if type(params) is dict: + action += '?' + urllib.urlencode(params) + + try: + connection_type = self.get_connection_type() + headers = headers or {"Content-Type": + "application/%s" % self.format} + + # Open connection and send request, handling SSL certs + certs = {'key_file': self.key_file, 'cert_file': self.cert_file} + certs = dict((x, certs[x]) for x in certs if certs[x] != None) + + if self.use_ssl and len(certs): + c = connection_type(self.host, self.port, **certs) + else: + c = connection_type(self.host, self.port) + + if self.logger: + self.logger.debug( + _("Quantum Client Request:\n%(method)s %(action)s\n" % + locals())) + if body: + self.logger.debug(body) + + c.request(method, action, body, headers) + res = c.getresponse() + status_code = self.get_status_code(res) + data = res.read() + + if self.logger: + self.logger.debug("Quantum Client Reply (code = %s) :\n %s" \ + % (str(status_code), data)) + + if status_code == httplib.NOT_FOUND: + raise QuantumNotFoundException( + _("Quantum entity not found: %s" % data)) + + if status_code in (httplib.OK, + httplib.CREATED, + httplib.ACCEPTED, + httplib.NO_CONTENT): + if data is not None and len(data): + return self.deserialize(data, status_code) + else: + raise QuantumServerException( + _("Server %(status_code)s error: %(data)s" + % locals())) + + except (socket.error, IOError), e: + raise QuantumIOException(_("Unable to connect to " + "server. Got error: %s" % e)) + + def get_status_code(self, response): + """Returns the integer status code from the response, which + can be either a Webob.Response (used in testing) or httplib.Response + """ + if hasattr(response, 'status_int'): + return response.status_int + else: + return response.status + + def serialize(self, data): + if not data: + return None + elif type(data) is dict: + return JSONSerializer().serialize(data, self.content_type()) + else: + raise Exception(_("unable to deserialize object of type = '%s'" % + type(data))) + + def deserialize(self, data, status_code): + if status_code == 202: + return data + return JSONSerializer().deserialize(data, self.content_type()) + + def content_type(self, format=None): + if not format: + format = self.format + return "application/%s" % (format) + + @api_call + def list_networks(self): + """Fetches a list of all networks for a tenant""" + return self.do_request("GET", self.networks_path) + + @api_call + def show_network_details(self, network): + """Fetches the details of a certain network""" + return self.do_request("GET", self.network_path % (network)) + + @api_call + def create_network(self, body=None): + """Creates a new network""" + body = self.serialize(body) + return self.do_request("POST", self.networks_path, body=body) + + @api_call + def update_network(self, network, body=None): + """Updates a network""" + body = self.serialize(body) + return self.do_request("PUT", self.network_path % (network), body=body) + + @api_call + def delete_network(self, network): + """Deletes the specified network""" + return self.do_request("DELETE", self.network_path % (network)) + + @api_call + def list_ports(self, network): + """Fetches a list of ports on a given network""" + return self.do_request("GET", self.ports_path % (network)) + + @api_call + def show_port_details(self, network, port): + """Fetches the details of a certain port""" + return self.do_request("GET", self.port_path % (network, port)) + + @api_call + def create_port(self, network, body=None): + """Creates a new port on a given network""" + body = self.serialize(body) + return self.do_request("POST", self.ports_path % (network), body=body) + + @api_call + def delete_port(self, network, port): + """Deletes the specified port from a network""" + return self.do_request("DELETE", self.port_path % (network, port)) + + @api_call + def set_port_state(self, network, port, body=None): + """Sets the state of the specified port""" + body = self.serialize(body) + return self.do_request("PUT", + self.port_path % (network, port), body=body) + + @api_call + def show_port_attachment(self, network, port): + """Fetches the attachment-id associated with the specified port""" + return self.do_request("GET", self.attachment_path % (network, port)) + + @api_call + def attach_resource(self, network, port, body=None): + """Sets the attachment-id of the specified port""" + body = self.serialize(body) + return self.do_request("PUT", + self.attachment_path % (network, port), body=body) + + @api_call + def detach_resource(self, network, port): + """Removes the attachment-id of the specified port""" + return self.do_request("DELETE", + self.attachment_path % (network, port)) diff --git a/nova/network/quantum/manager.py b/nova/network/quantum/manager.py new file mode 100644 index 000000000..23a9aba0d --- /dev/null +++ b/nova/network/quantum/manager.py @@ -0,0 +1,324 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira Networks, Inc +# 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 nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import manager +from nova.network import manager +from nova.network.quantum import quantum_connection +from nova import utils + +LOG = logging.getLogger("nova.network.quantum.manager") + +FLAGS = flags.FLAGS + +flags.DEFINE_string('quantum_ipam_lib', + 'nova.network.quantum.nova_ipam_lib', + "Indicates underlying IP address management library") + + +class QuantumManager(manager.FlatManager): + """NetworkManager class that communicates with a Quantum service + via a web services API to provision VM network connectivity. + + For IP Address management, QuantumManager can be configured to + use either Nova's local DB or the Melange IPAM service. + + Currently, the QuantumManager does NOT support any of the 'gateway' + functionality implemented by the Nova VlanManager, including: + * floating IPs + * DHCP + * NAT gateway + + Support for these capabilities are targted for future releases. + """ + + def __init__(self, q_conn=None, ipam_lib=None, *args, **kwargs): + """Initialize two key libraries, the connection to a + Quantum service, and the library for implementing IPAM. + + Calls inherited FlatManager constructor. + """ + + if not q_conn: + q_conn = quantum_connection.QuantumClientConnection() + self.q_conn = q_conn + + if not ipam_lib: + ipam_lib = FLAGS.quantum_ipam_lib + self.ipam = utils.import_object(ipam_lib).get_ipam_lib(self) + + super(QuantumManager, self).__init__(*args, **kwargs) + + def create_networks(self, context, label, cidr, multi_host, num_networks, + network_size, cidr_v6, gateway_v6, bridge, + bridge_interface, dns1=None, dns2=None, uuid=None, + **kwargs): + """Unlike other NetworkManagers, with QuantumManager, each + create_networks calls should create only a single network. + + Two scenarios exist: + - no 'uuid' is specified, in which case we contact + Quantum and create a new network. + - an existing 'uuid' is specified, corresponding to + a Quantum network created out of band. + + In both cases, we initialize a subnet using the IPAM lib. + """ + if num_networks != 1: + raise Exception(_("QuantumManager requires that only one" + " network is created per call")) + q_tenant_id = kwargs["project_id"] or FLAGS.quantum_default_tenant_id + quantum_net_id = uuid + if quantum_net_id: + if not self.q_conn.network_exists(q_tenant_id, quantum_net_id): + raise Exception(_("Unable to find existing quantum " \ + " network for tenant '%(q_tenant_id)s' with " + "net-id '%(quantum_net_id)s'" % locals())) + else: + # otherwise, create network from default quantum pool + quantum_net_id = self.q_conn.create_network(q_tenant_id, label) + + ipam_tenant_id = kwargs.get("project_id", None) + priority = kwargs.get("priority", 0) + self.ipam.create_subnet(context, label, ipam_tenant_id, quantum_net_id, + priority, cidr, gateway_v6, cidr_v6, dns1, dns2) + + def delete_network(self, context, fixed_range): + """Lookup network by IPv4 cidr, delete both the IPAM + subnet and the corresponding Quantum network. + """ + project_id = context.project_id + quantum_net_id = self.ipam.get_network_id_by_cidr( + context, fixed_range, project_id) + self.ipam.delete_subnets_by_net_id(context, quantum_net_id, + project_id) + q_tenant_id = project_id or FLAGS.quantum_default_tenant_id + self.q_conn.delete_network(q_tenant_id, quantum_net_id) + + def allocate_for_instance(self, context, **kwargs): + """Called by compute when it is creating a new VM. + + There are three key tasks: + - Determine the number and order of vNICs to create + - Allocate IP addresses + - Create ports on a Quantum network and attach vNICs. + + We support two approaches to determining vNICs: + - By default, a VM gets a vNIC for any network belonging + to the VM's project, and a vNIC for any "global" network + that has a NULL project_id. vNIC order is determined + by the network's 'priority' field. + - If the 'os-create-server-ext' was used to create the VM, + only the networks in 'requested_networks' are used to + create vNICs, and the vNIC order is determiend by the + order in the requested_networks array. + + For each vNIC, use the FlatManager to create the entries + in the virtual_interfaces table, contact Quantum to + create a port and attachment the vNIC, and use the IPAM + lib to allocate IP addresses. + """ + instance_id = kwargs.pop('instance_id') + instance_type_id = kwargs['instance_type_id'] + host = kwargs.pop('host') + project_id = kwargs.pop('project_id') + LOG.debug(_("network allocations for instance %s"), instance_id) + + requested_networks = kwargs.get('requested_networks') + + if requested_networks: + net_proj_pairs = [(net_id, project_id) \ + for (net_id, _i) in requested_networks] + else: + net_proj_pairs = self.ipam.get_project_and_global_net_ids(context, + project_id) + + # Create a port via quantum and attach the vif + for (quantum_net_id, project_id) in net_proj_pairs: + + # FIXME(danwent): We'd like to have the manager be + # completely decoupled from the nova networks table. + # However, other parts of nova sometimes go behind our + # back and access network data directly from the DB. So + # for now, the quantum manager knows that there is a nova + # networks DB table and accesses it here. updating the + # virtual_interfaces table to use UUIDs would be one + # solution, but this would require significant work + # elsewhere. + admin_context = context.elevated() + network_ref = db.network_get_by_uuid(admin_context, + quantum_net_id) + + vif_rec = manager.FlatManager.add_virtual_interface(self, + context, instance_id, network_ref['id']) + + # talk to Quantum API to create and attach port. + q_tenant_id = project_id or FLAGS.quantum_default_tenant_id + self.q_conn.create_and_attach_port(q_tenant_id, quantum_net_id, + vif_rec['uuid']) + self.ipam.allocate_fixed_ip(context, project_id, quantum_net_id, + vif_rec) + + return self.get_instance_nw_info(context, instance_id, + instance_type_id, host) + + def get_instance_nw_info(self, context, instance_id, + instance_type_id, host): + """This method is used by compute to fetch all network data + that should be used when creating the VM. + + The method simply loops through all virtual interfaces + stored in the nova DB and queries the IPAM lib to get + the associated IP data. + + The format of returned data is 'defined' by the initial + set of NetworkManagers found in nova/network/manager.py . + Ideally this 'interface' will be more formally defined + in the future. + """ + network_info = [] + instance = db.instance_get(context, instance_id) + project_id = instance.project_id + + admin_context = context.elevated() + vifs = db.virtual_interface_get_by_instance(admin_context, + instance_id) + for vif in vifs: + q_tenant_id = project_id + ipam_tenant_id = project_id + net_id, port_id = self.q_conn.get_port_by_attachment(q_tenant_id, + vif['uuid']) + if not net_id: + q_tenant_id = FLAGS.quantum_default_tenant_id + ipam_tenant_id = None + net_id, port_id = self.q_conn.get_port_by_attachment( + q_tenant_id, vif['uuid']) + if not net_id: + # TODO(bgh): We need to figure out a way to tell if we + # should actually be raising this exception or not. + # In the case that a VM spawn failed it may not have + # attached the vif and raising the exception here + # prevents deletion of the VM. In that case we should + # probably just log, continue, and move on. + raise Exception(_("No network for for virtual interface %s") % + vif['uuid']) + (v4_subnet, v6_subnet) = self.ipam.get_subnets_by_net_id(context, + ipam_tenant_id, net_id) + v4_ips = self.ipam.get_v4_ips_by_interface(context, + net_id, vif['uuid'], + project_id=ipam_tenant_id) + v6_ips = self.ipam.get_v6_ips_by_interface(context, + net_id, vif['uuid'], + project_id=ipam_tenant_id) + + quantum_net_id = v4_subnet['network_id'] or v6_subnet['network_id'] + + def ip_dict(ip, subnet): + return { + "ip": ip, + "netmask": subnet["netmask"], + "enabled": "1"} + + network_dict = { + 'cidr': v4_subnet['cidr'], + 'injected': True, + 'multi_host': False} + + info = { + 'gateway': v4_subnet['gateway'], + 'dhcp_server': v4_subnet['gateway'], + 'broadcast': v4_subnet['broadcast'], + 'mac': vif['address'], + 'vif_uuid': vif['uuid'], + 'dns': [], + 'ips': [ip_dict(ip, v4_subnet) for ip in v4_ips]} + + if v6_subnet: + if v6_subnet['cidr']: + network_dict['cidr_v6'] = v6_subnet['cidr'] + info['ip6s'] = [ip_dict(ip, v6_subnet) for ip in v6_ips] + + if v6_subnet['gateway']: + info['gateway6'] = v6_subnet['gateway'] + + dns_dict = {} + for s in [v4_subnet, v6_subnet]: + for k in ['dns1', 'dns2']: + if s and s[k]: + dns_dict[s[k]] = None + info['dns'] = [d for d in dns_dict.keys()] + + network_info.append((network_dict, info)) + return network_info + + def deallocate_for_instance(self, context, **kwargs): + """Called when a VM is terminated. Loop through each virtual + interface in the Nova DB and remove the Quantum port and + clear the IP allocation using the IPAM. Finally, remove + the virtual interfaces from the Nova DB. + """ + instance_id = kwargs.get('instance_id') + project_id = kwargs.pop('project_id', None) + + admin_context = context.elevated() + vifs = db.virtual_interface_get_by_instance(admin_context, + instance_id) + for vif_ref in vifs: + interface_id = vif_ref['uuid'] + q_tenant_id = project_id + ipam_tenant_id = project_id + (net_id, port_id) = self.q_conn.get_port_by_attachment(q_tenant_id, + interface_id) + if not net_id: + q_tenant_id = FLAGS.quantum_default_tenant_id + ipam_tenant_id = None + (net_id, port_id) = self.q_conn.get_port_by_attachment( + q_tenant_id, interface_id) + if not net_id: + LOG.error("Unable to find port with attachment: %s" % + (interface_id)) + continue + self.q_conn.detach_and_delete_port(q_tenant_id, + net_id, port_id) + + self.ipam.deallocate_ips_by_vif(context, ipam_tenant_id, + net_id, vif_ref) + + try: + db.virtual_interface_delete_by_instance(admin_context, + instance_id) + except exception.InstanceNotFound: + LOG.error(_("Attempted to deallocate non-existent instance: %s" % + (instance_id))) + + def validate_networks(self, context, networks): + """Validates that this tenant has quantum networks with the associated + UUIDs. This is called by the 'os-create-server-ext' API extension + code so that we can return an API error code to the caller if they + request an invalid network. + """ + if networks is None: + return + + project_id = context.project_id + for (net_id, _i) in networks: + self.ipam.verify_subnet_exists(context, project_id, net_id) + if not self.q_conn.network_exists(project_id, net_id): + raise exception.NetworkNotFound(network_id=net_id) diff --git a/nova/network/quantum/melange_connection.py b/nova/network/quantum/melange_connection.py new file mode 100644 index 000000000..71ac9b5f1 --- /dev/null +++ b/nova/network/quantum/melange_connection.py @@ -0,0 +1,141 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 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. + +import httplib +import socket +import urllib +import json + +from nova import flags + + +FLAGS = flags.FLAGS + +flags.DEFINE_string('melange_host', + '127.0.0.1', + 'HOST for connecting to melange') + +flags.DEFINE_string('melange_port', + '9898', + 'PORT for connecting to melange') + +json_content_type = {'Content-type': "application/json"} + + +# FIXME(danwent): talk to the Melange folks about creating a +# client lib that we can import as a library, instead of +# have to have all of the client code in here. +class MelangeConnection(object): + + def __init__(self, host=None, port=None, use_ssl=False): + if host is None: + host = FLAGS.melange_host + if port is None: + port = int(FLAGS.melange_port) + self.host = host + self.port = port + self.use_ssl = use_ssl + self.version = "v0.1" + + def get(self, path, params=None, headers=None): + return self.do_request("GET", path, params=params, headers=headers) + + def post(self, path, body=None, headers=None): + return self.do_request("POST", path, body=body, headers=headers) + + def delete(self, path, headers=None): + return self.do_request("DELETE", path, headers=headers) + + def _get_connection(self): + if self.use_ssl: + return httplib.HTTPSConnection(self.host, self.port) + else: + return httplib.HTTPConnection(self.host, self.port) + + def do_request(self, method, path, body=None, headers=None, params=None): + headers = headers or {} + params = params or {} + + url = "/%s/%s.json" % (self.version, path) + if params: + url += "?%s" % urllib.urlencode(params) + try: + connection = self._get_connection() + connection.request(method, url, body, headers) + response = connection.getresponse() + response_str = response.read() + if response.status < 400: + return response_str + raise Exception(_("Server returned error: %s" % response_str)) + except (socket.error, IOError), e: + raise Exception(_("Unable to connect to " + "server. Got error: %s" % e)) + + def allocate_ip(self, network_id, vif_id, + project_id=None, mac_address=None): + tenant_scope = "/tenants/%s" % project_id if project_id else "" + request_body = (json.dumps(dict(network=dict(mac_address=mac_address, + tenant_id=project_id))) + if mac_address else None) + url = ("ipam%(tenant_scope)s/networks/%(network_id)s/" + "interfaces/%(vif_id)s/ip_allocations" % locals()) + response = self.post(url, body=request_body, + headers=json_content_type) + return json.loads(response)['ip_addresses'] + + def create_block(self, network_id, cidr, + project_id=None, dns1=None, dns2=None): + tenant_scope = "/tenants/%s" % project_id if project_id else "" + + url = "ipam%(tenant_scope)s/ip_blocks" % locals() + + req_params = dict(ip_block=dict(cidr=cidr, network_id=network_id, + type='private', dns1=dns1, dns2=dns2)) + self.post(url, body=json.dumps(req_params), + headers=json_content_type) + + def delete_block(self, block_id, project_id=None): + tenant_scope = "/tenants/%s" % project_id if project_id else "" + + url = "ipam%(tenant_scope)s/ip_blocks/%(block_id)s" % locals() + + self.delete(url, headers=json_content_type) + + def get_blocks(self, project_id=None): + tenant_scope = "/tenants/%s" % project_id if project_id else "" + + url = "ipam%(tenant_scope)s/ip_blocks" % locals() + + response = self.get(url, headers=json_content_type) + return json.loads(response) + + def get_allocated_ips(self, network_id, vif_id, project_id=None): + tenant_scope = "/tenants/%s" % project_id if project_id else "" + + url = ("ipam%(tenant_scope)s/networks/%(network_id)s/" + "interfaces/%(vif_id)s/ip_allocations" % locals()) + + response = self.get(url, headers=json_content_type) + return json.loads(response)['ip_addresses'] + + def deallocate_ips(self, network_id, vif_id, project_id=None): + tenant_scope = "/tenants/%s" % project_id if project_id else "" + + url = ("ipam%(tenant_scope)s/networks/%(network_id)s/" + "interfaces/%(vif_id)s/ip_allocations" % locals()) + + self.delete(url, headers=json_content_type) diff --git a/nova/network/quantum/melange_ipam_lib.py b/nova/network/quantum/melange_ipam_lib.py new file mode 100644 index 000000000..a0ac10fd3 --- /dev/null +++ b/nova/network/quantum/melange_ipam_lib.py @@ -0,0 +1,205 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira Networks, Inc +# 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 netaddr import IPNetwork + +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova.network.quantum import melange_connection + + +LOG = logging.getLogger("nova.network.quantum.melange_ipam_lib") + +FLAGS = flags.FLAGS + + +def get_ipam_lib(net_man): + return QuantumMelangeIPAMLib() + + +class QuantumMelangeIPAMLib(object): + """Implements Quantum IP Address Management (IPAM) interface + using the Melange service, which is access using the Melange + web services API. + """ + + def __init__(self): + """Initialize class used to connect to Melange server""" + self.m_conn = melange_connection.MelangeConnection() + + def create_subnet(self, context, label, project_id, + quantum_net_id, priority, cidr=None, + gateway_v6=None, cidr_v6=None, + dns1=None, dns2=None): + """Contact Melange and create a subnet for any non-NULL + IPv4 or IPv6 subnets. + + Also create a entry in the Nova networks DB, but only + to store values not represented in Melange or to + temporarily provide compatibility with Nova code that + accesses IPAM data directly via the DB (e.g., nova-api) + """ + tenant_id = project_id or FLAGS.quantum_default_tenant_id + if cidr: + self.m_conn.create_block(quantum_net_id, cidr, + project_id=tenant_id, + dns1=dns1, dns2=dns2) + if cidr_v6: + self.m_conn.create_block(quantum_net_id, cidr_v6, + project_id=tenant_id, + dns1=dns1, dns2=dns2) + + net = {"uuid": quantum_net_id, + "project_id": project_id, + "priority": priority, + "label": label} + admin_context = context.elevated() + network = db.network_create_safe(admin_context, net) + + def allocate_fixed_ip(self, context, project_id, quantum_net_id, vif_ref): + """Pass call to allocate fixed IP on to Melange""" + tenant_id = project_id or FLAGS.quantum_default_tenant_id + self.m_conn.allocate_ip(quantum_net_id, + vif_ref['uuid'], project_id=tenant_id, + mac_address=vif_ref['address']) + + def get_network_id_by_cidr(self, context, cidr, project_id): + """Find the Quantum UUID associated with a IPv4 CIDR + address for the specified tenant. + """ + tenant_id = project_id or FLAGS.quantum_default_tenant_id + all_blocks = self.m_conn.get_blocks(tenant_id) + for b in all_blocks['ip_blocks']: + if b['cidr'] == cidr: + return b['network_id'] + raise exception.NotFound(_("No network found for cidr %s" % cidr)) + + def delete_subnets_by_net_id(self, context, net_id, project_id): + """Find Melange block associated with the Quantum UUID, + then tell Melange to delete that block. + """ + admin_context = context.elevated() + tenant_id = project_id or FLAGS.quantum_default_tenant_id + all_blocks = self.m_conn.get_blocks(tenant_id) + for b in all_blocks['ip_blocks']: + if b['network_id'] == net_id: + self.m_conn.delete_block(b['id'], tenant_id) + + network = db.network_get_by_uuid(admin_context, net_id) + db.network_delete_safe(context, network['id']) + + def get_project_and_global_net_ids(self, context, project_id): + """Fetches all networks associated with this project, or + that are "global" (i.e., have no project set). + Returns list sorted by 'priority' (lowest integer value + is highest priority). + """ + if project_id is None: + raise Exception(_("get_project_and_global_net_ids must be called" + " with a non-null project_id")) + + admin_context = context.elevated() + + # Decorate with priority + priority_nets = [] + for tenant_id in (project_id, FLAGS.quantum_default_tenant_id): + blocks = self.m_conn.get_blocks(tenant_id) + for ip_block in blocks['ip_blocks']: + network_id = ip_block['network_id'] + network = db.network_get_by_uuid(admin_context, network_id) + if network: + priority = network['priority'] + priority_nets.append((priority, network_id, tenant_id)) + + # Sort by priority + priority_nets.sort() + + # Undecorate + return [(network_id, tenant_id) + for priority, network_id, tenant_id in priority_nets] + + def get_subnets_by_net_id(self, context, project_id, net_id): + """Returns information about the IPv4 and IPv6 subnets + associated with a Quantum Network UUID. + """ + + # FIXME(danwent): Melange actually returns the subnet info + # when we query for a particular interface. We may want to + # rework the ipam_manager python API to let us take advantage of + # this, as right now we have to get all blocks and cycle through + # them. + subnet_v4 = None + subnet_v6 = None + tenant_id = project_id or FLAGS.quantum_default_tenant_id + all_blocks = self.m_conn.get_blocks(tenant_id) + for b in all_blocks['ip_blocks']: + if b['network_id'] == net_id: + subnet = {'network_id': b['network_id'], + 'cidr': b['cidr'], + 'gateway': b['gateway'], + 'broadcast': b['broadcast'], + 'netmask': b['netmask'], + 'dns1': b['dns1'], + 'dns2': b['dns2']} + + if IPNetwork(b['cidr']).version == 6: + subnet_v6 = subnet + else: + subnet_v4 = subnet + return (subnet_v4, subnet_v6) + + def get_v4_ips_by_interface(self, context, net_id, vif_id, project_id): + """Returns a list of IPv4 address strings associated with + the specified virtual interface. + """ + return self._get_ips_by_interface(context, net_id, vif_id, + project_id, 4) + + def get_v6_ips_by_interface(self, context, net_id, vif_id, project_id): + """Returns a list of IPv6 address strings associated with + the specified virtual interface. + """ + return self._get_ips_by_interface(context, net_id, vif_id, + project_id, 6) + + def _get_ips_by_interface(self, context, net_id, vif_id, project_id, + ip_version): + """Helper method to fetch v4 or v6 addresses for a particular + virtual interface. + """ + tenant_id = project_id or FLAGS.quantum_default_tenant_id + ip_list = self.m_conn.get_allocated_ips(net_id, vif_id, tenant_id) + return [ip['address'] for ip in ip_list + if IPNetwork(ip['address']).version == ip_version] + + def verify_subnet_exists(self, context, project_id, quantum_net_id): + """Confirms that a subnet exists that is associated with the + specified Quantum Network UUID. + """ + tenant_id = project_id or FLAGS.quantum_default_tenant_id + v4_subnet, v6_subnet = self.get_subnets_by_net_id(context, tenant_id, + quantum_net_id) + return v4_subnet is not None + + def deallocate_ips_by_vif(self, context, project_id, net_id, vif_ref): + """Deallocate all fixed IPs associated with the specified + virtual interface. + """ + tenant_id = project_id or FLAGS.quantum_default_tenant_id + self.m_conn.deallocate_ips(net_id, vif_ref['uuid'], tenant_id) diff --git a/nova/network/quantum/nova_ipam_lib.py b/nova/network/quantum/nova_ipam_lib.py new file mode 100644 index 000000000..21dee8f6a --- /dev/null +++ b/nova/network/quantum/nova_ipam_lib.py @@ -0,0 +1,195 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira Networks, Inc +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import netaddr + +from nova import db +from nova import exception +from nova import flags +from nova import ipv6 +from nova import log as logging +from nova.network import manager +from nova.network.quantum import melange_connection as melange +from nova import utils + + +LOG = logging.getLogger("nova.network.quantum.nova_ipam_lib") + +FLAGS = flags.FLAGS + + +def get_ipam_lib(net_man): + return QuantumNovaIPAMLib(net_man) + + +class QuantumNovaIPAMLib(object): + """Implements Quantum IP Address Management (IPAM) interface + using the local Nova database. This implementation is inline + with how IPAM is used by other NetworkManagers. + """ + + def __init__(self, net_manager): + """Holds a reference to the "parent" network manager, used + to take advantage of various FlatManager methods to avoid + code duplication. + """ + self.net_manager = net_manager + + def create_subnet(self, context, label, tenant_id, + quantum_net_id, priority, cidr=None, + gateway_v6=None, cidr_v6=None, + dns1=None, dns2=None): + """Re-use the basic FlatManager create_networks method to + initialize the networks and fixed_ips tables in Nova DB. + + Also stores a few more fields in the networks table that + are needed by Quantum but not the FlatManager. + """ + admin_context = context.elevated() + subnet_size = len(netaddr.IPNetwork(cidr)) + networks = manager.FlatManager.create_networks(self.net_manager, + admin_context, label, cidr, + False, 1, subnet_size, cidr_v6, + gateway_v6, quantum_net_id, None, dns1, dns2) + + if len(networks) != 1: + raise Exception(_("Error creating network entry")) + + network = networks[0] + net = {"project_id": tenant_id, + "priority": priority, + "uuid": quantum_net_id} + db.network_update(admin_context, network['id'], net) + + def get_network_id_by_cidr(self, context, cidr, project_id): + """ Grabs Quantum network UUID based on IPv4 CIDR. """ + admin_context = context.elevated() + network = db.network_get_by_cidr(admin_context, cidr) + if not network: + raise Exception(_("No network with fixed_range = %s" % + fixed_range)) + return network['uuid'] + + def delete_subnets_by_net_id(self, context, net_id, project_id): + """Deletes a network based on Quantum UUID. Uses FlatManager + delete_network to avoid duplication. + """ + admin_context = context.elevated() + network = db.network_get_by_uuid(admin_context, net_id) + if not network: + raise Exception(_("No network with net_id = %s" % net_id)) + manager.FlatManager.delete_network(self.net_manager, + admin_context, network['cidr'], + require_disassociated=False) + + def get_project_and_global_net_ids(self, context, project_id): + """Fetches all networks associated with this project, or + that are "global" (i.e., have no project set). + Returns list sorted by 'priority'. + """ + admin_context = context.elevated() + networks = db.project_get_networks(admin_context, project_id, False) + networks.extend(db.project_get_networks(admin_context, None, False)) + id_priority_map = {} + net_list = [] + for n in networks: + net_id = n['uuid'] + net_list.append((net_id, n["project_id"])) + id_priority_map[net_id] = n['priority'] + return sorted(net_list, key=lambda x: id_priority_map[x[0]]) + + def allocate_fixed_ip(self, context, tenant_id, quantum_net_id, vif_rec): + """Allocates a single fixed IPv4 address for a virtual interface.""" + admin_context = context.elevated() + network = db.network_get_by_uuid(admin_context, quantum_net_id) + if network['cidr']: + address = db.fixed_ip_associate_pool(admin_context, + network['id'], + vif_rec['instance_id']) + values = {'allocated': True, + 'virtual_interface_id': vif_rec['id']} + db.fixed_ip_update(admin_context, address, values) + + def get_subnets_by_net_id(self, context, tenant_id, net_id): + """Returns information about the IPv4 and IPv6 subnets + associated with a Quantum Network UUID. + """ + n = db.network_get_by_uuid(context.elevated(), net_id) + subnet_data_v4 = { + 'network_id': n['uuid'], + 'cidr': n['cidr'], + 'gateway': n['gateway'], + 'broadcast': n['broadcast'], + 'netmask': n['netmask'], + 'dns1': n['dns1'], + 'dns2': n['dns2']} + subnet_data_v6 = { + 'network_id': n['uuid'], + 'cidr': n['cidr_v6'], + 'gateway': n['gateway_v6'], + 'broadcast': None, + 'netmask': None, + 'dns1': None, + 'dns2': None} + return (subnet_data_v4, subnet_data_v6) + + def get_v4_ips_by_interface(self, context, net_id, vif_id, project_id): + """Returns a list of IPv4 address strings associated with + the specified virtual interface, based on the fixed_ips table. + """ + vif_rec = db.virtual_interface_get_by_uuid(context, vif_id) + fixed_ips = db.fixed_ip_get_by_virtual_interface(context, + vif_rec['id']) + return [fixed_ip['address'] for fixed_ip in fixed_ips] + + def get_v6_ips_by_interface(self, context, net_id, vif_id, project_id): + """Returns a list containing a single IPv6 address strings + associated with the specified virtual interface. + """ + admin_context = context.elevated() + network = db.network_get_by_uuid(admin_context, net_id) + vif_rec = db.virtual_interface_get_by_uuid(context, vif_id) + if network['cidr_v6']: + ip = ipv6.to_global(network['cidr_v6'], + vif_rec['address'], + project_id) + return [ip] + return [] + + def verify_subnet_exists(self, context, tenant_id, quantum_net_id): + """Confirms that a subnet exists that is associated with the + specified Quantum Network UUID. Raises an exception if no + such subnet exists. + """ + admin_context = context.elevated() + db.network_get_by_uuid(admin_context, quantum_net_id) + + def deallocate_ips_by_vif(self, context, tenant_id, net_id, vif_ref): + """Deallocate all fixed IPs associated with the specified + virtual interface. + """ + try: + admin_context = context.elevated() + fixed_ips = db.fixed_ip_get_by_virtual_interface(admin_context, + vif_ref['id']) + for fixed_ip in fixed_ips: + db.fixed_ip_update(admin_context, fixed_ip['address'], + {'allocated': False, + 'virtual_interface_id': None}) + except exception.FixedIpNotFoundForInstance: + LOG.error(_('No fixed IPs to deallocate for vif %s' % + vif_ref['id'])) diff --git a/nova/network/quantum/quantum_connection.py b/nova/network/quantum/quantum_connection.py new file mode 100644 index 000000000..21917653c --- /dev/null +++ b/nova/network/quantum/quantum_connection.py @@ -0,0 +1,118 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira Networks +# 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 nova import flags +from nova import log as logging +from nova.network.quantum import client as quantum_client +from nova import utils + + +LOG = logging.getLogger("nova.network.quantum.quantum_connection") +FLAGS = flags.FLAGS + +flags.DEFINE_string('quantum_connection_host', + '127.0.0.1', + 'HOST for connecting to quantum') + +flags.DEFINE_string('quantum_connection_port', + '9696', + 'PORT for connecting to quantum') + +flags.DEFINE_string('quantum_default_tenant_id', + "default", + 'Default tenant id when creating quantum networks') + + +class QuantumClientConnection(object): + """Abstracts connection to Quantum service into higher level + operations performed by the QuantumManager. + + Separating this out as a class also let's us create a 'fake' + version of this class for unit tests. + """ + + def __init__(self): + """Initialize Quantum client class based on flags.""" + self.client = quantum_client.Client(FLAGS.quantum_connection_host, + FLAGS.quantum_connection_port, + format="json", + logger=LOG) + + def create_network(self, tenant_id, network_name): + """Create network using specified name, return Quantum + network UUID. + """ + data = {'network': {'name': network_name}} + resdict = self.client.create_network(data, tenant=tenant_id) + return resdict["network"]["id"] + + def delete_network(self, tenant_id, net_id): + """Deletes Quantum network with specified UUID.""" + self.client.delete_network(net_id, tenant=tenant_id) + + def network_exists(self, tenant_id, net_id): + """Determine if a Quantum network exists for the + specified tenant. + """ + try: + self.client.show_network_details(net_id, tenant=tenant_id) + return True + except client.QuantumNotFoundException: + # Not really an error. Real errors will be propogated to caller + return False + + def create_and_attach_port(self, tenant_id, net_id, interface_id): + """Creates a Quantum port on the specified network, sets + status to ACTIVE to enable traffic, and attaches the + vNIC with the specified interface-id. + """ + LOG.debug(_("Connecting interface %(interface_id)s to " + "net %(net_id)s for %(tenant_id)s" % locals())) + port_data = {'port': {'state': 'ACTIVE'}} + resdict = self.client.create_port(net_id, port_data, tenant=tenant_id) + port_id = resdict["port"]["id"] + + attach_data = {'attachment': {'id': interface_id}} + self.client.attach_resource(net_id, port_id, attach_data, + tenant=tenant_id) + + def detach_and_delete_port(self, tenant_id, net_id, port_id): + """Detach and delete the specified Quantum port.""" + LOG.debug(_("Deleting port %(port_id)s on net %(net_id)s" + " for %(tenant_id)s" % locals())) + + self.client.detach_resource(net_id, port_id, tenant=tenant_id) + self.client.delete_port(net_id, port_id, tenant=tenant_id) + + def get_port_by_attachment(self, tenant_id, attachment_id): + """Given a tenant, search for the Quantum network and port + UUID that has the specified interface-id attachment. + """ + # FIXME(danwent): this will be inefficient until the Quantum + # API implements querying a port by the interface-id + net_list_resdict = self.client.list_networks(tenant=tenant_id) + for n in net_list_resdict["networks"]: + net_id = n['id'] + port_list_resdict = self.client.list_ports(net_id, + tenant=tenant_id) + for p in port_list_resdict["ports"]: + port_id = p["id"] + port_get_resdict = self.client.show_port_attachment(net_id, + port_id, tenant=tenant_id) + if attachment_id == port_get_resdict["attachment"]["id"]: + return (net_id, port_id) + return (None, None) diff --git a/nova/scheduler/abstract_scheduler.py b/nova/scheduler/abstract_scheduler.py index 7f17b642f..6e8c7d715 100644 --- a/nova/scheduler/abstract_scheduler.py +++ b/nova/scheduler/abstract_scheduler.py @@ -20,8 +20,8 @@ customize the behavior: filter_hosts() and weigh_hosts(). The default behavior is to simply select all hosts and weight them the same. """ -import operator import json +import operator import M2Crypto @@ -110,7 +110,6 @@ class AbstractScheduler(driver.Scheduler): flavor_id = instance_type['flavorid'] reservation_id = instance_properties['reservation_id'] files = kwargs['injected_files'] - ipgroup = None # Not supported in OS API ... yet child_zone = zone_info['child_zone'] child_blob = zone_info['child_blob'] zone = db.zone_get(context, child_zone) @@ -124,8 +123,17 @@ class AbstractScheduler(driver.Scheduler): except novaclient_exceptions.BadRequest, e: raise exception.NotAuthorized(_("Bad credentials attempting " "to talk to zone at %(url)s.") % locals()) - nova.servers.create(name, image_ref, flavor_id, ipgroup, meta, files, - child_blob, reservation_id=reservation_id) + # NOTE(Vek): Novaclient has two different calling conventions + # for this call, depending on whether you're using + # 1.0 or 1.1 API: in 1.0, there's an ipgroups + # argument after flavor_id which isn't present in + # 1.1. To work around this, all the extra + # arguments are passed as keyword arguments + # (there's a reasonable default for ipgroups in the + # novaclient call). + nova.servers.create(name, image_ref, flavor_id, + meta=meta, files=files, zone_blob=child_blob, + reservation_id=reservation_id) def _provision_resource_from_blob(self, context, build_plan_item, instance_id, request_spec, kwargs): @@ -269,9 +277,6 @@ class AbstractScheduler(driver.Scheduler): # Filter local hosts based on requirements ... filtered_hosts = self.filter_hosts(topic, request_spec, unfiltered_hosts) - if not filtered_hosts: - LOG.warn(_("No hosts available")) - return [] # weigh the selected hosts. # weighted_hosts = [{weight=weight, hostname=hostname, diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py index 55cea5f8f..719437b73 100644 --- a/nova/scheduler/api.py +++ b/nova/scheduler/api.py @@ -103,22 +103,6 @@ def update_service_capabilities(context, service_name, host, capabilities): return rpc.fanout_cast(context, 'scheduler', kwargs) -def _wrap_method(function, self): - """Wrap method to supply self.""" - def _wrap(*args, **kwargs): - return function(self, *args, **kwargs) - return _wrap - - -def _process(func, zone): - """Worker stub for green thread pool. Give the worker - an authenticated nova client and zone info.""" - nova = novaclient.Client(zone.username, zone.password, None, - zone.api_url) - nova.authenticate() - return func(nova, zone) - - def call_zone_method(context, method_name, errors_to_ignore=None, novaclient_collection_name='zones', zones=None, *args, **kwargs): @@ -166,6 +150,32 @@ def child_zone_helper(zone_list, func): For example, if you are calling server.pause(), the list will be whatever the response from server.pause() is. One entry per child zone called.""" + + def _wrap_method(function, arg1): + """Wrap method to supply an argument.""" + def _wrap(*args, **kwargs): + return function(arg1, *args, **kwargs) + return _wrap + + def _process(func, zone): + """Worker stub for green thread pool. Give the worker + an authenticated nova client and zone info.""" + try: + nova = novaclient.Client(zone.username, zone.password, None, + zone.api_url) + nova.authenticate() + except novaclient_exceptions.BadRequest, e: + url = zone.api_url + LOG.warn(_("Failed request to zone; URL=%(url)s: %(e)s") + % locals()) + # This is being returned instead of raised, so that when + # results are processed in unmarshal_result() after the + # greenpool.imap completes, the exception can be raised + # there if no other zones had a response. + return exception.ZoneRequestError() + else: + return func(nova, zone) + green_pool = greenpool.GreenPool() return [result for result in green_pool.imap( _wrap_method(_process, func), zone_list)] @@ -260,6 +270,8 @@ class reroute_compute(object): if not FLAGS.enable_zone_routing: raise exception.InstanceNotFound(instance_id=item_uuid) + self.item_uuid = item_uuid + zones = db.zone_get_all(context) if not zones: raise exception.InstanceNotFound(instance_id=item_uuid) @@ -342,9 +354,13 @@ class reroute_compute(object): dict {'server':{k:v}}. Others may return a list of them, like {'servers':[{k,v}]}""" reduced_response = [] + found_exception = None for zone_response in zone_responses: if not zone_response: continue + if isinstance(zone_response, BaseException): + found_exception = zone_response + continue server = zone_response.__dict__ @@ -355,7 +371,9 @@ class reroute_compute(object): reduced_response.append(dict(server=server)) if reduced_response: return reduced_response[0] # first for now. - return {} + elif found_exception: + raise found_exception + raise exception.InstanceNotFound(instance_id=self.item_uuid) def redirect_handler(f): diff --git a/nova/scheduler/base_scheduler.py b/nova/scheduler/base_scheduler.py index 35e5af035..e8629ca92 100644 --- a/nova/scheduler/base_scheduler.py +++ b/nova/scheduler/base_scheduler.py @@ -27,6 +27,8 @@ from nova.scheduler import abstract_scheduler from nova.scheduler import host_filter FLAGS = flags.FLAGS +flags.DEFINE_boolean('spread_first', False, + 'Use a spread-first zone scheduler strategy') LOG = logging.getLogger('nova.scheduler.base_scheduler') @@ -55,5 +57,22 @@ class BaseScheduler(abstract_scheduler.AbstractScheduler): scheduling objectives """ # NOTE(sirp): The default logic is the same as the NoopCostFunction - return [dict(weight=1, hostname=hostname, capabilities=capabilities) - for hostname, capabilities in hosts] + hosts = [dict(weight=1, hostname=hostname, capabilities=capabilities) + for hostname, capabilities in hosts] + + # NOTE(Vek): What we actually need to return is enough hosts + # for all the instances! + num_instances = request_spec.get('num_instances', 1) + instances = [] + while num_instances > len(hosts): + instances.extend(hosts) + num_instances -= len(hosts) + if num_instances > 0: + instances.extend(hosts[:num_instances]) + + # Adjust the weights for a spread-first strategy + if FLAGS.spread_first: + for i, host in enumerate(hosts): + host['weight'] = i + 1 + + return instances diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 826a99b0a..9f7d34ea7 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -32,6 +32,12 @@ from nova import exception from nova import flags import nova.scheduler +# NOTE(Vek): Even though we don't use filters in here anywhere, we +# depend on default_host_filter being available in FLAGS, +# and that happens only when filters/abstract_filter.py is +# imported. +from nova.scheduler import filters + FLAGS = flags.FLAGS diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 0e395ee79..bf18abc6c 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -93,12 +93,14 @@ class SchedulerManager(manager.Manager): driver_method = 'schedule_%s' % method elevated = context.elevated() try: - host = getattr(self.driver, driver_method)(elevated, *args, - **kwargs) + real_meth = getattr(self.driver, driver_method) + args = (elevated,) + args except AttributeError, e: LOG.warning(_("Driver Method %(driver_method)s missing: %(e)s." - "Reverting to schedule()") % locals()) - host = self.driver.schedule(elevated, topic, *args, **kwargs) + "Reverting to schedule()") % locals()) + real_meth = self.driver.schedule + args = (elevated, topic) + args + host = real_meth(*args, **kwargs) if not host: LOG.debug(_("%(topic)s %(method)s handled in Scheduler") diff --git a/nova/tests/public_key/dummy.fingerprint b/nova/tests/api/ec2/public_key/dummy.fingerprint index 715bca27a..715bca27a 100644 --- a/nova/tests/public_key/dummy.fingerprint +++ b/nova/tests/api/ec2/public_key/dummy.fingerprint diff --git a/nova/tests/public_key/dummy.pub b/nova/tests/api/ec2/public_key/dummy.pub index d4cf2bc0d..d4cf2bc0d 100644 --- a/nova/tests/public_key/dummy.pub +++ b/nova/tests/api/ec2/public_key/dummy.pub diff --git a/nova/tests/test_cloud.py b/nova/tests/api/ec2/test_cloud.py index 96aebdd54..cc85cbd95 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py @@ -541,11 +541,9 @@ class CloudTestCase(test.TestCase): inst2 = db.instance_create(self.context, args2) db.instance_destroy(self.context, inst1.id) result = self.cloud.describe_instances(self.context) + self.assertEqual(len(result['reservationSet']), 1) result1 = result['reservationSet'][0]['instancesSet'] self.assertEqual(result1[0]['instanceId'], - ec2utils.id_to_ec2_id(inst1.id)) - result2 = result['reservationSet'][1]['instancesSet'] - self.assertEqual(result2[0]['instanceId'], ec2utils.id_to_ec2_id(inst2.id)) def _block_device_mapping_create(self, instance_id, mappings): @@ -1597,7 +1595,9 @@ class CloudTestCase(test.TestCase): 'ephemeral0': '/dev/sdb', 'swap': '/dev/sdc', 'ephemeral1': '/dev/sdd', - 'ephemeral2': '/dev/sd3'} + 'ephemeral2': '/dev/sd3', + 'ebs0': '/dev/sdh', + 'ebs1': '/dev/sdi'} self.assertEqual(self.cloud._format_instance_mapping(ctxt, instance_ref0), diff --git a/nova/tests/api/openstack/common.py b/nova/tests/api/openstack/common.py index 74bb8729a..19515ca67 100644 --- a/nova/tests/api/openstack/common.py +++ b/nova/tests/api/openstack/common.py @@ -34,3 +34,25 @@ def webob_factory(url): req.body = json.dumps(body) return req return web_request + + +def compare_links(actual, expected): + """Compare xml atom links.""" + + return compare_tree_to_dict(actual, expected, ('rel', 'href', 'type')) + + +def compare_media_types(actual, expected): + """Compare xml media types.""" + + return compare_tree_to_dict(actual, expected, ('base', 'type')) + + +def compare_tree_to_dict(actual, expected, keys): + """Compare parts of lxml.etree objects to dicts.""" + + for elem, data in zip(actual, expected): + for key in keys: + if elem.get(key) != data.get(key): + return False + return True diff --git a/nova/tests/api/openstack/contrib/test_createserverext.py b/nova/tests/api/openstack/contrib/test_createserverext.py index 089c8e59d..03c7d1ec5 100644 --- a/nova/tests/api/openstack/contrib/test_createserverext.py +++ b/nova/tests/api/openstack/contrib/test_createserverext.py @@ -16,6 +16,7 @@ # under the License. import base64 +import datetime import json import unittest from xml.dom import minidom @@ -27,15 +28,7 @@ from nova import db from nova import exception from nova import flags from nova import test -from nova import utils import nova.api.openstack -from nova.api.openstack import servers -from nova.api.openstack.contrib import createserverext -import nova.compute.api - -import nova.scheduler.api -import nova.image.fake -import nova.rpc from nova.tests.api.openstack import fakes @@ -52,22 +45,49 @@ DUPLICATE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'), INVALID_NETWORKS = [('invalid', 'invalid-ip-address')] +INSTANCE = { + "id": 1, + "display_name": "test_server", + "uuid": FAKE_UUID, + "user_id": 'fake_user_id', + "tenant_id": 'fake_tenant_id', + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "security_groups": [{"id": 1, "name": "test"}], + "image_ref": 'http://foo.com/123', + "instance_type": {"flavorid": '124'}, + } + + +def return_server_by_id(context, id, session=None): + INSTANCE['id'] = id + return INSTANCE + + +def return_security_group_non_existing(context, project_id, group_name): + raise exception.SecurityGroupNotFoundForProject(project_id=project_id, + security_group_id=group_name) + + +def return_security_group_get_by_name(context, project_id, group_name): + return {'id': 1, 'name': group_name} + + +def return_security_group_get(context, security_group_id, session): + return {'id': security_group_id} + + +def return_instance_add_security_group(context, instance_id, + security_group_id): + pass + class CreateserverextTest(test.TestCase): def setUp(self): super(CreateserverextTest, self).setUp() - self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.auth_data = {} - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_auth(self.stubs) - fakes.stub_out_image_service(self.stubs) - fakes.stub_out_key_pair_funcs(self.stubs) - self.allow_admin = FLAGS.allow_admin_api def tearDown(self): - self.stubs.UnsetAll() - FLAGS.allow_admin_api = self.allow_admin super(CreateserverextTest, self).tearDown() def _setup_mock_compute_api(self): @@ -96,6 +116,8 @@ class CreateserverextTest(test.TestCase): return [{'id': '1234', 'display_name': 'fakeinstance', 'uuid': FAKE_UUID, + 'user_id': 'fake', + 'project_id': 'fake', 'created_at': "", 'updated_at': ""}] @@ -114,6 +136,18 @@ class CreateserverextTest(test.TestCase): '_get_kernel_ramdisk_from_image', make_stub_method((1, 1))) return compute_api + def _create_security_group_request_dict(self, security_groups): + server = {} + server['name'] = 'new-server-test' + server['imageRef'] = 1 + server['flavorRef'] = 1 + if security_groups is not None: + sg_list = [] + for name in security_groups: + sg_list.append({'name': name}) + server['security_groups'] = sg_list + return {'server': server} + def _create_networks_request_dict(self, networks): server = {} server['name'] = 'new-server-test' @@ -348,3 +382,38 @@ class CreateserverextTest(test.TestCase): self._create_instance_with_user_data_json(user_data_contents) self.assertEquals(response.status_int, 400) self.assertEquals(user_data, None) + + def test_create_instance_with_security_group_json(self): + security_groups = ['test', 'test1'] + self.stubs.Set(nova.db.api, 'security_group_get_by_name', + return_security_group_get_by_name) + self.stubs.Set(nova.db.api, 'instance_add_security_group', + return_instance_add_security_group) + body_dict = self._create_security_group_request_dict(security_groups) + request = self._get_create_request_json(body_dict) + response = request.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + + def test_get_server_by_id_verify_security_groups_json(self): + self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id) + req = webob.Request.blank('/v1.1/123/os-create-server-ext/1') + req.headers['Content-Type'] = 'application/json' + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 200) + res_dict = json.loads(response.body) + expected_security_group = [{"name": "test"}] + self.assertEquals(res_dict['server']['security_groups'], + expected_security_group) + + def test_get_server_by_id_verify_security_groups_xml(self): + self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id) + req = webob.Request.blank('/v1.1/123/os-create-server-ext/1') + req.headers['Accept'] = 'application/xml' + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 200) + dom = minidom.parseString(response.body) + server = dom.childNodes[0] + sec_groups = server.getElementsByTagName('security_groups')[0] + sec_group = sec_groups.getElementsByTagName('security_group')[0] + self.assertEqual(INSTANCE['security_groups'][0]['name'], + sec_group.getAttribute("name")) diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index 642f2b841..0744f0a11 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -212,11 +212,45 @@ class FloatingIpTest(test.TestCase): "fixed_ip": None} self.assertEqual(ip, expected) - def test_floating_ip_release(self): + def test_floating_ip_release_associated(self): + self.disassociated = False + + def get_floating_ip(ignore, context, id): + return {'id': 1, 'address': '10.10.10.10', + 'fixed_ip': {'id': 1}} + + def disassociate(ignore, context, floating_address): + self.disassociated = True + + self.stubs.Set(network.api.API, "get_floating_ip", + get_floating_ip) + self.stubs.Set(network.api.API, "disassociate_floating_ip", + disassociate) + req = webob.Request.blank('/v1.1/123/os-floating-ips/1') + req.method = 'DELETE' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + self.assertTrue(self.disassociated) + + def test_floating_ip_release_disassociated(self): + self.disassociated = False + + def fake_get_floating_ip(ignore, context, id): + return {'id': 1, 'address': '10.10.10.10', + 'fixed_ip': None} + + def fake_disassociate(ignore, context, floating_address): + self.disassociated = True + + self.stubs.Set(network.api.API, "get_floating_ip", + fake_get_floating_ip) + self.stubs.Set(network.api.API, "disassociate_floating_ip", + fake_disassociate) req = webob.Request.blank('/v1.1/123/os-floating-ips/1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 202) + self.assertFalse(self.disassociated) def test_add_floating_ip_to_instance(self): self.stubs.Set(network.api.API, "associate_floating_ip", @@ -289,8 +323,45 @@ class FloatingIpTest(test.TestCase): self.assertEqual(resp.status_int, 202) self.assertTrue(self.disassociated) - def test_remove_floating_ip_from_instance(self): - body = dict(removeFloatingIp=dict(address='11.0.0.1')) + def test_remove_associated_floating_ip_from_instance(self): + self.disassociated = False + + def fake_get_floating_ip_by_ip(ignore, context, ip): + return {'id': 1, 'address': '10.10.10.10', + 'fixed_ip': {'id': 1}} + + def fake_disassociate(ignore, context, floating_address): + self.disassociated = True + + self.stubs.Set(network.api.API, "get_floating_ip_by_ip", + fake_get_floating_ip_by_ip) + self.stubs.Set(network.api.API, "disassociate_floating_ip", + fake_disassociate) + body = dict(removeFloatingIp=dict(address='10.10.10.10')) + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) + self.assertTrue(self.disassociated) + + def test_remove_disassociated_floating_ip_from_instance(self): + self.disassociated = False + + def fake_get_floating_ip_by_ip(ignore, context, ip): + return {'id': 1, 'address': '10.10.10.10', + 'fixed_ip': None} + + def fake_disassociate(ignore, context, floating_address): + self.disassociated = True + + self.stubs.Set(network.api.API, "get_floating_ip_by_ip", + fake_get_floating_ip_by_ip) + self.stubs.Set(network.api.API, "disassociate_floating_ip", + fake_disassociate) + body = dict(removeFloatingIp=dict(address='10.10.10.10')) req = webob.Request.blank('/v1.1/123/servers/test_inst/action') req.method = "POST" req.body = json.dumps(body) @@ -298,6 +369,7 @@ class FloatingIpTest(test.TestCase): resp = req.get_response(fakes.wsgi_app()) self.assertEqual(resp.status_int, 202) + self.assertFalse(self.disassociated) def test_bad_address_param_in_remove_floating_ip(self): body = dict(removeFloatingIp=dict(badparam='11.0.0.1')) diff --git a/nova/tests/api/openstack/contrib/test_volumes.py b/nova/tests/api/openstack/contrib/test_volumes.py new file mode 100644 index 000000000..443ec399f --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_volumes.py @@ -0,0 +1,73 @@ +# Copyright 2011 Josh Durgin +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import json +import webob + +import nova +from nova import context +from nova import test +from nova.api.openstack.contrib.volumes import BootFromVolumeController +from nova.compute import instance_types +from nova.tests.api.openstack import fakes +from nova.tests.api.openstack.test_servers import fake_gen_uuid + + +def fake_compute_api_create(cls, context, instance_type, image_href, **kwargs): + inst_type = instance_types.get_instance_type_by_flavor_id(2) + return [{'id': 1, + 'display_name': 'test_server', + 'uuid': fake_gen_uuid(), + 'instance_type': dict(inst_type), + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', + 'image_ref': 3, + 'user_id': 'fake', + 'project_id': 'fake', + 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0), + 'updated_at': datetime.datetime(2010, 11, 11, 11, 0, 0), + }] + + +class BootFromVolumeTest(test.TestCase): + + def setUp(self): + super(BootFromVolumeTest, self).setUp() + self.stubs.Set(nova.compute.API, 'create', fake_compute_api_create) + + def test_create_root_volume(self): + body = dict(server=dict( + name='test_server', imageRef=3, + flavorRef=2, min_count=1, max_count=1, + block_device_mapping=[dict( + volume_id=1, + device_name='/dev/vda', + virtual='root', + delete_on_termination=False, + )] + )) + req = webob.Request.blank('/v1.1/fake/os-volumes_boot') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + server = json.loads(res.body)['server'] + self.assertEqual(1, server['id']) + self.assertEqual(2, int(server['flavor']['id'])) + self.assertEqual(u'test_server', server['name']) + self.assertEqual(3, int(server['image']['id'])) + self.assertEqual(16, len(server['adminPass'])) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 44681d395..3a567f0cc 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -40,8 +40,8 @@ from nova.api.openstack import limits from nova.auth.manager import User, Project import nova.image.fake from nova.image import glance -from nova.image import service from nova.tests import fake_flags +from nova.tests.glance import stubs as glance_stubs class Context(object): @@ -83,7 +83,7 @@ def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True, if fake_auth_context is not None: ctxt = fake_auth_context else: - ctxt = context.RequestContext('fake', 'fake') + ctxt = context.RequestContext('fake', 'fake', auth_token=True) api10 = openstack.FaultWrapper(api_auth.InjectContext(ctxt, limits.RateLimitingMiddleware(inner_app10))) api11 = openstack.FaultWrapper(api_auth.InjectContext(ctxt, @@ -124,7 +124,7 @@ def stub_out_key_pair_funcs(stubs, have_key_pair=True): def stub_out_image_service(stubs): - def fake_get_image_service(image_href): + def fake_get_image_service(context, image_href): return (nova.image.fake.FakeImageService(), image_href) stubs.Set(nova.image, 'get_image_service', fake_get_image_service) stubs.Set(nova.image, 'get_default_image_service', @@ -177,6 +177,39 @@ def stub_out_compute_api_backup(stubs): stubs.Set(nova.compute.API, 'backup', backup) +def _make_image_fixtures(): + NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" + + image_id = 123 + base_attrs = {'deleted': False} + + fixtures = [] + + def add_fixture(**kwargs): + kwargs.update(base_attrs) + fixtures.append(kwargs) + + # Public image + add_fixture(id=image_id, name='public image', is_public=True, + status='active', properties={'key1': 'value1'}) + image_id += 1 + + # Snapshot for User 1 + server_ref = 'http://localhost/v1.1/servers/42' + snapshot_properties = {'instance_ref': server_ref, 'user_id': 'fake'} + for status in ('queued', 'saving', 'active', 'killed', + 'deleted', 'pending_delete'): + add_fixture(id=image_id, name='%s snapshot' % status, + is_public=False, status=status, + properties=snapshot_properties) + image_id += 1 + + # Image without a name + add_fixture(id=image_id, is_public=True, status='active', properties={}) + + return fixtures + + def stub_out_glance_add_image(stubs, sent_to_glance): """ We return the metadata sent to glance by modifying the sent_to_glance dict @@ -192,91 +225,11 @@ def stub_out_glance_add_image(stubs, sent_to_glance): stubs.Set(glance_client.Client, 'add_image', fake_add_image) -def stub_out_glance(stubs, initial_fixtures=None): - - class FakeGlanceClient: - - def __init__(self, initial_fixtures): - self.fixtures = initial_fixtures or [] - - def _filter_images(self, filters=None, marker=None, limit=None): - found = True - if marker: - found = False - if limit == 0: - limit = None - - fixtures = [] - count = 0 - for f in self.fixtures: - if limit and count >= limit: - break - if found: - fixtures.append(f) - count = count + 1 - if f['id'] == marker: - found = True - - return fixtures - - def fake_get_images(self, filters=None, marker=None, limit=None): - fixtures = self._filter_images(filters, marker, limit) - return [dict(id=f['id'], name=f['name']) - for f in fixtures] - - def fake_get_images_detailed(self, filters=None, - marker=None, limit=None): - return self._filter_images(filters, marker, limit) - - def fake_get_image_meta(self, image_id): - image = self._find_image(image_id) - if image: - return copy.deepcopy(image) - raise glance_exc.NotFound - - def fake_add_image(self, image_meta, data=None): - image_meta = copy.deepcopy(image_meta) - image_id = ''.join(random.choice(string.letters) - for _ in range(20)) - image_meta['id'] = image_id - self.fixtures.append(image_meta) - return copy.deepcopy(image_meta) - - def fake_update_image(self, image_id, image_meta, data=None): - for attr in ('created_at', 'updated_at', 'deleted_at', 'deleted'): - if attr in image_meta: - del image_meta[attr] - - f = self._find_image(image_id) - if not f: - raise glance_exc.NotFound - - f.update(image_meta) - return copy.deepcopy(f) - - def fake_delete_image(self, image_id): - f = self._find_image(image_id) - if not f: - raise glance_exc.NotFound - - self.fixtures.remove(f) - - def _find_image(self, image_id): - for f in self.fixtures: - if str(f['id']) == str(image_id): - return f - return None - - GlanceClient = glance_client.Client - fake = FakeGlanceClient(initial_fixtures) - - stubs.Set(GlanceClient, 'get_images', fake.fake_get_images) - stubs.Set(GlanceClient, 'get_images_detailed', - fake.fake_get_images_detailed) - stubs.Set(GlanceClient, 'get_image_meta', fake.fake_get_image_meta) - stubs.Set(GlanceClient, 'add_image', fake.fake_add_image) - stubs.Set(GlanceClient, 'update_image', fake.fake_update_image) - stubs.Set(GlanceClient, 'delete_image', fake.fake_delete_image) +def stub_out_glance(stubs): + def fake_get_image_service(): + client = glance_stubs.StubGlanceClient(_make_image_fixtures()) + return nova.image.glance.GlanceImageService(client) + stubs.Set(nova.image, 'get_default_image_service', fake_get_image_service) class FakeToken(object): diff --git a/nova/tests/api/openstack/test_api.py b/nova/tests/api/openstack/test_api.py index 7321c329f..b7a0b01ef 100644 --- a/nova/tests/api/openstack/test_api.py +++ b/nova/tests/api/openstack/test_api.py @@ -20,6 +20,7 @@ import json import webob.exc import webob.dec +from lxml import etree from webob import Request from nova import test @@ -52,6 +53,30 @@ class APITest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_vendor_content_type_json(self): + ctype = 'application/vnd.openstack.compute+json' + + req = webob.Request.blank('/') + req.headers['Accept'] = ctype + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, ctype) + + body = json.loads(res.body) + + def test_vendor_content_type_xml(self): + ctype = 'application/vnd.openstack.compute+xml' + + req = webob.Request.blank('/') + req.headers['Accept'] = ctype + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, ctype) + + body = etree.XML(res.body) + def test_exceptions_are_converted_to_faults(self): @webob.dec.wsgify diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index b422bc4d1..1628ad1c8 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -19,6 +19,7 @@ Test suites for 'common' code used throughout the OpenStack HTTP API. """ +from lxml import etree import webob.exc import xml.dom.minidom as minidom @@ -26,6 +27,11 @@ from webob import Request from nova import test from nova.api.openstack import common +from nova.api.openstack import xmlutil + + +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" class LimiterTest(test.TestCase): @@ -237,21 +243,41 @@ class MiscFunctionsTest(test.TestCase): common.remove_version_from_href, fixture) - def test_get_id_from_href(self): + def test_get_id_from_href_with_int_url(self): fixture = 'http://www.testsite.com/dir/45' actual = common.get_id_from_href(fixture) - expected = 45 + expected = '45' self.assertEqual(actual, expected) - def test_get_id_from_href_bad_request(self): - fixture = 'http://45' - self.assertRaises(ValueError, - common.get_id_from_href, - fixture) + def test_get_id_from_href_with_int(self): + fixture = '45' + actual = common.get_id_from_href(fixture) + expected = '45' + self.assertEqual(actual, expected) + + def test_get_id_from_href_with_int_url_query(self): + fixture = 'http://www.testsite.com/dir/45?asdf=jkl' + actual = common.get_id_from_href(fixture) + expected = '45' + self.assertEqual(actual, expected) - def test_get_id_from_href_int(self): - fixture = 1 - self.assertEqual(fixture, common.get_id_from_href(fixture)) + def test_get_id_from_href_with_uuid_url(self): + fixture = 'http://www.testsite.com/dir/abc123' + actual = common.get_id_from_href(fixture) + expected = "abc123" + self.assertEqual(actual, expected) + + def test_get_id_from_href_with_uuid_url_query(self): + fixture = 'http://www.testsite.com/dir/abc123?asdf=jkl' + actual = common.get_id_from_href(fixture) + expected = "abc123" + self.assertEqual(actual, expected) + + def test_get_id_from_href_with_uuid(self): + fixture = 'abc123' + actual = common.get_id_from_href(fixture) + expected = 'abc123' + self.assertEqual(actual, expected) def test_get_version_from_href(self): fixture = 'http://www.testsite.com/v1.1/images' @@ -314,7 +340,7 @@ class MetadataXMLDeserializationTest(test.TestCase): class MetadataXMLSerializationTest(test.TestCase): - def test_index(self): + def test_xml_declaration(self): serializer = common.MetadataXMLSerializer() fixture = { 'metadata': { @@ -322,17 +348,31 @@ class MetadataXMLSerializationTest(test.TestCase): 'three': 'four', }, } - output = serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) - expected = minidom.parseString(""" - <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> - <meta key="three">four</meta> - <meta key="one">two</meta> - </metadata> - """.replace(" ", "").replace("\n", "")) + output = serializer.serialize(fixture, 'index') + print output + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) - self.assertEqual(expected.toxml(), actual.toxml()) + def test_index(self): + serializer = common.MetadataXMLSerializer() + fixture = { + 'metadata': { + 'one': 'two', + 'three': 'four', + }, + } + output = serializer.serialize(fixture, 'index') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) def test_index_null(self): serializer = common.MetadataXMLSerializer() @@ -342,15 +382,16 @@ class MetadataXMLSerializationTest(test.TestCase): }, } output = serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> - <meta key="None">None</meta> - </metadata> - """.replace(" ", "").replace("\n", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) def test_index_unicode(self): serializer = common.MetadataXMLSerializer() @@ -360,15 +401,16 @@ class MetadataXMLSerializationTest(test.TestCase): }, } output = serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(u""" - <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> - <meta key="three">Jos\xe9</meta> - </metadata> - """.encode("UTF-8").replace(" ", "").replace("\n", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(metadata_elem.text.strip(), meta_value) def test_show(self): serializer = common.MetadataXMLSerializer() @@ -378,14 +420,12 @@ class MetadataXMLSerializationTest(test.TestCase): }, } output = serializer.serialize(fixture, 'show') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <meta xmlns="http://docs.openstack.org/compute/api/v1.1" - key="one">two</meta> - """.replace(" ", "").replace("\n", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + meta_dict = fixture['meta'] + (meta_key, meta_value) = meta_dict.items()[0] + self.assertEqual(str(root.get('key')), str(meta_key)) + self.assertEqual(root.text.strip(), meta_value) def test_update_all(self): serializer = common.MetadataXMLSerializer() @@ -396,16 +436,16 @@ class MetadataXMLSerializationTest(test.TestCase): }, } output = serializer.serialize(fixture, 'update_all') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> - <meta key="key6">value6</meta> - <meta key="key4">value4</meta> - </metadata> - """.replace(" ", "").replace("\n", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) def test_update_item(self): serializer = common.MetadataXMLSerializer() @@ -415,14 +455,12 @@ class MetadataXMLSerializationTest(test.TestCase): }, } output = serializer.serialize(fixture, 'update') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <meta xmlns="http://docs.openstack.org/compute/api/v1.1" - key="one">two</meta> - """.replace(" ", "").replace("\n", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + meta_dict = fixture['meta'] + (meta_key, meta_value) = meta_dict.items()[0] + self.assertEqual(str(root.get('key')), str(meta_key)) + self.assertEqual(root.text.strip(), meta_value) def test_create(self): serializer = common.MetadataXMLSerializer() @@ -434,6 +472,16 @@ class MetadataXMLSerializationTest(test.TestCase): }, } output = serializer.serialize(fixture, 'create') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 3) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) actual = minidom.parseString(output.replace(" ", "")) expected = minidom.parseString(""" diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 31443242b..44f4eb055 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -87,6 +87,7 @@ class ExtensionControllerTest(test.TestCase): self.ext_list = [ "Createserverext", "FlavorExtraSpecs", + "FlavorExtraData", "Floating_ips", "Fox In Socks", "Hosts", diff --git a/nova/tests/api/openstack/test_flavors.py b/nova/tests/api/openstack/test_flavors.py index 812bece42..348042bfe 100644 --- a/nova/tests/api/openstack/test_flavors.py +++ b/nova/tests/api/openstack/test_flavors.py @@ -17,16 +17,21 @@ import json import webob -import xml.dom.minidom as minidom +from lxml import etree from nova.api.openstack import flavors import nova.db.api from nova import exception from nova import test +from nova.api.openstack import xmlutil from nova.tests.api.openstack import fakes from nova import wsgi +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" + + def stub_flavor(flavorid, name, memory_mb="256", local_gb="10"): return { "flavorid": str(flavorid), @@ -107,12 +112,20 @@ class FlavorsTest(test.TestCase): "name": "flavor 1", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", }, { "id": "2", "name": "flavor 2", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", }, ] self.assertEqual(flavors, expected) @@ -127,6 +140,10 @@ class FlavorsTest(test.TestCase): "name": "flavor 12", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", } self.assertEqual(flavor, expected) @@ -149,6 +166,10 @@ class FlavorsTest(test.TestCase): "name": "flavor 12", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -216,6 +237,10 @@ class FlavorsTest(test.TestCase): "name": "flavor 1", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -232,6 +257,10 @@ class FlavorsTest(test.TestCase): "name": "flavor 2", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -262,15 +291,50 @@ class FlavorsTest(test.TestCase): class FlavorsXMLSerializationTest(test.TestCase): + def test_xml_declaration(self): + serializer = flavors.FlavorXMLSerializer() + + fixture = { + "flavor": { + "id": "12", + "name": "asdf", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + print output + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + def test_show(self): serializer = flavors.FlavorXMLSerializer() - input = { + fixture = { "flavor": { "id": "12", "name": "asdf", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -284,34 +348,34 @@ class FlavorsXMLSerializationTest(test.TestCase): }, } - output = serializer.serialize(input, 'show') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <flavor xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" - id="12" - name="asdf" - ram="256" - disk="10"> - <atom:link href="http://localhost/v1.1/fake/flavors/12" - rel="self"/> - <atom:link href="http://localhost/fake/flavors/12" - rel="bookmark"/> - </flavor> - """.replace(" ", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + output = serializer.serialize(fixture, 'show') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavor') + flavor_dict = fixture['flavor'] + + for key in ['name', 'id', 'ram', 'disk']: + self.assertEqual(root.get(key), str(flavor_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(flavor_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_show_handles_integers(self): serializer = flavors.FlavorXMLSerializer() - input = { + fixture = { "flavor": { "id": 12, "name": "asdf", "ram": 256, "disk": 10, + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -325,35 +389,35 @@ class FlavorsXMLSerializationTest(test.TestCase): }, } - output = serializer.serialize(input, 'show') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <flavor xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" - id="12" - name="asdf" - ram="256" - disk="10"> - <atom:link href="http://localhost/v1.1/fake/flavors/12" - rel="self"/> - <atom:link href="http://localhost/fake/flavors/12" - rel="bookmark"/> - </flavor> - """.replace(" ", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + output = serializer.serialize(fixture, 'show') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavor') + flavor_dict = fixture['flavor'] + + for key in ['name', 'id', 'ram', 'disk']: + self.assertEqual(root.get(key), str(flavor_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(flavor_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_detail(self): serializer = flavors.FlavorXMLSerializer() - input = { + fixture = { "flavors": [ { "id": "23", "name": "flavor 23", "ram": "512", "disk": "20", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -369,6 +433,10 @@ class FlavorsXMLSerializationTest(test.TestCase): "name": "flavor 13", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -383,45 +451,38 @@ class FlavorsXMLSerializationTest(test.TestCase): ], } - output = serializer.serialize(input, 'detail') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <flavors xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom"> - <flavor id="23" - name="flavor 23" - ram="512" - disk="20"> - <atom:link href="http://localhost/v1.1/fake/flavors/23" - rel="self"/> - <atom:link href="http://localhost/fake/flavors/23" - rel="bookmark"/> - </flavor> - <flavor id="13" - name="flavor 13" - ram="256" - disk="10"> - <atom:link href="http://localhost/v1.1/fake/flavors/13" - rel="self"/> - <atom:link href="http://localhost/fake/flavors/13" - rel="bookmark"/> - </flavor> - </flavors> - """.replace(" ", "") % locals()) - - self.assertEqual(expected.toxml(), actual.toxml()) + output = serializer.serialize(fixture, 'detail') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavors') + flavor_elems = root.findall('{0}flavor'.format(NS)) + self.assertEqual(len(flavor_elems), 2) + for i, flavor_elem in enumerate(flavor_elems): + flavor_dict = fixture['flavors'][i] + + for key in ['name', 'id', 'ram', 'disk']: + self.assertEqual(flavor_elem.get(key), str(flavor_dict[key])) + + link_nodes = flavor_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(flavor_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_index(self): serializer = flavors.FlavorXMLSerializer() - input = { + fixture = { "flavors": [ { "id": "23", "name": "flavor 23", "ram": "512", "disk": "20", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -437,6 +498,10 @@ class FlavorsXMLSerializationTest(test.TestCase): "name": "flavor 13", "ram": "256", "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", "links": [ { "rel": "self", @@ -451,42 +516,34 @@ class FlavorsXMLSerializationTest(test.TestCase): ], } - output = serializer.serialize(input, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <flavors xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom"> - <flavor id="23" name="flavor 23"> - <atom:link href="http://localhost/v1.1/fake/flavors/23" - rel="self"/> - <atom:link href="http://localhost/fake/flavors/23" - rel="bookmark"/> - </flavor> - <flavor id="13" name="flavor 13"> - <atom:link href="http://localhost/v1.1/fake/flavors/13" - rel="self"/> - <atom:link href="http://localhost/fake/flavors/13" - rel="bookmark"/> - </flavor> - </flavors> - """.replace(" ", "") % locals()) - - self.assertEqual(expected.toxml(), actual.toxml()) + output = serializer.serialize(fixture, 'index') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavors_index') + flavor_elems = root.findall('{0}flavor'.format(NS)) + self.assertEqual(len(flavor_elems), 2) + for i, flavor_elem in enumerate(flavor_elems): + flavor_dict = fixture['flavors'][i] + + for key in ['name', 'id']: + self.assertEqual(flavor_elem.get(key), str(flavor_dict[key])) + + link_nodes = flavor_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(flavor_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_index_empty(self): serializer = flavors.FlavorXMLSerializer() - input = { + fixture = { "flavors": [], } - output = serializer.serialize(input, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <flavors xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" /> - """.replace(" ", "") % locals()) - - self.assertEqual(expected.toxml(), actual.toxml()) + output = serializer.serialize(fixture, 'index') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavors_index') + flavor_elems = root.findall('{0}flavor'.format(NS)) + self.assertEqual(len(flavor_elems), 0) diff --git a/nova/tests/api/openstack/test_image_metadata.py b/nova/tests/api/openstack/test_image_metadata.py index fe42e35e5..314c3c38e 100644 --- a/nova/tests/api/openstack/test_image_metadata.py +++ b/nova/tests/api/openstack/test_image_metadata.py @@ -23,7 +23,6 @@ from nova import flags from nova.api import openstack from nova import test from nova.tests.api.openstack import fakes -import nova.wsgi FLAGS = flags.FLAGS @@ -31,76 +30,20 @@ FLAGS = flags.FLAGS class ImageMetaDataTest(test.TestCase): - IMAGE_FIXTURES = [ - {'status': 'active', - 'name': 'image1', - 'deleted': False, - 'container_format': None, - 'checksum': None, - 'created_at': '2011-03-22T17:40:15', - 'disk_format': None, - 'updated_at': '2011-03-22T17:40:15', - 'id': '1', - 'location': 'file:///var/lib/glance/images/1', - 'is_public': True, - 'deleted_at': None, - 'properties': { - 'key1': 'value1', - 'key2': 'value2'}, - 'size': 5882349}, - {'status': 'active', - 'name': 'image2', - 'deleted': False, - 'container_format': None, - 'checksum': None, - 'created_at': '2011-03-22T17:40:15', - 'disk_format': None, - 'updated_at': '2011-03-22T17:40:15', - 'id': '2', - 'location': 'file:///var/lib/glance/images/2', - 'is_public': True, - 'deleted_at': None, - 'properties': { - 'key1': 'value1', - 'key2': 'value2'}, - 'size': 5882349}, - {'status': 'active', - 'name': 'image3', - 'deleted': False, - 'container_format': None, - 'checksum': None, - 'created_at': '2011-03-22T17:40:15', - 'disk_format': None, - 'updated_at': '2011-03-22T17:40:15', - 'id': '3', - 'location': 'file:///var/lib/glance/images/2', - 'is_public': True, - 'deleted_at': None, - 'properties': {}, - 'size': 5882349}, - ] - def setUp(self): super(ImageMetaDataTest, self).setUp() - self.flags(image_service='nova.image.glance.GlanceImageService') - # NOTE(dprince) max out properties/metadata in image 3 for testing - img3 = self.IMAGE_FIXTURES[2] - for num in range(FLAGS.quota_metadata_items): - img3['properties']['key%i' % num] = "blah" - fakes.stub_out_glance(self.stubs, self.IMAGE_FIXTURES) + fakes.stub_out_glance(self.stubs) def test_index(self): - req = webob.Request.blank('/v1.1/123/images/1/metadata') + req = webob.Request.blank('/v1.1/123/images/123/metadata') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) - expected = self.IMAGE_FIXTURES[0]['properties'] - self.assertEqual(len(expected), len(res_dict['metadata'])) - for (key, value) in res_dict['metadata'].items(): - self.assertEqual(value, res_dict['metadata'][key]) + expected = {'metadata': {'key1': 'value1'}} + self.assertEqual(res_dict, expected) def test_show(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -109,32 +52,38 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual('value1', res_dict['meta']['key1']) def test_show_not_found(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key9') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key9') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(404, res.status_int) + + def test_show_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata/key1') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_create(self): - req = webob.Request.blank('/v1.1/fake/images/2/metadata') + req = webob.Request.blank('/v1.1/fake/images/123/metadata') req.method = 'POST' - req.body = '{"metadata": {"key9": "value9"}}' + req.body = '{"metadata": {"key7": "value7"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) actual_output = json.loads(res.body) + expected_output = {'metadata': {'key1': 'value1', 'key7': 'value7'}} + self.assertEqual(expected_output, actual_output) - expected_output = { - 'metadata': { - 'key1': 'value1', - 'key2': 'value2', - 'key9': 'value9', - }, - } + def test_create_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata') + req.method = 'POST' + req.body = '{"metadata": {"key7": "value7"}}' + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) - self.assertEqual(expected_output, actual_output) + self.assertEqual(404, res.status_int) def test_update_all(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata') + req = webob.Request.blank('/v1.1/fake/images/123/metadata') req.method = 'PUT' req.body = '{"metadata": {"key9": "value9"}}' req.headers["content-type"] = "application/json" @@ -142,17 +91,20 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(200, res.status_int) actual_output = json.loads(res.body) + expected_output = {'metadata': {'key9': 'value9'}} + self.assertEqual(expected_output, actual_output) - expected_output = { - 'metadata': { - 'key9': 'value9', - }, - } + def test_update_all_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata') + req.method = 'PUT' + req.body = '{"metadata": {"key9": "value9"}}' + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) - self.assertEqual(expected_output, actual_output) + self.assertEqual(404, res.status_int) def test_update_item(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"key1": "zz"}}' req.headers["content-type"] = "application/json" @@ -160,15 +112,20 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(200, res.status_int) actual_output = json.loads(res.body) - expected_output = { - 'meta': { - 'key1': 'zz', - }, - } + expected_output = {'meta': {'key1': 'zz'}} self.assertEqual(actual_output, expected_output) + def test_update_item_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata/key1') + req.method = 'PUT' + req.body = '{"meta": {"key1": "zz"}}' + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(404, res.status_int) + def test_update_item_bad_body(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'PUT' req.body = '{"key1": "zz"}' req.headers["content-type"] = "application/json" @@ -176,15 +133,18 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_too_many_keys(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'PUT' - req.body = '{"meta": {"key1": "value1", "key2": "value2"}}' + overload = {} + for num in range(FLAGS.quota_metadata_items + 1): + overload['key%s' % num] = 'value%s' % num + req.body = json.dumps({'meta': overload}) req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) def test_update_item_body_uri_mismatch(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/bad') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/bad') req.method = 'PUT' req.body = '{"meta": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -192,7 +152,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_xml(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'PUT' req.body = '<meta key="key1">five</meta>' req.headers["content-type"] = "application/xml" @@ -200,22 +160,24 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(200, res.status_int) actual_output = json.loads(res.body) - expected_output = { - 'meta': { - 'key1': 'five', - }, - } + expected_output = {'meta': {'key1': 'five'}} self.assertEqual(actual_output, expected_output) def test_delete(self): - req = webob.Request.blank('/v1.1/fake/images/2/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(204, res.status_int) self.assertEqual('', res.body) def test_delete_not_found(self): - req = webob.Request.blank('/v1.1/fake/images/2/metadata/blah') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/blah') + req.method = 'DELETE' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(404, res.status_int) + + def test_delete_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata/key1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -225,7 +187,7 @@ class ImageMetaDataTest(test.TestCase): for num in range(FLAGS.quota_metadata_items + 1): data['metadata']['key%i' % num] = "blah" json_string = str(data).replace("\'", "\"") - req = webob.Request.blank('/v1.1/fake/images/2/metadata') + req = webob.Request.blank('/v1.1/fake/images/123/metadata') req.method = 'POST' req.body = json_string req.headers["content-type"] = "application/json" @@ -233,7 +195,8 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(413, res.status_int) def test_too_many_metadata_items_on_put(self): - req = webob.Request.blank('/v1.1/fake/images/3/metadata/blah') + FLAGS.quota_metadata_items = 1 + req = webob.Request.blank('/v1.1/fake/images/123/metadata/blah') req.method = 'PUT' req.body = '{"meta": {"blah": "blah"}}' req.headers["content-type"] = "application/json" diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 2a7cfc382..e5fd4764a 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -22,340 +22,57 @@ and as a WSGI layer import copy import json -import os -import shutil -import tempfile import xml.dom.minidom as minidom +from lxml import etree import mox import stubout import webob -from glance import client as glance_client from nova import context -from nova import exception -from nova import test -from nova import utils import nova.api.openstack from nova.api.openstack import images +from nova.api.openstack import xmlutil +from nova import test from nova.tests.api.openstack import fakes -class _BaseImageServiceTests(test.TestCase): - """Tasks to test for all image services""" - - def __init__(self, *args, **kwargs): - super(_BaseImageServiceTests, self).__init__(*args, **kwargs) - self.service = None - self.context = None - - def test_create(self): - fixture = self._make_fixture('test image') - num_images = len(self.service.index(self.context)) - - image_id = self.service.create(self.context, fixture)['id'] - - self.assertNotEquals(None, image_id) - self.assertEquals(num_images + 1, - len(self.service.index(self.context))) - - def test_create_and_show_non_existing_image(self): - fixture = self._make_fixture('test image') - num_images = len(self.service.index(self.context)) - - image_id = self.service.create(self.context, fixture)['id'] - - self.assertNotEquals(None, image_id) - self.assertRaises(exception.NotFound, - self.service.show, - self.context, - 'bad image id') - - def test_create_and_show_non_existing_image_by_name(self): - fixture = self._make_fixture('test image') - num_images = len(self.service.index(self.context)) - - image_id = self.service.create(self.context, fixture)['id'] - - self.assertNotEquals(None, image_id) - self.assertRaises(exception.ImageNotFound, - self.service.show_by_name, - self.context, - 'bad image id') - - def test_update(self): - fixture = self._make_fixture('test image') - image_id = self.service.create(self.context, fixture)['id'] - fixture['status'] = 'in progress' - - self.service.update(self.context, image_id, fixture) - - new_image_data = self.service.show(self.context, image_id) - self.assertEquals('in progress', new_image_data['status']) - - def test_delete(self): - fixture1 = self._make_fixture('test image 1') - fixture2 = self._make_fixture('test image 2') - fixtures = [fixture1, fixture2] - - num_images = len(self.service.index(self.context)) - self.assertEquals(0, num_images, str(self.service.index(self.context))) - - ids = [] - for fixture in fixtures: - new_id = self.service.create(self.context, fixture)['id'] - ids.append(new_id) +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" +NOW_API_FORMAT = "2010-10-11T10:30:22Z" - num_images = len(self.service.index(self.context)) - self.assertEquals(2, num_images, str(self.service.index(self.context))) - self.service.delete(self.context, ids[0]) - - num_images = len(self.service.index(self.context)) - self.assertEquals(1, num_images) - - def test_index(self): - fixture = self._make_fixture('test image') - image_id = self.service.create(self.context, fixture)['id'] - image_metas = self.service.index(self.context) - expected = [{'id': 'DONTCARE', 'name': 'test image'}] - self.assertDictListMatch(image_metas, expected) - - @staticmethod - def _make_fixture(name): - fixture = {'name': name, - 'updated': None, - 'created': None, - 'status': None, - 'is_public': True} - return fixture - - -class GlanceImageServiceTest(_BaseImageServiceTests): - - """Tests the Glance image service, in particular that metadata translation - works properly. - - At a high level, the translations involved are: - - 1. Glance -> ImageService - This is needed so we can support - multple ImageServices (Glance, Local, etc) - - 2. ImageService -> API - This is needed so we can support multple - APIs (OpenStack, EC2) - """ - def setUp(self): - super(GlanceImageServiceTest, self).setUp() - self.stubs = stubout.StubOutForTesting() - fakes.stub_out_glance(self.stubs) - fakes.stub_out_compute_api_snapshot(self.stubs) - service_class = 'nova.image.glance.GlanceImageService' - self.service = utils.import_object(service_class) - self.context = context.RequestContext('fake', 'fake') - self.service.delete_all() - self.sent_to_glance = {} - fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) - - def tearDown(self): - self.stubs.UnsetAll() - super(GlanceImageServiceTest, self).tearDown() - - def test_create_with_instance_id(self): - """Ensure instance_id is persisted as an image-property""" - fixture = {'name': 'test image', - 'is_public': False, - 'properties': {'instance_id': '42', 'user_id': 'fake'}} - - image_id = self.service.create(self.context, fixture)['id'] - expected = fixture - self.assertDictMatch(self.sent_to_glance['metadata'], expected) - - image_meta = self.service.show(self.context, image_id) - expected = {'id': image_id, - 'name': 'test image', - 'is_public': False, - 'properties': {'instance_id': '42', 'user_id': 'fake'}} - self.assertDictMatch(image_meta, expected) - - image_metas = self.service.detail(self.context) - self.assertDictMatch(image_metas[0], expected) - - def test_create_without_instance_id(self): - """ - Ensure we can create an image without having to specify an - instance_id. Public images are an example of an image not tied to an - instance. - """ - fixture = {'name': 'test image'} - image_id = self.service.create(self.context, fixture)['id'] - - expected = {'name': 'test image', 'properties': {}} - self.assertDictMatch(self.sent_to_glance['metadata'], expected) - - def test_index_default_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.index(self.context) - i = 0 - for meta in image_metas: - expected = {'id': 'DONTCARE', - 'name': 'TestImage %d' % (i)} - self.assertDictMatch(meta, expected) - i = i + 1 - - def test_index_marker(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.index(self.context, marker=ids[1]) - self.assertEquals(len(image_metas), 8) - i = 2 - for meta in image_metas: - expected = {'id': 'DONTCARE', - 'name': 'TestImage %d' % (i)} - self.assertDictMatch(meta, expected) - i = i + 1 - - def test_index_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.index(self.context, limit=3) - self.assertEquals(len(image_metas), 3) - - def test_index_marker_and_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.index(self.context, marker=ids[3], limit=1) - self.assertEquals(len(image_metas), 1) - i = 4 - for meta in image_metas: - expected = {'id': 'DONTCARE', - 'name': 'TestImage %d' % (i)} - self.assertDictMatch(meta, expected) - i = i + 1 - - def test_detail_marker(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.detail(self.context, marker=ids[1]) - self.assertEquals(len(image_metas), 8) - i = 2 - for meta in image_metas: - expected = { - 'id': 'DONTCARE', - 'status': None, - 'is_public': True, - 'name': 'TestImage %d' % (i), - 'properties': { - 'updated': None, - 'created': None, - }, - } - - self.assertDictMatch(meta, expected) - i = i + 1 - - def test_detail_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.detail(self.context, limit=3) - self.assertEquals(len(image_metas), 3) - - def test_detail_marker_and_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.detail(self.context, marker=ids[3], limit=3) - self.assertEquals(len(image_metas), 3) - i = 4 - for meta in image_metas: - expected = { - 'id': 'DONTCARE', - 'status': None, - 'is_public': True, - 'name': 'TestImage %d' % (i), - 'properties': { - 'updated': None, 'created': None}, - } - self.assertDictMatch(meta, expected) - i = i + 1 - - -class ImageControllerWithGlanceServiceTest(test.TestCase): +class ImagesTest(test.TestCase): """ Test of the OpenStack API /images application controller w/Glance. """ - NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" - NOW_API_FORMAT = "2010-10-11T10:30:22Z" def setUp(self): """Run before each test.""" - super(ImageControllerWithGlanceServiceTest, self).setUp() - self.flags(image_service='nova.image.glance.GlanceImageService') + super(ImagesTest, self).setUp() self.stubs = stubout.StubOutForTesting() fakes.stub_out_networking(self.stubs) fakes.stub_out_rate_limiting(self.stubs) fakes.stub_out_key_pair_funcs(self.stubs) - self.fixtures = self._make_image_fixtures() - fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures) fakes.stub_out_compute_api_snapshot(self.stubs) fakes.stub_out_compute_api_backup(self.stubs) + fakes.stub_out_glance(self.stubs) def tearDown(self): """Run after each test.""" self.stubs.UnsetAll() - super(ImageControllerWithGlanceServiceTest, self).tearDown() + super(ImagesTest, self).tearDown() def _get_fake_context(self): class Context(object): project_id = 'fake' + auth_token = True return Context() - def _applicable_fixture(self, fixture, user_id): - """Determine if this fixture is applicable for given user id.""" - is_public = fixture["is_public"] - try: - uid = fixture["properties"]["user_id"] - except KeyError: - uid = None - return uid == user_id or is_public - def test_get_image_index(self): request = webob.Request.blank('/v1.0/images') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) response_dict = json.loads(response.body) response_list = response_dict["images"] @@ -365,13 +82,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): {'id': 125, 'name': 'saving snapshot'}, {'id': 126, 'name': 'active snapshot'}, {'id': 127, 'name': 'killed snapshot'}, - {'id': 129, 'name': None}] + {'id': 128, 'name': 'deleted snapshot'}, + {'id': 129, 'name': 'pending_delete snapshot'}, + {'id': 130, 'name': None}] self.assertDictListMatch(response_list, expected) def test_get_image(self): request = webob.Request.blank('/v1.0/images/123') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) self.assertEqual(200, response.status_int) @@ -381,18 +101,19 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "image": { "id": 123, "name": "public image", - "updated": self.NOW_API_FORMAT, - "created": self.NOW_API_FORMAT, + "updated": NOW_API_FORMAT, + "created": NOW_API_FORMAT, "status": "ACTIVE", "progress": 100, }, } - self.assertEqual(expected_image, actual_image) + self.assertDictMatch(expected_image, actual_image) def test_get_image_v1_1(self): request = webob.Request.blank('/v1.1/fake/images/124') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) actual_image = json.loads(response.body) @@ -403,14 +124,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): expected_image = { "image": { - "id": 124, + "id": "124", "name": "queued snapshot", - "updated": self.NOW_API_FORMAT, - "created": self.NOW_API_FORMAT, - "status": "QUEUED", + "updated": NOW_API_FORMAT, + "created": NOW_API_FORMAT, + "status": "SAVING", "progress": 0, 'server': { - 'id': 42, + 'id': '42', "links": [{ "rel": "self", "href": server_href, @@ -440,11 +161,12 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_get_image_xml(self): request = webob.Request.blank('/v1.0/images/123') request.accept = "application/xml" - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) actual_image = minidom.parseString(response.body.replace(" ", "")) - expected_now = self.NOW_API_FORMAT + expected_now = NOW_API_FORMAT expected_image = minidom.parseString(""" <image id="123" name="public image" @@ -458,15 +180,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected_image.toxml(), actual_image.toxml()) def test_get_image_xml_no_name(self): - request = webob.Request.blank('/v1.0/images/129') + request = webob.Request.blank('/v1.0/images/130') request.accept = "application/xml" - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) actual_image = minidom.parseString(response.body.replace(" ", "")) - expected_now = self.NOW_API_FORMAT + expected_now = NOW_API_FORMAT expected_image = minidom.parseString(""" - <image id="129" + <image id="130" name="None" updated="%(expected_now)s" created="%(expected_now)s" @@ -501,12 +224,10 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): expected = minidom.parseString(""" <itemNotFound code="404" - xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> - <message> - Image not found. - </message> + xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> + <message>Image not found.</message> </itemNotFound> - """.replace(" ", "")) + """.replace(" ", "").replace("\n", "")) actual = minidom.parseString(response.body.replace(" ", "")) @@ -538,12 +259,10 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): # because the element hasn't changed definition expected = minidom.parseString(""" <itemNotFound code="404" - xmlns="http://docs.openstack.org/compute/api/v1.1"> - <message> - Image not found. - </message> + xmlns="http://docs.openstack.org/compute/api/v1.1"> + <message>Image not found.</message> </itemNotFound> - """.replace(" ", "")) + """.replace(" ", "").replace("\n", "")) actual = minidom.parseString(response.body.replace(" ", "")) @@ -551,41 +270,133 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_get_image_index_v1_1(self): request = webob.Request.blank('/v1.1/fake/images') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) response_dict = json.loads(response.body) response_list = response_dict["images"] - fixtures = copy.copy(self.fixtures) - - for image in fixtures: - if not self._applicable_fixture(image, "fake"): - fixtures.remove(image) - continue - - href = "http://localhost/v1.1/fake/images/%s" % image["id"] - bookmark = "http://localhost/fake/images/%s" % image["id"] - test_image = { - "id": image["id"], - "name": image["name"], + expected = [ + { + "id": "123", + "name": "public image", "links": [ { "rel": "self", - "href": href, + "href": "http://localhost/v1.1/fake/images/123", }, { "rel": "bookmark", - "href": bookmark, + "href": "http://localhost/fake/images/123", }, ], - } - self.assertTrue(test_image in response_list) + }, + { + "id": "124", + "name": "queued snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }, + ], + }, + { + "id": "125", + "name": "saving snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/125", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/125", + }, + ], + }, + { + "id": "126", + "name": "active snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/126", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/126", + }, + ], + }, + { + "id": "127", + "name": "killed snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/127", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/127", + }, + ], + }, + { + "id": "128", + "name": "deleted snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/128", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/128", + }, + ], + }, + { + "id": "129", + "name": "pending_delete snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/129", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/129", + }, + ], + }, + { + "id": "130", + "name": None, + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/130", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/130", + }, + ], + }, + ] - self.assertEqual(len(response_list), len(fixtures)) + self.assertDictListMatch(response_list, expected) def test_get_image_details(self): request = webob.Request.blank('/v1.0/images/detail') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) response_dict = json.loads(response.body) response_list = response_dict["images"] @@ -593,48 +404,64 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): expected = [{ 'id': 123, 'name': 'public image', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, }, { 'id': 124, 'name': 'queued snapshot', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, - 'status': 'QUEUED', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', 'progress': 0, }, { 'id': 125, 'name': 'saving snapshot', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'SAVING', 'progress': 0, }, { 'id': 126, 'name': 'active snapshot', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, }, { 'id': 127, 'name': 'killed snapshot', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, - 'status': 'FAILED', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ERROR', + 'progress': 0, + }, + { + 'id': 128, + 'name': 'deleted snapshot', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', 'progress': 0, }, { 'id': 129, + 'name': 'pending_delete snapshot', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + }, + { + 'id': 130, 'name': None, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, }] @@ -643,7 +470,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_get_image_details_v1_1(self): request = webob.Request.blank('/v1.1/fake/images/detail') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) response_dict = json.loads(response.body) response_list = response_dict["images"] @@ -651,11 +479,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): server_bookmark = "http://localhost/servers/42" expected = [{ - 'id': 123, + 'id': '123', 'name': 'public image', - 'metadata': {}, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'metadata': {'key1': 'value1'}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, "links": [{ @@ -668,18 +496,18 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 124, + 'id': '124', 'name': 'queued snapshot', 'metadata': { u'instance_ref': u'http://localhost/v1.1/servers/42', u'user_id': u'fake', }, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, - 'status': 'QUEUED', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', 'progress': 0, 'server': { - 'id': 42, + 'id': '42', "links": [{ "rel": "self", "href": server_href, @@ -699,18 +527,18 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 125, + 'id': '125', 'name': 'saving snapshot', 'metadata': { u'instance_ref': u'http://localhost/v1.1/servers/42', u'user_id': u'fake', }, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'SAVING', 'progress': 0, 'server': { - 'id': 42, + 'id': '42', "links": [{ "rel": "self", "href": server_href, @@ -730,18 +558,18 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 126, + 'id': '126', 'name': 'active snapshot', 'metadata': { u'instance_ref': u'http://localhost/v1.1/servers/42', u'user_id': u'fake', }, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, 'server': { - 'id': 42, + 'id': '42', "links": [{ "rel": "self", "href": server_href, @@ -761,18 +589,18 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 127, + 'id': '127', 'name': 'killed snapshot', 'metadata': { u'instance_ref': u'http://localhost/v1.1/servers/42', u'user_id': u'fake', }, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, - 'status': 'FAILED', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ERROR', 'progress': 0, 'server': { - 'id': 42, + 'id': '42', "links": [{ "rel": "self", "href": server_href, @@ -792,20 +620,82 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 129, + 'id': '128', + 'name': 'deleted snapshot', + 'metadata': { + u'instance_ref': u'http://localhost/v1.1/servers/42', + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + 'server': { + 'id': '42', + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v1.1/fake/images/128", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/128", + }], + }, + { + 'id': '129', + 'name': 'pending_delete snapshot', + 'metadata': { + u'instance_ref': u'http://localhost/v1.1/servers/42', + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + 'server': { + 'id': '42', + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v1.1/fake/images/129", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/129", + }], + }, + { + 'id': '130', 'name': None, 'metadata': {}, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, "links": [{ "rel": "self", - "href": "http://localhost/v1.1/fake/images/129", + "href": "http://localhost/v1.1/fake/images/130", }, { "rel": "bookmark", - "href": "http://localhost/fake/images/129", + "href": "http://localhost/fake/images/130", }], }, ] @@ -1017,11 +907,12 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_get_image_found(self): req = webob.Request.blank('/v1.0/images/123') - res = req.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + res = req.get_response(app) image_meta = json.loads(res.body)['image'] expected = {'id': 123, 'name': 'public image', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100} self.assertDictMatch(image_meta, expected) @@ -1030,14 +921,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 404) - def test_get_image_not_owned(self): - """We should return a 404 if we request an image that doesn't belong - to us - """ - req = webob.Request.blank('/v1.0/images/128') - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 404) - def test_create_image(self): body = dict(image=dict(serverId='123', name='Snapshot 1')) req = webob.Request.blank('/v1.0/images') @@ -1080,49 +963,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(400, response.status_int) - @classmethod - def _make_image_fixtures(cls): - image_id = 123 - base_attrs = {'created_at': cls.NOW_GLANCE_FORMAT, - 'updated_at': cls.NOW_GLANCE_FORMAT, - 'deleted_at': None, - 'deleted': False} - - fixtures = [] - - def add_fixture(**kwargs): - kwargs.update(base_attrs) - fixtures.append(kwargs) - - # Public image - add_fixture(id=image_id, name='public image', is_public=True, - status='active', properties={}) - image_id += 1 - - # Snapshot for User 1 - server_ref = 'http://localhost/v1.1/servers/42' - snapshot_properties = {'instance_ref': server_ref, 'user_id': 'fake'} - for status in ('queued', 'saving', 'active', 'killed'): - add_fixture(id=image_id, name='%s snapshot' % status, - is_public=False, status=status, - properties=snapshot_properties) - image_id += 1 - - # Snapshot for User 2 - other_snapshot_properties = {'instance_id': '43', 'user_id': 'other'} - add_fixture(id=image_id, name='someone elses snapshot', - is_public=False, status='active', - properties=other_snapshot_properties) - - image_id += 1 - - # Image without a name - add_fixture(id=image_id, is_public=True, status='active', - properties={}) - image_id += 1 - - return fixtures - class ImageXMLSerializationTest(test.TestCase): @@ -1132,7 +972,7 @@ class ImageXMLSerializationTest(test.TestCase): IMAGE_HREF = 'http://localhost/v1.1/fake/images/%s' IMAGE_BOOKMARK = 'http://localhost/fake/images/%s' - def test_show(self): + def test_xml_declaration(self): serializer = images.ImageXMLSerializer() fixture = { @@ -1144,7 +984,7 @@ class ImageXMLSerializationTest(test.TestCase): 'status': 'ACTIVE', 'progress': 80, 'server': { - 'id': 1, + 'id': '1', 'links': [ { 'href': self.SERVER_HREF, @@ -1173,37 +1013,80 @@ class ImageXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'show') - actual = minidom.parseString(output.replace(" ", "")) + print output + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_href = self.IMAGE_HREF % 1 - expected_bookmark = self.IMAGE_BOOKMARK % 1 - expected_now = self.TIMESTAMP - expected = minidom.parseString(""" - <image id="1" - xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" - name="Image1" - updated="%(expected_now)s" - created="%(expected_now)s" - status="ACTIVE" - progress="80"> - <server id="1"> - <atom:link rel="self" href="%(expected_server_href)s"/> - <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> - </server> - <metadata> - <meta key="key1"> - value1 - </meta> - </metadata> - <atom:link href="%(expected_href)s" rel="self"/> - <atom:link href="%(expected_bookmark)s" rel="bookmark"/> - </image> - """.replace(" ", "") % (locals())) + def test_show(self): + serializer = images.ImageXMLSerializer() - self.assertEqual(expected.toxml(), actual.toxml()) + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'server': { + 'id': '1', + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_show_zero_metadata(self): serializer = images.ImageXMLSerializer() @@ -1216,7 +1099,7 @@ class ImageXMLSerializationTest(test.TestCase): 'updated': self.TIMESTAMP, 'status': 'ACTIVE', 'server': { - 'id': 1, + 'id': '1', 'links': [ { 'href': self.SERVER_HREF, @@ -1243,31 +1126,31 @@ class ImageXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'show') - actual = minidom.parseString(output.replace(" ", "")) - - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_href = self.IMAGE_HREF % 1 - expected_bookmark = self.IMAGE_BOOKMARK % 1 - expected_now = self.TIMESTAMP - expected = minidom.parseString(""" - <image id="1" - xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" - name="Image1" - updated="%(expected_now)s" - created="%(expected_now)s" - status="ACTIVE"> - <server id="1"> - <atom:link rel="self" href="%(expected_server_href)s"/> - <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> - </server> - <atom:link href="%(expected_href)s" rel="self"/> - <atom:link href="%(expected_bookmark)s" rel="bookmark"/> - </image> - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + meta_nodes = root.findall('{0}meta'.format(ATOMNS)) + self.assertEqual(len(meta_nodes), 0) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_show_image_no_metadata_key(self): serializer = images.ImageXMLSerializer() @@ -1280,7 +1163,7 @@ class ImageXMLSerializationTest(test.TestCase): 'updated': self.TIMESTAMP, 'status': 'ACTIVE', 'server': { - 'id': 1, + 'id': '1', 'links': [ { 'href': self.SERVER_HREF, @@ -1306,31 +1189,31 @@ class ImageXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'show') - actual = minidom.parseString(output.replace(" ", "")) - - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_href = self.IMAGE_HREF % 1 - expected_bookmark = self.IMAGE_BOOKMARK % 1 - expected_now = self.TIMESTAMP - expected = minidom.parseString(""" - <image id="1" - xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" - name="Image1" - updated="%(expected_now)s" - created="%(expected_now)s" - status="ACTIVE"> - <server id="1"> - <atom:link rel="self" href="%(expected_server_href)s"/> - <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> - </server> - <atom:link href="%(expected_href)s" rel="self"/> - <atom:link href="%(expected_bookmark)s" rel="bookmark"/> - </image> - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + meta_nodes = root.findall('{0}meta'.format(ATOMNS)) + self.assertEqual(len(meta_nodes), 0) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_show_no_server(self): serializer = images.ImageXMLSerializer() @@ -1359,30 +1242,30 @@ class ImageXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'show') - actual = minidom.parseString(output.replace(" ", "")) - - expected_href = self.IMAGE_HREF % 1 - expected_bookmark = self.IMAGE_BOOKMARK % 1 - expected_now = self.TIMESTAMP - expected = minidom.parseString(""" - <image id="1" - xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" - name="Image1" - updated="%(expected_now)s" - created="%(expected_now)s" - status="ACTIVE"> - <metadata> - <meta key="key1"> - value1 - </meta> - </metadata> - <atom:link href="%(expected_href)s" rel="self"/> - <atom:link href="%(expected_bookmark)s" rel="bookmark"/> - </image> - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root, None) def test_index(self): serializer = images.ImageXMLSerializer() @@ -1397,6 +1280,10 @@ class ImageXMLSerializationTest(test.TestCase): 'href': self.IMAGE_HREF % 1, 'rel': 'self', }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, ], }, { @@ -1407,35 +1294,32 @@ class ImageXMLSerializationTest(test.TestCase): 'href': self.IMAGE_HREF % 2, 'rel': 'self', }, + { + 'href': self.IMAGE_BOOKMARK % 2, + 'rel': 'bookmark', + }, ], }, ] } output = serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_href = self.IMAGE_HREF % 1 - expected_bookmark = self.IMAGE_BOOKMARK % 1 - expected_href_two = self.IMAGE_HREF % 2 - expected_bookmark_two = self.IMAGE_BOOKMARK % 2 - expected_now = self.TIMESTAMP - expected = minidom.parseString(""" - <images - xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom"> - <image id="1" name="Image1"> - <atom:link href="%(expected_href)s" rel="self"/> - </image> - <image id="2" name="Image2"> - <atom:link href="%(expected_href_two)s" rel="self"/> - </image> - </images> - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'images_index') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 2) + for i, image_elem in enumerate(image_elems): + image_dict = fixture['images'][i] + + for key in ['name', 'id']: + self.assertEqual(image_elem.get(key), str(image_dict[key])) + + link_nodes = image_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_index_zero_images(self): serializer = images.ImageXMLSerializer() @@ -1445,15 +1329,11 @@ class ImageXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixtures, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <images - xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" /> - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'images_index') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 0) def test_detail(self): serializer = images.ImageXMLSerializer() @@ -1467,7 +1347,7 @@ class ImageXMLSerializationTest(test.TestCase): 'updated': self.TIMESTAMP, 'status': 'ACTIVE', 'server': { - 'id': 1, + 'id': '1', 'links': [ { 'href': self.SERVER_HREF, @@ -1491,7 +1371,7 @@ class ImageXMLSerializationTest(test.TestCase): ], }, { - 'id': 2, + 'id': '2', 'name': 'Image2', 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, @@ -1515,46 +1395,22 @@ class ImageXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'detail') - actual = minidom.parseString(output.replace(" ", "")) - - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_href = self.IMAGE_HREF % 1 - expected_bookmark = self.IMAGE_BOOKMARK % 1 - expected_href_two = self.IMAGE_HREF % 2 - expected_bookmark_two = self.IMAGE_BOOKMARK % 2 - expected_now = self.TIMESTAMP - expected = minidom.parseString(""" - <images - xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom"> - <image id="1" - name="Image1" - updated="%(expected_now)s" - created="%(expected_now)s" - status="ACTIVE"> - <server id="1"> - <atom:link rel="self" href="%(expected_server_href)s"/> - <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> - </server> - <atom:link href="%(expected_href)s" rel="self"/> - <atom:link href="%(expected_bookmark)s" rel="bookmark"/> - </image> - <image id="2" - name="Image2" - updated="%(expected_now)s" - created="%(expected_now)s" - status="SAVING" - progress="80"> - <metadata> - <meta key="key1"> - value1 - </meta> - </metadata> - <atom:link href="%(expected_href_two)s" rel="self"/> - <atom:link href="%(expected_bookmark_two)s" rel="bookmark"/> - </image> - </images> - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'images') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 2) + for i, image_elem in enumerate(image_elems): + image_dict = fixture['images'][i] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(image_elem.get(key), str(image_dict[key])) + + link_nodes = image_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = image_elem.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index 801b06230..6f0210c27 100644 --- a/nova/tests/api/openstack/test_limits.py +++ b/nova/tests/api/openstack/test_limits.py @@ -19,6 +19,7 @@ Tests dealing with HTTP rate-limiting. import httplib import json +from lxml import etree import StringIO import stubout import time @@ -29,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 xmlutil from nova import test @@ -39,6 +41,10 @@ TEST_LIMITS = [ limits.Limit("PUT", "*", "", 10, limits.PER_MINUTE), limits.Limit("PUT", "/servers", "^/servers", 5, limits.PER_MINUTE), ] +NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/compute/api/v1.1' +} class BaseLimitTestSuite(unittest.TestCase): @@ -168,12 +174,11 @@ class LimitsControllerV10Test(BaseLimitTestSuite): response = request.get_response(self.controller) expected = minidom.parseString(""" - <limits - xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> + <limits xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> <rate/> <absolute/> </limits> - """.replace(" ", "")) + """.replace(" ", "").replace("\n", "")) body = minidom.parseString(response.body.replace(" ", "")) @@ -186,17 +191,16 @@ class LimitsControllerV10Test(BaseLimitTestSuite): response = request.get_response(self.controller) expected = minidom.parseString(""" - <limits - xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> + <limits xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> <rate> <limit URI="*" regex=".*" remaining="10" resetTime="0" - unit="MINUTE" value="10" verb="GET"/> + unit="MINUTE" value="10" verb="GET"/> <limit URI="*" regex=".*" remaining="5" resetTime="0" - unit="HOUR" value="5" verb="POST"/> + unit="HOUR" value="5" verb="POST"/> </rate> <absolute/> </limits> - """.replace(" ", "")) + """.replace(" ", "").replace("\n", "")) body = minidom.parseString(response.body.replace(" ", "")) self.assertEqual(expected.toxml(), body.toxml()) @@ -980,9 +984,22 @@ class LimitsXMLSerializationTest(test.TestCase): def tearDown(self): pass - def test_index(self): + def test_xml_declaration(self): serializer = limits.LimitsXMLSerializer() + fixture = {"limits": { + "rate": [], + "absolute": {}}} + + output = serializer.serialize(fixture, 'index') + print output + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + + def test_index(self): + serializer = limits.LimitsXMLSerializer() + fixture = { + "limits": { "rate": [{ "uri": "*", "regex": ".*", @@ -1006,32 +1023,32 @@ class LimitsXMLSerializationTest(test.TestCase): "maxPersonalitySize": 10240}}} output = serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <limits xmlns="http://docs.openstack.org/compute/api/v1.1"> - <rates> - <rate uri="*" regex=".*"> - <limit value="10" verb="POST" remaining="2" - unit="MINUTE" - next-available="2011-12-15T22:42:45Z"/> - </rate> - <rate uri="*/servers" regex="^/servers"> - <limit value="50" verb="POST" remaining="10" - unit="DAY" - next-available="2011-12-15T22:42:45Z"/> - </rate> - </rates> - <absolute> - <limit name="maxServerMeta" value="1"/> - <limit name="maxPersonality" value="5"/> - <limit name="maxImageMeta" value="1"/> - <limit name="maxPersonalitySize" value="10240"/> - </absolute> - </limits> - """.replace(" ", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'limits') + + #verify absolute limits + absolutes = root.xpath('ns:absolute/ns:limit', namespaces=NS) + self.assertEqual(len(absolutes), 4) + for limit in absolutes: + name = limit.get('name') + value = limit.get('value') + self.assertEqual(value, str(fixture['limits']['absolute'][name])) + + #verify rate limits + rates = root.xpath('ns:rates/ns:rate', namespaces=NS) + self.assertEqual(len(rates), 2) + for i, rate in enumerate(rates): + for key in ['uri', 'regex']: + self.assertEqual(rate.get(key), + str(fixture['limits']['rate'][i][key])) + rate_limits = rate.xpath('ns:limit', namespaces=NS) + self.assertEqual(len(rate_limits), 1) + for j, limit in enumerate(rate_limits): + for key in ['verb', 'value', 'remaining', 'unit', + 'next-available']: + self.assertEqual(limit.get(key), + str(fixture['limits']['rate'][i]['limit'][j][key])) def test_index_no_limits(self): serializer = limits.LimitsXMLSerializer() @@ -1041,13 +1058,14 @@ class LimitsXMLSerializationTest(test.TestCase): "absolute": {}}} output = serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'limits') - expected = minidom.parseString(""" - <limits xmlns="http://docs.openstack.org/compute/api/v1.1"> - <rates /> - <absolute /> - </limits> - """.replace(" ", "")) + #verify absolute limits + absolutes = root.xpath('ns:absolute/ns:limit', namespaces=NS) + self.assertEqual(len(absolutes), 0) - self.assertEqual(expected.toxml(), actual.toxml()) + #verify rate limits + rates = root.xpath('ns:rates/ns:rate', namespaces=NS) + self.assertEqual(len(rates), 0) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 1591ea56c..ee7927c64 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -52,6 +52,10 @@ from nova.tests.api.openstack import fakes FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' NS = "{http://docs.openstack.org/compute/api/v1.1}" ATOMNS = "{http://www.w3.org/2005/Atom}" +XPATH_NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/compute/api/v1.1' +} def fake_gen_uuid(): @@ -347,6 +351,8 @@ class ServersTest(test.TestCase): "server": { "id": 1, "uuid": FAKE_UUID, + "user_id": "fake", + "tenant_id": "fake", "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 0, @@ -410,12 +416,7 @@ class ServersTest(test.TestCase): def test_get_server_by_id_v1_1_xml(self): image_bookmark = "http://localhost/fake/images/10" - flavor_ref = "http://localhost/v1.1/fake/flavors/1" - flavor_id = "1" flavor_bookmark = "http://localhost/fake/flavors/1" - server_href = "http://localhost/v1.1/fake/servers/1" - server_bookmark = "http://localhost/fake/servers/1" - public_ip = '192.168.0.3' private_ip = '172.19.0.1' interfaces = [ @@ -439,48 +440,88 @@ class ServersTest(test.TestCase): req = webob.Request.blank('/v1.1/fake/servers/1') req.headers['Accept'] = 'application/xml' res = req.get_response(fakes.wsgi_app()) - actual = minidom.parseString(res.body.replace(' ', '')) - expected_uuid = FAKE_UUID - expected_updated = "2010-11-11T11:00:00Z" - expected_created = "2010-10-10T12:00:00Z" - expected = minidom.parseString(""" - <server id="1" - uuid="%(expected_uuid)s" - xmlns="http://docs.openstack.org/compute/api/v1.1" - xmlns:atom="http://www.w3.org/2005/Atom" - name="server1" - updated="%(expected_updated)s" - created="%(expected_created)s" - hostId="" - status="BUILD" - accessIPv4="" - accessIPv6="" - progress="0"> - <atom:link href="%(server_href)s" rel="self"/> - <atom:link href="%(server_bookmark)s" rel="bookmark"/> - <image id="10"> - <atom:link rel="bookmark" href="%(image_bookmark)s"/> - </image> - <flavor id="1"> - <atom:link rel="bookmark" href="%(flavor_bookmark)s"/> - </flavor> - <metadata> - <meta key="seq"> - 1 - </meta> - </metadata> - <addresses> - <network id="public"> - <ip version="4" addr="%(public_ip)s"/> - </network> - <network id="private"> - <ip version="4" addr="%(private_ip)s"/> - </network> - </addresses> - </server> - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + output = res.body + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + expected = { + 'id': 1, + 'uuid': FAKE_UUID, + 'user_id': 'fake', + 'tenant_id': 'fake', + 'updated': '2010-11-11T11:00:00Z', + 'created': '2010-10-10T12:00:00Z', + 'progress': 0, + 'name': 'server1', + 'status': 'BUILD', + 'accessIPv4': '', + 'accessIPv6': '', + 'hostId': '', + 'key_name': '', + 'image': { + 'id': '10', + 'links': [{'rel': 'bookmark', 'href': image_bookmark}], + }, + 'flavor': { + 'id': '1', + 'links': [{'rel': 'bookmark', 'href': flavor_bookmark}], + }, + 'addresses': { + 'public': [{'version': 4, 'addr': public_ip}], + 'private': [{'version': 4, 'addr': private_ip}], + }, + 'metadata': {'seq': '1'}, + 'config_drive': None, + 'links': [ + { + 'rel': 'self', + 'href': 'http://localhost/v1.1/fake/servers/1', + }, + { + 'rel': 'bookmark', + 'href': 'http://localhost/fake/servers/1', + }, + ], + } + + self.assertTrue(root.xpath('/ns:server', namespaces=XPATH_NS)) + for key in ['id', 'uuid', 'created', 'progress', 'name', 'status', + 'accessIPv4', 'accessIPv6', 'hostId']: + self.assertEqual(root.get(key), str(expected[key])) + self.assertEqual(root.get('userId'), str(expected['user_id'])) + self.assertEqual(root.get('tenantId'), str(expected['tenant_id'])) + + (image,) = root.xpath('ns:image', namespaces=XPATH_NS) + self.assertEqual(image.get('id'), str(expected['image']['id'])) + + links = root.xpath('ns:image/atom:link', namespaces=XPATH_NS) + self.assertTrue(common.compare_links(links, + expected['image']['links'])) + + (flavor,) = root.xpath('ns:flavor', namespaces=XPATH_NS) + self.assertEqual(flavor.get('id'), str(expected['flavor']['id'])) + + (meta,) = root.xpath('ns:metadata/ns:meta', namespaces=XPATH_NS) + self.assertEqual(meta.get('key'), 'seq') + self.assertEqual(meta.text, '1') + + (pub_network, priv_network) = root.xpath('ns:addresses/ns:network', + namespaces=XPATH_NS) + self.assertEqual(pub_network.get('id'), 'public') + (pub_ip,) = pub_network.xpath('ns:ip', namespaces=XPATH_NS) + (priv_ip,) = priv_network.xpath('ns:ip', namespaces=XPATH_NS) + self.assertEqual(pub_ip.get('version'), + str(expected['addresses']['public'][0]['version'])) + self.assertEqual(pub_ip.get('addr'), + str(expected['addresses']['public'][0]['addr'])) + self.assertEqual(priv_ip.get('version'), + str(expected['addresses']['private'][0]['version'])) + self.assertEqual(priv_ip.get('addr'), + str(expected['addresses']['private'][0]['addr'])) + + links = root.xpath('atom:link', namespaces=XPATH_NS) + self.assertTrue(common.compare_links(links, expected['links'])) def test_get_server_with_active_status_by_id_v1_1(self): image_bookmark = "http://localhost/fake/images/10" @@ -515,6 +556,8 @@ class ServersTest(test.TestCase): "server": { "id": 1, "uuid": FAKE_UUID, + "user_id": "fake", + "tenant_id": "fake", "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 100, @@ -610,6 +653,8 @@ class ServersTest(test.TestCase): "server": { "id": 1, "uuid": FAKE_UUID, + "user_id": "fake", + "tenant_id": "fake", "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 100, @@ -1199,6 +1244,26 @@ class ServersTest(test.TestCase): self.assertEqual(len(servers), 1) self.assertEqual(servers[0]['id'], 100) + def test_tenant_id_filter_converts_to_project_id_for_admin(self): + def fake_get_all(context, filters=None): + self.assertNotEqual(filters, None) + self.assertEqual(filters['project_id'], 'faketenant') + self.assertFalse(filters.get('tenant_id')) + return [stub_instance(100)] + + self.stubs.Set(nova.db.api, 'instance_get_all_by_filters', + fake_get_all) + self.flags(allow_admin_api=True) + + req = webob.Request.blank('/v1.1/fake/servers?tenant_id=faketenant') + # Use admin context + context = nova.context.RequestContext('testuser', 'testproject', + is_admin=True) + res = req.get_response(fakes.wsgi_app(fake_auth_context=context)) + res_dict = json.loads(res.body) + # Failure in fake_get_all returns non 200 status code + self.assertEqual(res.status_int, 200) + def test_get_servers_allows_flavor_v1_1(self): def fake_get_all(compute_self, context, search_opts=None): self.assertNotEqual(search_opts, None) @@ -1455,6 +1520,8 @@ class ServersTest(test.TestCase): 'access_ip_v4': '1.2.3.4', 'access_ip_v6': 'fead::1234', 'image_ref': image_ref, + 'user_id': 'fake', + 'project_id': 'fake', "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), "config_drive": self.config_drive, @@ -3103,7 +3170,7 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "1", "flavorRef": "1", - "networks": [] + "networks": [], }} self.assertEquals(request['body'], expected) @@ -3255,7 +3322,7 @@ class TestAddressesXMLSerialization(test.TestCase): serializer = nova.api.openstack.ips.IPXMLSerializer() - def test_show(self): + def test_xml_declaration(self): fixture = { 'network_2': [ {'addr': '192.168.0.1', 'version': 4}, @@ -3263,17 +3330,29 @@ class TestAddressesXMLSerialization(test.TestCase): ], } output = self.serializer.serialize(fixture, 'show') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <network xmlns="http://docs.openstack.org/compute/api/v1.1" - id="network_2"> - <ip version="4" addr="192.168.0.1"/> - <ip version="6" addr="fe80::beef"/> - </network> - """.replace(" ", "")) + print output + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) - self.assertEqual(expected.toxml(), actual.toxml()) + def test_show(self): + fixture = { + 'network_2': [ + {'addr': '192.168.0.1', 'version': 4}, + {'addr': 'fe80::beef', 'version': 6}, + ], + } + output = self.serializer.serialize(fixture, 'show') + print output + root = etree.XML(output) + network = fixture['network_2'] + self.assertEqual(str(root.get('id')), 'network_2') + ip_elems = root.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) def test_index(self): fixture = { @@ -3289,22 +3368,22 @@ class TestAddressesXMLSerialization(test.TestCase): }, } output = self.serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <addresses xmlns="http://docs.openstack.org/compute/api/v1.1"> - <network id="network_2"> - <ip version="4" addr="192.168.0.1"/> - <ip version="6" addr="fe80::beef"/> - </network> - <network id="network_1"> - <ip version="4" addr="192.168.0.3"/> - <ip version="4" addr="192.168.0.5"/> - </network> - </addresses> - """.replace(" ", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'addresses') + addresses_dict = fixture['addresses'] + network_elems = root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) class TestServerInstanceCreation(test.TestCase): @@ -3330,6 +3409,8 @@ class TestServerInstanceCreation(test.TestCase): self.injected_files = None return [{'id': '1234', 'display_name': 'fakeinstance', + 'user_id': 'fake', + 'project_id': 'fake', 'uuid': FAKE_UUID}] def set_admin_password(self, *args, **kwargs): @@ -3583,10 +3664,14 @@ class TestGetKernelRamdiskFromImage(test.TestCase): self.assertRaises(exception.NotFound, self._get_k_r, image_meta) def test_ami_no_ramdisk(self): - """If an ami is missing a ramdisk it should raise NotFound""" + """If an ami is missing a ramdisk, return kernel ID and None for + ramdisk ID + """ image_meta = {'id': 1, 'status': 'active', 'container_format': 'ami', 'properties': {'kernel_id': 1}} - self.assertRaises(exception.NotFound, self._get_k_r, image_meta) + kernel_id, ramdisk_id = self._get_k_r(image_meta) + self.assertEqual(kernel_id, 1) + self.assertEqual(ramdisk_id, None) def test_ami_kernel_ramdisk_present(self): """Return IDs if both kernel and ramdisk are present""" @@ -3621,8 +3706,8 @@ class ServersViewBuilderV11Test(test.TestCase): "created_at": created_at, "updated_at": updated_at, "admin_pass": "", - "user_id": "", - "project_id": "", + "user_id": "fake", + "project_id": "fake", "image_ref": "5", "kernel_id": "", "ramdisk_id": "", @@ -3647,7 +3732,6 @@ class ServersViewBuilderV11Test(test.TestCase): "terminated_at": utils.utcnow(), "availability_zone": "", "display_name": "test_server", - "display_description": "", "locked": False, "metadata": [], "accessIPv4": "1.2.3.4", @@ -3680,7 +3764,6 @@ class ServersViewBuilderV11Test(test.TestCase): "id": 1, "uuid": self.instance['uuid'], "name": "test_server", - "key_name": '', "links": [ { "rel": "self", @@ -3691,7 +3774,6 @@ class ServersViewBuilderV11Test(test.TestCase): "href": "http://localhost/servers/1", }, ], - "config_drive": None, } } @@ -3704,8 +3786,6 @@ class ServersViewBuilderV11Test(test.TestCase): "id": 1, "uuid": self.instance['uuid'], "name": "test_server", - "key_name": '', - "config_drive": None, "links": [ { "rel": "self", @@ -3730,6 +3810,8 @@ class ServersViewBuilderV11Test(test.TestCase): "server": { "id": 1, "uuid": self.instance['uuid'], + "user_id": "fake", + "tenant_id": "fake", "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 0, @@ -3785,6 +3867,8 @@ class ServersViewBuilderV11Test(test.TestCase): "server": { "id": 1, "uuid": self.instance['uuid'], + "user_id": "fake", + "tenant_id": "fake", "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 100, @@ -3841,6 +3925,8 @@ class ServersViewBuilderV11Test(test.TestCase): "server": { "id": 1, "uuid": self.instance['uuid'], + "user_id": "fake", + "tenant_id": "fake", "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 0, @@ -3897,6 +3983,8 @@ class ServersViewBuilderV11Test(test.TestCase): "server": { "id": 1, "uuid": self.instance['uuid'], + "user_id": "fake", + "tenant_id": "fake", "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 0, @@ -3956,6 +4044,8 @@ class ServersViewBuilderV11Test(test.TestCase): "server": { "id": 1, "uuid": self.instance['uuid'], + "user_id": "fake", + "tenant_id": "fake", "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 0, @@ -4018,12 +4108,93 @@ class ServerXMLSerializationTest(test.TestCase): self.maxDiff = None test.TestCase.setUp(self) + def test_xml_declaration(self): + serializer = servers.ServerXMLSerializer() + + fixture = { + "server": { + 'id': 1, + 'uuid': FAKE_UUID, + 'user_id': 'fake_user_id', + 'tenant_id': 'fake_tenant_id', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": self.IMAGE_BOOKMARK, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": self.FLAVOR_BOOKMARK, + }, + ], + }, + "addresses": { + "network_one": [ + { + "version": 4, + "addr": "67.23.10.138", + }, + { + "version": 6, + "addr": "::babe:67.23.10.138", + }, + ], + "network_two": [ + { + "version": 4, + "addr": "67.23.10.139", + }, + { + "version": 6, + "addr": "::babe:67.23.10.139", + }, + ], + }, + "metadata": { + "Open": "Stack", + "Number": "1", + }, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + } + } + + output = serializer.serialize(fixture, 'show') + print output + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + def test_show(self): serializer = servers.ServerXMLSerializer() fixture = { "server": { "id": 1, + "user_id": "fake", + "tenant_id": "fake", "uuid": FAKE_UUID, 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, @@ -4161,6 +4332,8 @@ class ServerXMLSerializationTest(test.TestCase): "server": { "id": 1, "uuid": FAKE_UUID, + "user_id": "fake", + "tenant_id": "fake", 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, "progress": 0, @@ -4361,6 +4534,8 @@ class ServerXMLSerializationTest(test.TestCase): { "id": 1, "uuid": FAKE_UUID, + "user_id": "fake", + "tenant_id": "fake", 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, "progress": 0, @@ -4416,6 +4591,8 @@ class ServerXMLSerializationTest(test.TestCase): { "id": 2, "uuid": FAKE_UUID, + "user_id": 'fake', + "tenant_id": 'fake', 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, "progress": 100, @@ -4535,6 +4712,8 @@ class ServerXMLSerializationTest(test.TestCase): fixture = { "server": { "id": 1, + "user_id": "fake", + "tenant_id": "fake", "uuid": FAKE_UUID, 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, @@ -4671,6 +4850,8 @@ class ServerXMLSerializationTest(test.TestCase): "server": { "id": 1, "uuid": FAKE_UUID, + "user_id": "fake", + "tenant_id": "fake", 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, "progress": 0, diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 1269f13c9..f69dbd316 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -15,19 +15,24 @@ # License for the specific language governing permissions and limitations # under the License. +import feedparser import json import stubout import webob -import xml.etree.ElementTree - +from lxml import etree from nova import context from nova import test -from nova.tests.api.openstack import fakes from nova.api.openstack import versions from nova.api.openstack import views from nova.api.openstack import wsgi +from nova.tests.api.openstack import common +from nova.tests.api.openstack import fakes +NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/compute/api/v1.1' +} VERSIONS = { "v1.0": { "id": "v1.0", @@ -113,23 +118,23 @@ class VersionsTest(test.TestCase): versions = json.loads(res.body)["versions"] expected = [ { - "id": "v1.1", - "status": "CURRENT", + "id": "v1.0", + "status": "DEPRECATED", "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", - "href": "http://localhost/v1.1/", + "href": "http://localhost/v1.0/", }], }, { - "id": "v1.0", - "status": "DEPRECATED", + "id": "v1.1", + "status": "CURRENT", "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", - "href": "http://localhost/v1.0/", + "href": "http://localhost/v1.1/", }], }, ] @@ -233,48 +238,20 @@ class VersionsTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/xml") - root = xml.etree.ElementTree.XML(res.body) - self.assertEqual(root.tag.split('}')[1], "version") - self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11) - children = list(root) - media_types = children[0] - media_type_nodes = list(media_types) - links = (children[1], children[2], children[3]) - - self.assertEqual(media_types.tag.split('}')[1], 'media-types') - for media_node in media_type_nodes: - self.assertEqual(media_node.tag.split('}')[1], 'media-type') - - expected = """ - <version id="v1.0" status="DEPRECATED" - updated="2011-01-21T11:33:21Z" - xmlns="%s" - xmlns:atom="http://www.w3.org/2005/Atom"> - - <media-types> - <media-type base="application/xml" - type="application/vnd.openstack.compute-v1.0+xml"/> - <media-type base="application/json" - type="application/vnd.openstack.compute-v1.0+json"/> - </media-types> - - <atom:link href="http://localhost/v1.0/" - rel="self"/> - - <atom:link href="http://docs.rackspacecloud.com/servers/ - api/v1.0/cs-devguide-20110125.pdf" - rel="describedby" - type="application/pdf"/> - - <atom:link href="http://docs.rackspacecloud.com/servers/ - api/v1.0/application.wadl" - rel="describedby" - type="application/vnd.sun.wadl+xml"/> - </version>""".replace(" ", "").replace("\n", "") % wsgi.XMLNS_V11 - - actual = res.body.replace(" ", "").replace("\n", "") - self.assertEqual(expected, actual) + version = etree.XML(res.body) + expected = VERSIONS['v1.0'] + self.assertTrue(version.xpath('/ns:version', namespaces=NS)) + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common.compare_media_types(media_types, + expected['media-types'])) + for key in ['id', 'status', 'updated']: + self.assertEqual(version.get(key), expected[key]) + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v1.0/'}] + + expected['links'])) def test_get_version_1_1_detail_xml(self): req = webob.Request.blank('/v1.1/') @@ -282,35 +259,20 @@ class VersionsTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/xml") - expected = """ - <version id="v1.1" status="CURRENT" - updated="2011-01-21T11:33:21Z" - xmlns="%s" - xmlns:atom="http://www.w3.org/2005/Atom"> - - <media-types> - <media-type base="application/xml" - type="application/vnd.openstack.compute-v1.1+xml"/> - <media-type base="application/json" - type="application/vnd.openstack.compute-v1.1+json"/> - </media-types> - - <atom:link href="http://localhost/v1.1/" - rel="self"/> - - <atom:link href="http://docs.rackspacecloud.com/servers/ - api/v1.1/cs-devguide-20110125.pdf" - rel="describedby" - type="application/pdf"/> - - <atom:link href="http://docs.rackspacecloud.com/servers/ - api/v1.1/application.wadl" - rel="describedby" - type="application/vnd.sun.wadl+xml"/> - </version>""".replace(" ", "").replace("\n", "") % wsgi.XMLNS_V11 - - actual = res.body.replace(" ", "").replace("\n", "") - self.assertEqual(expected, actual) + + version = etree.XML(res.body) + expected = VERSIONS['v1.1'] + self.assertTrue(version.xpath('/ns:version', namespaces=NS)) + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common.compare_media_types(media_types, + expected['media-types'])) + for key in ['id', 'status', 'updated']: + self.assertEqual(version.get(key), expected[key]) + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v1.1/'}] + + expected['links'])) def test_get_version_list_xml(self): req = webob.Request.blank('/') @@ -319,21 +281,19 @@ class VersionsTest(test.TestCase): self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/xml") - expected = """ - <versions xmlns="%s" xmlns:atom="%s"> - <version id="v1.1" status="CURRENT" updated="2011-01-21T11:33:21Z"> - <atom:link href="http://localhost/v1.1/" rel="self"/> - </version> - <version id="v1.0" status="DEPRECATED" - updated="2011-01-21T11:33:21Z"> - <atom:link href="http://localhost/v1.0/" rel="self"/> - </version> - </versions>""".replace(" ", "").replace("\n", "") % (wsgi.XMLNS_V11, - wsgi.XMLNS_ATOM) + root = etree.XML(res.body) + self.assertTrue(root.xpath('/ns:versions', namespaces=NS)) + versions = root.xpath('ns:version', namespaces=NS) + self.assertEqual(len(versions), 2) - actual = res.body.replace(" ", "").replace("\n", "") - - self.assertEqual(expected, actual) + for i, v in enumerate(['v1.0', 'v1.1']): + version = versions[i] + expected = VERSIONS[v] + for key in ['id', 'status', 'updated']: + self.assertEqual(version.get(key), expected[key]) + (link,) = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(link, + [{'rel': 'self', 'href': 'http://localhost/%s/' % v}])) def test_get_version_1_0_detail_atom(self): req = webob.Request.blank('/v1.0/') @@ -341,36 +301,38 @@ class VersionsTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) self.assertEqual("application/atom+xml", res.content_type) - expected = """ - <feed xmlns="http://www.w3.org/2005/Atom"> - <title type="text">About This Version</title> - <updated>2011-01-21T11:33:21Z</updated> - <id>http://localhost/v1.0/</id> - <author> - <name>Rackspace</name> - <uri>http://www.rackspace.com/</uri> - </author> - <link href="http://localhost/v1.0/" rel="self"/> - <entry> - <id>http://localhost/v1.0/</id> - <title type="text">Version v1.0</title> - <updated>2011-01-21T11:33:21Z</updated> - <link href="http://localhost/v1.0/" - rel="self"/> - <link href="http://docs.rackspacecloud.com/servers/ - api/v1.0/cs-devguide-20110125.pdf" - rel="describedby" type="application/pdf"/> - <link href="http://docs.rackspacecloud.com/servers/ - api/v1.0/application.wadl" - rel="describedby" type="application/vnd.sun.wadl+xml"/> - <content type="text"> - Version v1.0 DEPRECATED (2011-01-21T11:33:21Z) - </content> - </entry> - </feed>""".replace(" ", "").replace("\n", "") - - actual = res.body.replace(" ", "").replace("\n", "") - self.assertEqual(expected, actual) + + f = feedparser.parse(res.body) + self.assertEqual(f.feed.title, 'About This Version') + self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z') + self.assertEqual(f.feed.id, 'http://localhost/v1.0/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://localhost/v1.0/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v1.0/') + self.assertEqual(entry.title, 'Version v1.0') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v1.0 DEPRECATED (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 3) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.0/') + self.assertEqual(entry.links[0]['rel'], 'self') + self.assertEqual(entry.links[1], { + 'href': 'http://docs.rackspacecloud.com/servers/api/v1.0/'\ + 'cs-devguide-20110125.pdf', + 'type': 'application/pdf', + 'rel': 'describedby'}) + self.assertEqual(entry.links[2], { + 'href': 'http://docs.rackspacecloud.com/servers/api/v1.0/'\ + 'application.wadl', + 'type': 'application/vnd.sun.wadl+xml', + 'rel': 'describedby'}) def test_get_version_1_1_detail_atom(self): req = webob.Request.blank('/v1.1/') @@ -378,36 +340,38 @@ class VersionsTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) self.assertEqual("application/atom+xml", res.content_type) - expected = """ - <feed xmlns="http://www.w3.org/2005/Atom"> - <title type="text">About This Version</title> - <updated>2011-01-21T11:33:21Z</updated> - <id>http://localhost/v1.1/</id> - <author> - <name>Rackspace</name> - <uri>http://www.rackspace.com/</uri> - </author> - <link href="http://localhost/v1.1/" rel="self"/> - <entry> - <id>http://localhost/v1.1/</id> - <title type="text">Version v1.1</title> - <updated>2011-01-21T11:33:21Z</updated> - <link href="http://localhost/v1.1/" - rel="self"/> - <link href="http://docs.rackspacecloud.com/servers/ - api/v1.1/cs-devguide-20110125.pdf" - rel="describedby" type="application/pdf"/> - <link href="http://docs.rackspacecloud.com/servers/ - api/v1.1/application.wadl" - rel="describedby" type="application/vnd.sun.wadl+xml"/> - <content type="text"> - Version v1.1 CURRENT (2011-01-21T11:33:21Z) - </content> - </entry> - </feed>""".replace(" ", "").replace("\n", "") - - actual = res.body.replace(" ", "").replace("\n", "") - self.assertEqual(expected, actual) + + f = feedparser.parse(res.body) + self.assertEqual(f.feed.title, 'About This Version') + self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z') + self.assertEqual(f.feed.id, 'http://localhost/v1.1/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://localhost/v1.1/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v1.1/') + self.assertEqual(entry.title, 'Version v1.1') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 3) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') + self.assertEqual(entry.links[0]['rel'], 'self') + self.assertEqual(entry.links[1], { + 'href': 'http://docs.rackspacecloud.com/servers/api/v1.1/'\ + 'cs-devguide-20110125.pdf', + 'type': 'application/pdf', + 'rel': 'describedby'}) + self.assertEqual(entry.links[2], { + 'href': 'http://docs.rackspacecloud.com/servers/api/v1.1/'\ + 'application.wadl', + 'type': 'application/vnd.sun.wadl+xml', + 'rel': 'describedby'}) def test_get_version_list_atom(self): req = webob.Request.blank('/') @@ -416,40 +380,37 @@ class VersionsTest(test.TestCase): self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/atom+xml") - expected = """ - <feed xmlns="http://www.w3.org/2005/Atom"> - <title type="text">Available API Versions</title> - <updated>2011-01-21T11:33:21Z</updated> - <id>http://localhost/</id> - <author> - <name>Rackspace</name> - <uri>http://www.rackspace.com/</uri> - </author> - <link href="http://localhost/" rel="self"/> - <entry> - <id>http://localhost/v1.1/</id> - <title type="text">Version v1.1</title> - <updated>2011-01-21T11:33:21Z</updated> - <link href="http://localhost/v1.1/" rel="self"/> - <content type="text"> - Version v1.1 CURRENT (2011-01-21T11:33:21Z) - </content> - </entry> - <entry> - <id>http://localhost/v1.0/</id> - <title type="text">Version v1.0</title> - <updated>2011-01-21T11:33:21Z</updated> - <link href="http://localhost/v1.0/" rel="self"/> - <content type="text"> - Version v1.0 DEPRECATED (2011-01-21T11:33:21Z) - </content> - </entry> - </feed> - """.replace(" ", "").replace("\n", "") - - actual = res.body.replace(" ", "").replace("\n", "") - - self.assertEqual(expected, actual) + f = feedparser.parse(res.body) + self.assertEqual(f.feed.title, 'Available API Versions') + self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z') + self.assertEqual(f.feed.id, 'http://localhost/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://localhost/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 2) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v1.0/') + self.assertEqual(entry.title, 'Version v1.0') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v1.0 DEPRECATED (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 1) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.0/') + self.assertEqual(entry.links[0]['rel'], 'self') + entry = f.entries[1] + self.assertEqual(entry.id, 'http://localhost/v1.1/') + self.assertEqual(entry.title, 'Version v1.1') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 1) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') + self.assertEqual(entry.links[0]['rel'], 'self') def test_multi_choice_image(self): req = webob.Request.blank('/images/1') @@ -511,28 +472,32 @@ class VersionsTest(test.TestCase): self.assertEqual(res.status_int, 300) self.assertEqual(res.content_type, "application/xml") - expected = """ - <choices xmlns="%s" xmlns:atom="%s"> - <version id="v1.1" status="CURRENT"> - <media-types> - <media-type base="application/xml" - type="application/vnd.openstack.compute-v1.1+xml"/> - <media-type base="application/json" - type="application/vnd.openstack.compute-v1.1+json"/> - </media-types> - <atom:link href="http://localhost/v1.1/images/1" rel="self"/> - </version> - <version id="v1.0" status="DEPRECATED"> - <media-types> - <media-type base="application/xml" - type="application/vnd.openstack.compute-v1.0+xml"/> - <media-type base="application/json" - type="application/vnd.openstack.compute-v1.0+json"/> - </media-types> - <atom:link href="http://localhost/v1.0/images/1" rel="self"/> - </version> - </choices>""".replace(" ", "").replace("\n", "") % (wsgi.XMLNS_V11, - wsgi.XMLNS_ATOM) + root = etree.XML(res.body) + self.assertTrue(root.xpath('/ns:choices', namespaces=NS)) + versions = root.xpath('ns:version', namespaces=NS) + self.assertEqual(len(versions), 2) + + version = versions[0] + self.assertEqual(version.get('id'), 'v1.1') + self.assertEqual(version.get('status'), 'CURRENT') + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common.compare_media_types(media_types, + VERSIONS['v1.1']['media-types'])) + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v1.1/images/1'}])) + + version = versions[1] + self.assertEqual(version.get('id'), 'v1.0') + self.assertEqual(version.get('status'), 'DEPRECATED') + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common.compare_media_types(media_types, + VERSIONS['v1.0']['media-types'])) + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v1.0/images/1'}])) def test_multi_choice_server_atom(self): """ @@ -665,22 +630,20 @@ class VersionsSerializerTests(test.TestCase): serializer = versions.VersionsXMLSerializer() response = serializer.index(versions_data) - root = xml.etree.ElementTree.XML(response) - self.assertEqual(root.tag.split('}')[1], "versions") - self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11) - version = list(root)[0] - self.assertEqual(version.tag.split('}')[1], "version") - self.assertEqual(version.get('id'), - versions_data['versions'][0]['id']) + root = etree.XML(response) + self.assertTrue(root.xpath('/ns:versions', namespaces=NS)) + version_elems = root.xpath('ns:version', namespaces=NS) + self.assertEqual(len(version_elems), 1) + version = version_elems[0] + self.assertEqual(version.get('id'), versions_data['versions'][0]['id']) self.assertEqual(version.get('status'), versions_data['versions'][0]['status']) - link = list(version)[0] - - self.assertEqual(link.tag.split('}')[1], "link") - self.assertEqual(link.tag.split('}')[0].strip('{'), wsgi.XMLNS_ATOM) - for key, val in versions_data['versions'][0]['links'][0].items(): - self.assertEqual(link.get(key), val) + (link,) = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(link, [{ + 'rel': 'self', + 'href': 'http://test/2.7.1', + 'type': 'application/atom+xml'}])) def test_versions_multi_xml_serializer(self): versions_data = { @@ -703,11 +666,9 @@ class VersionsSerializerTests(test.TestCase): serializer = versions.VersionsXMLSerializer() response = serializer.multi(versions_data) - root = xml.etree.ElementTree.XML(response) - self.assertEqual(root.tag.split('}')[1], "choices") - self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11) - version = list(root)[0] - self.assertEqual(version.tag.split('}')[1], "version") + root = etree.XML(response) + self.assertTrue(root.xpath('/ns:choices', namespaces=NS)) + (version,) = root.xpath('ns:version', namespaces=NS) self.assertEqual(version.get('id'), versions_data['choices'][0]['id']) self.assertEqual(version.get('status'), versions_data['choices'][0]['status']) @@ -716,19 +677,14 @@ class VersionsSerializerTests(test.TestCase): media_type_nodes = list(media_types) self.assertEqual(media_types.tag.split('}')[1], "media-types") - set_types = versions_data['choices'][0]['media-types'] - for i, type in enumerate(set_types): - node = media_type_nodes[i] - self.assertEqual(node.tag.split('}')[1], "media-type") - for key, val in set_types[i].items(): - self.assertEqual(node.get(key), val) - - link = list(version)[1] + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common.compare_media_types(media_types, + versions_data['choices'][0]['media-types'])) - self.assertEqual(link.tag.split('}')[1], "link") - self.assertEqual(link.tag.split('}')[0].strip('{'), wsgi.XMLNS_ATOM) - for key, val in versions_data['choices'][0]['links'][0].items(): - self.assertEqual(link.get(key), val) + (link,) = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(link, + versions_data['choices'][0]['links'])) def test_version_detail_xml_serializer(self): version_data = { @@ -770,7 +726,7 @@ class VersionsSerializerTests(test.TestCase): serializer = versions.VersionsXMLSerializer() response = serializer.show(version_data) - root = xml.etree.ElementTree.XML(response) + root = etree.XML(response) self.assertEqual(root.tag.split('}')[1], "version") self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11) @@ -811,59 +767,28 @@ class VersionsSerializerTests(test.TestCase): serializer = versions.VersionsAtomSerializer() response = serializer.index(versions_data) - - root = xml.etree.ElementTree.XML(response) - self.assertEqual(root.tag.split('}')[1], "feed") - self.assertEqual(root.tag.split('}')[0].strip('{'), - "http://www.w3.org/2005/Atom") - - children = list(root) - title = children[0] - updated = children[1] - id = children[2] - author = children[3] - link = children[4] - entry = children[5] - - self.assertEqual(title.tag.split('}')[1], 'title') - self.assertEqual(title.text, 'Available API Versions') - self.assertEqual(updated.tag.split('}')[1], 'updated') - self.assertEqual(updated.text, '2011-07-20T11:40:00Z') - self.assertEqual(id.tag.split('}')[1], 'id') - self.assertEqual(id.text, 'http://test/') - - self.assertEqual(author.tag.split('}')[1], 'author') - author_name = list(author)[0] - author_uri = list(author)[1] - self.assertEqual(author_name.tag.split('}')[1], 'name') - self.assertEqual(author_name.text, 'Rackspace') - self.assertEqual(author_uri.tag.split('}')[1], 'uri') - self.assertEqual(author_uri.text, 'http://www.rackspace.com/') - - self.assertEqual(link.get('href'), 'http://test/') - self.assertEqual(link.get('rel'), 'self') - - self.assertEqual(entry.tag.split('}')[1], 'entry') - entry_children = list(entry) - entry_id = entry_children[0] - entry_title = entry_children[1] - entry_updated = entry_children[2] - entry_link = entry_children[3] - entry_content = entry_children[4] - self.assertEqual(entry_id.tag.split('}')[1], "id") - self.assertEqual(entry_id.text, "http://test/2.9.8") - self.assertEqual(entry_title.tag.split('}')[1], "title") - self.assertEqual(entry_title.get('type'), "text") - self.assertEqual(entry_title.text, "Version 2.9.8") - self.assertEqual(entry_updated.tag.split('}')[1], "updated") - self.assertEqual(entry_updated.text, "2011-07-20T11:40:00Z") - self.assertEqual(entry_link.tag.split('}')[1], "link") - self.assertEqual(entry_link.get('href'), "http://test/2.9.8") - self.assertEqual(entry_link.get('rel'), "self") - self.assertEqual(entry_content.tag.split('}')[1], "content") - self.assertEqual(entry_content.get('type'), "text") - self.assertEqual(entry_content.text, - "Version 2.9.8 CURRENT (2011-07-20T11:40:00Z)") + f = feedparser.parse(response) + + self.assertEqual(f.feed.title, 'Available API Versions') + self.assertEqual(f.feed.updated, '2011-07-20T11:40:00Z') + self.assertEqual(f.feed.id, 'http://test/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://test/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://test/2.9.8') + self.assertEqual(entry.title, 'Version 2.9.8') + self.assertEqual(entry.updated, '2011-07-20T11:40:00Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version 2.9.8 CURRENT (2011-07-20T11:40:00Z)') + self.assertEqual(len(entry.links), 1) + self.assertEqual(entry.links[0]['href'], 'http://test/2.9.8') + self.assertEqual(entry.links[0]['rel'], 'self') def test_version_detail_atom_serializer(self): versions_data = { @@ -904,63 +829,36 @@ class VersionsSerializerTests(test.TestCase): serializer = versions.VersionsAtomSerializer() response = serializer.show(versions_data) - - root = xml.etree.ElementTree.XML(response) - self.assertEqual(root.tag.split('}')[1], "feed") - self.assertEqual(root.tag.split('}')[0].strip('{'), - "http://www.w3.org/2005/Atom") - - children = list(root) - title = children[0] - updated = children[1] - id = children[2] - author = children[3] - link = children[4] - entry = children[5] - - self.assertEqual(root.tag.split('}')[1], 'feed') - self.assertEqual(title.tag.split('}')[1], 'title') - self.assertEqual(title.text, 'About This Version') - self.assertEqual(updated.tag.split('}')[1], 'updated') - self.assertEqual(updated.text, '2011-01-21T11:33:21Z') - self.assertEqual(id.tag.split('}')[1], 'id') - self.assertEqual(id.text, 'http://localhost/v1.1/') - - self.assertEqual(author.tag.split('}')[1], 'author') - author_name = list(author)[0] - author_uri = list(author)[1] - self.assertEqual(author_name.tag.split('}')[1], 'name') - self.assertEqual(author_name.text, 'Rackspace') - self.assertEqual(author_uri.tag.split('}')[1], 'uri') - self.assertEqual(author_uri.text, 'http://www.rackspace.com/') - - self.assertEqual(link.get('href'), - 'http://localhost/v1.1/') - self.assertEqual(link.get('rel'), 'self') - - self.assertEqual(entry.tag.split('}')[1], 'entry') - entry_children = list(entry) - entry_id = entry_children[0] - entry_title = entry_children[1] - entry_updated = entry_children[2] - entry_links = (entry_children[3], entry_children[4], entry_children[5]) - entry_content = entry_children[6] - - self.assertEqual(entry_id.tag.split('}')[1], "id") - self.assertEqual(entry_id.text, - "http://localhost/v1.1/") - self.assertEqual(entry_title.tag.split('}')[1], "title") - self.assertEqual(entry_title.get('type'), "text") - self.assertEqual(entry_title.text, "Version v1.1") - self.assertEqual(entry_updated.tag.split('}')[1], "updated") - self.assertEqual(entry_updated.text, "2011-01-21T11:33:21Z") - - for i, link in enumerate(versions_data["version"]["links"]): - self.assertEqual(entry_links[i].tag.split('}')[1], "link") - for key, val in versions_data["version"]["links"][i].items(): - self.assertEqual(entry_links[i].get(key), val) - - self.assertEqual(entry_content.tag.split('}')[1], "content") - self.assertEqual(entry_content.get('type'), "text") - self.assertEqual(entry_content.text, - "Version v1.1 CURRENT (2011-01-21T11:33:21Z)") + f = feedparser.parse(response) + + self.assertEqual(f.feed.title, 'About This Version') + self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z') + self.assertEqual(f.feed.id, 'http://localhost/v1.1/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://localhost/v1.1/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v1.1/') + self.assertEqual(entry.title, 'Version v1.1') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 3) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') + self.assertEqual(entry.links[0]['rel'], 'self') + self.assertEqual(entry.links[1], { + 'rel': 'describedby', + 'type': 'application/pdf', + 'href': 'http://docs.rackspacecloud.com/' + 'servers/api/v1.1/cs-devguide-20110125.pdf'}) + self.assertEqual(entry.links[2], { + 'rel': 'describedby', + 'type': 'application/vnd.sun.wadl+xml', + 'href': 'http://docs.rackspacecloud.com/' + 'servers/api/v1.1/application.wadl', + }) diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py index 6dea78d17..74b9ce853 100644 --- a/nova/tests/api/openstack/test_wsgi.py +++ b/nova/tests/api/openstack/test_wsgi.py @@ -27,17 +27,17 @@ class RequestTest(test.TestCase): result = request.get_content_type() self.assertEqual(result, "application/json") - def test_content_type_from_accept_xml(self): - request = wsgi.Request.blank('/tests/123') - request.headers["Accept"] = "application/xml" - result = request.best_match_content_type() - self.assertEqual(result, "application/xml") - - request = wsgi.Request.blank('/tests/123') - request.headers["Accept"] = "application/json" - result = request.best_match_content_type() - self.assertEqual(result, "application/json") - + def test_content_type_from_accept(self): + for content_type in ('application/xml', + 'application/vnd.openstack.compute+xml', + 'application/json', + 'application/vnd.openstack.compute+json'): + request = wsgi.Request.blank('/tests/123') + request.headers["Accept"] = content_type + result = request.best_match_content_type() + self.assertEqual(result, content_type) + + def test_content_type_from_accept_best(self): request = wsgi.Request.blank('/tests/123') request.headers["Accept"] = "application/xml, application/json" result = request.best_match_content_type() @@ -231,7 +231,7 @@ class ResponseSerializerTest(test.TestCase): self.body_serializers = { 'application/json': JSONSerializer(), - 'application/XML': XMLSerializer(), + 'application/xml': XMLSerializer(), } self.serializer = wsgi.ResponseSerializer(self.body_serializers, @@ -250,15 +250,24 @@ class ResponseSerializerTest(test.TestCase): self.serializer.get_body_serializer, 'application/unknown') - def test_serialize_response(self): - response = self.serializer.serialize({}, 'application/json') - self.assertEqual(response.headers['Content-Type'], 'application/json') - self.assertEqual(response.body, 'pew_json') - self.assertEqual(response.status_int, 404) + def test_serialize_response_json(self): + for content_type in ('application/json', + 'application/vnd.openstack.compute+json'): + response = self.serializer.serialize({}, content_type) + self.assertEqual(response.headers['Content-Type'], content_type) + self.assertEqual(response.body, 'pew_json') + self.assertEqual(response.status_int, 404) + + def test_serialize_response_xml(self): + for content_type in ('application/xml', + 'application/vnd.openstack.compute+xml'): + response = self.serializer.serialize({}, 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') - print response self.assertEqual(response.headers['Content-Type'], 'application/json') self.assertEqual(response.body, '') self.assertEqual(response.status_int, 404) @@ -281,7 +290,7 @@ class RequestDeserializerTest(test.TestCase): self.body_deserializers = { 'application/json': JSONDeserializer(), - 'application/XML': XMLDeserializer(), + 'application/xml': XMLDeserializer(), } self.deserializer = wsgi.RequestDeserializer(self.body_deserializers) @@ -290,8 +299,9 @@ class RequestDeserializerTest(test.TestCase): pass def test_get_deserializer(self): - expected = self.deserializer.get_body_deserializer('application/json') - self.assertEqual(expected, self.body_deserializers['application/json']) + ctype = 'application/json' + expected = self.deserializer.get_body_deserializer(ctype) + self.assertEqual(expected, self.body_deserializers[ctype]) def test_get_deserializer_unknown_content_type(self): self.assertRaises(exception.InvalidContentType, @@ -299,10 +309,11 @@ class RequestDeserializerTest(test.TestCase): 'application/unknown') def test_get_expected_content_type(self): + ctype = 'application/json' request = wsgi.Request.blank('/') - request.headers['Accept'] = 'application/json' + request.headers['Accept'] = ctype self.assertEqual(self.deserializer.get_expected_content_type(request), - 'application/json') + ctype) def test_get_action_args(self): env = { diff --git a/nova/tests/db/fakes.py b/nova/tests/db/fakes.py index 19028a451..cdbfba63a 100644 --- a/nova/tests/db/fakes.py +++ b/nova/tests/db/fakes.py @@ -125,10 +125,11 @@ def stub_out_db_network_api(stubs): if ips[0]['fixed_ip']: fixed_ip_address = ips[0]['fixed_ip']['address'] ips[0]['fixed_ip'] = None + ips[0]['host'] = None return fixed_ip_address def fake_floating_ip_fixed_ip_associate(context, floating_address, - fixed_address): + fixed_address, host): float = filter(lambda i: i['address'] == floating_address, floating_ips) fixed = filter(lambda i: i['address'] == fixed_address, @@ -136,6 +137,7 @@ def stub_out_db_network_api(stubs): if float and fixed: float[0]['fixed_ip'] = fixed[0] float[0]['fixed_ip_id'] = fixed[0]['id'] + float[0]['host'] = host def fake_floating_ip_get_all_by_host(context, host): # TODO(jkoelker): Once we get the patches that remove host from diff --git a/nova/tests/fake_network.py b/nova/tests/fake_network.py new file mode 100644 index 000000000..142206755 --- /dev/null +++ b/nova/tests/fake_network.py @@ -0,0 +1,194 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Rackspace +# 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 nova import db +from nova import flags +from nova import test +from nova.network import manager as network_manager + + +HOST = "testhost" +FLAGS = flags.FLAGS + + +class FakeIptablesFirewallDriver(object): + def __init__(self, **kwargs): + pass + + def setattr(self, key, val): + self.__setattr__(key, val) + + def apply_instance_filter(self, instance, network_info): + pass + + +class FakeVIFDriver(object): + + def __init__(self, **kwargs): + pass + + def setattr(self, key, val): + self.__setattr__(key, val) + + def plug(self, instance, network, mapping): + return { + 'id': 'fake', + 'bridge_name': 'fake', + 'mac_address': 'fake', + 'ip_address': 'fake', + 'dhcp_server': 'fake', + 'extra_params': 'fake', + } + + +class FakeModel(dict): + """Represent a model from the db""" + def __init__(self, *args, **kwargs): + self.update(kwargs) + + def __getattr__(self, name): + return self[name] + + +flavor = {'id': 0, + 'name': 'fake_flavor', + 'memory_mb': 2048, + 'vcpus': 2, + 'local_gb': 10, + 'flavor_id': 0, + 'swap': 0, + 'rxtx_quota': 0, + 'rxtx_cap': 3} + + +def fake_network(network_id, ipv6=None): + if ipv6 is None: + ipv6 = FLAGS.use_ipv6 + fake_network = {'id': network_id, + 'label': 'test%d' % network_id, + 'injected': False, + 'multi_host': False, + 'cidr': '192.168.%d.0/24' % network_id, + 'cidr_v6': None, + 'netmask': '255.255.255.0', + 'netmask_v6': None, + 'bridge': 'fake_br%d' % network_id, + 'bridge_interface': 'fake_eth%d' % network_id, + 'gateway': '192.168.%d.1' % network_id, + 'gateway_v6': None, + 'broadcast': '192.168.%d.255' % network_id, + 'dns1': '192.168.%d.3' % network_id, + 'dns2': '192.168.%d.4' % network_id, + 'vlan': None, + 'host': None, + 'project_id': 'fake_project', + 'vpn_public_address': '192.168.%d.2' % network_id} + if ipv6: + fake_network['cidr_v6'] = '2001:db8:0:%x::/64' % network_id + fake_network['gateway_v6'] = '2001:db8:0:%x::1' % network_id + fake_network['netmask_v6'] = '64' + + return fake_network + + +def vifs(n): + for x in xrange(n): + yield {'id': x, + 'address': 'DE:AD:BE:EF:00:%02x' % x, + 'uuid': '00000000-0000-0000-0000-00000000000000%02d' % x, + 'network_id': x, + 'network': FakeModel(**fake_network(x)), + 'instance_id': 0} + + +def floating_ip_ids(): + for i in xrange(99): + yield i + + +def fixed_ip_ids(): + for i in xrange(99): + yield i + + +floating_ip_id = floating_ip_ids() +fixed_ip_id = fixed_ip_ids() + + +def next_fixed_ip(network_id, num_floating_ips=0): + next_id = fixed_ip_id.next() + f_ips = [FakeModel(**next_floating_ip(next_id)) + for i in xrange(num_floating_ips)] + return {'id': next_id, + 'network_id': network_id, + 'address': '192.168.%d.1%02d' % (network_id, next_id), + 'instance_id': 0, + 'allocated': False, + # and since network_id and vif_id happen to be equivalent + 'virtual_interface_id': network_id, + 'floating_ips': f_ips} + + +def next_floating_ip(fixed_ip_id): + next_id = floating_ip_id.next() + return {'id': next_id, + 'address': '10.10.10.1%02d' % next_id, + 'fixed_ip_id': fixed_ip_id, + 'project_id': None, + 'auto_assigned': False} + + +def ipv4_like(ip, match_string): + ip = ip.split('.') + match_octets = match_string.split('.') + + for i, octet in enumerate(match_octets): + if octet == '*': + continue + if octet != ip[i]: + return False + return True + + +def fake_get_instance_nw_info(stubs, num_networks=1, ips_per_vif=2, + floating_ips_per_fixed_ip=0): + # stubs is the self.stubs from the test + # ips_per_vif is the number of ips each vif will have + # num_floating_ips is number of float ips for each fixed ip + network = network_manager.FlatManager(host=HOST) + network.db = db + + # reset the fixed and floating ip generators + global floating_ip_id, fixed_ip_id + floating_ip_id = floating_ip_ids() + fixed_ip_id = fixed_ip_ids() + + def fixed_ips_fake(*args, **kwargs): + return [next_fixed_ip(i, floating_ips_per_fixed_ip) + for i in xrange(num_networks) for j in xrange(ips_per_vif)] + + def virtual_interfaces_fake(*args, **kwargs): + return [vif for vif in vifs(num_networks)] + + def instance_type_fake(*args, **kwargs): + return flavor + + stubs.Set(db, 'fixed_ip_get_by_instance', fixed_ips_fake) + stubs.Set(db, 'virtual_interface_get_by_instance', virtual_interfaces_fake) + stubs.Set(db, 'instance_type_get', instance_type_fake) + + return network.get_instance_nw_info(None, 0, 0, None) diff --git a/nova/tests/glance/stubs.py b/nova/tests/glance/stubs.py index f2a19f22d..1567393e3 100644 --- a/nova/tests/glance/stubs.py +++ b/nova/tests/glance/stubs.py @@ -16,14 +16,15 @@ import StringIO -import nova.image +from nova import exception +from nova.image import glance def stubout_glance_client(stubs): - def fake_get_glance_client(image_href): + def fake_get_glance_client(context, image_href): image_id = int(str(image_href).split('/')[-1]) return (FakeGlance('foo'), image_id) - stubs.Set(nova.image, 'get_glance_client', fake_get_glance_client) + stubs.Set(glance, 'get_glance_client', fake_get_glance_client) class FakeGlance(object): @@ -78,3 +79,70 @@ class FakeGlance(object): def get_image(self, image_id): image = self.IMAGE_FIXTURES[int(image_id)] return image['image_meta'], image['image_data'] + + +NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" + + +class StubGlanceClient(object): + + def __init__(self, images=None): + self.images = [] + _images = images or [] + map(lambda image: self.add_image(image, None), _images) + + def set_auth_token(self, auth_tok): + pass + + def get_image_meta(self, image_id): + for image in self.images: + if image['id'] == str(image_id): + return image + raise exception.ImageNotFound(image_id=image_id) + + #TODO(bcwaldon): implement filters + def get_images_detailed(self, filters=None, marker=None, limit=3): + if marker is None: + index = 0 + else: + for index, image in enumerate(self.images): + if image['id'] == str(marker): + index += 1 + break + + return self.images[index:index + limit] + + def get_image(self, image_id): + return self.get_image_meta(image_id), [] + + def add_image(self, metadata, data): + metadata['created_at'] = NOW_GLANCE_FORMAT + metadata['updated_at'] = NOW_GLANCE_FORMAT + + self.images.append(metadata) + + try: + image_id = str(metadata['id']) + except KeyError: + # auto-generate an id if one wasn't provided + image_id = str(len(self.images)) + + self.images[-1]['id'] = image_id + + return self.images[-1] + + def update_image(self, image_id, metadata, data): + for i, image in enumerate(self.images): + if image['id'] == str(image_id): + if 'id' in metadata: + metadata['id'] = str(metadata['id']) + self.images[i].update(metadata) + return self.images[i] + raise exception.ImageNotFound(image_id=image_id) + + def delete_image(self, image_id): + for i, image in enumerate(self.images): + if image['id'] == image_id: + del self.images[i] + return + raise exception.ImageNotFound(image_id=image_id) diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index b1ebd8436..290c9a04a 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -17,47 +17,14 @@ import datetime -import unittest +import stubout +from nova.tests.api.openstack import fakes from nova import context from nova import exception -from nova import test from nova.image import glance - - -class StubGlanceClient(object): - - def __init__(self, images, add_response=None, update_response=None): - self.images = images - self.add_response = add_response - self.update_response = update_response - - def set_auth_token(self, auth_tok): - pass - - def get_image_meta(self, image_id): - return self.images[image_id] - - def get_images_detailed(self, filters=None, marker=None, limit=None): - images = self.images.values() - if marker is None: - index = 0 - else: - for index, image in enumerate(images): - if image['id'] == marker: - index += 1 - break - # default to a page size of 3 to ensure we flex the pagination code - return images[index:index + 3] - - def get_image(self, image_id): - return self.images[image_id], [] - - def add_image(self, metadata, data): - return self.add_response - - def update_image(self, image_id, metadata, data): - return self.update_response +from nova import test +from nova.tests.glance import stubs as glance_stubs class NullWriter(object): @@ -67,218 +34,7 @@ class NullWriter(object): pass -class BaseGlanceTest(unittest.TestCase): - NOW_GLANCE_OLD_FORMAT = "2010-10-11T10:30:22" - NOW_GLANCE_FORMAT = "2010-10-11T10:30:22.000000" - NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22) - - def setUp(self): - self.client = StubGlanceClient(None) - self.service = glance.GlanceImageService(client=self.client) - self.context = context.RequestContext(None, None) - - def assertDateTimesFilled(self, image_meta): - self.assertEqual(image_meta['created_at'], self.NOW_DATETIME) - self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME) - self.assertEqual(image_meta['deleted_at'], self.NOW_DATETIME) - - def assertDateTimesEmpty(self, image_meta): - self.assertEqual(image_meta['updated_at'], None) - self.assertEqual(image_meta['deleted_at'], None) - - def assertDateTimesBlank(self, image_meta): - self.assertEqual(image_meta['updated_at'], '') - self.assertEqual(image_meta['deleted_at'], '') - - -class TestGlanceImageServiceProperties(BaseGlanceTest): - def test_show_passes_through_to_client(self): - """Ensure attributes which aren't BASE_IMAGE_ATTRS are stored in the - properties dict - """ - fixtures = {'image1': {'id': '1', 'name': 'image1', 'is_public': True, - 'foo': 'bar', - 'properties': {'prop1': 'propvalue1'}}} - self.client.images = fixtures - image_meta = self.service.show(self.context, 'image1') - - expected = {'id': '1', 'name': 'image1', 'is_public': True, - 'properties': {'prop1': 'propvalue1', 'foo': 'bar'}} - self.assertEqual(image_meta, expected) - - def test_show_raises_when_no_authtoken_in_the_context(self): - fixtures = {'image1': {'name': 'image1', 'is_public': False, - 'foo': 'bar', - 'properties': {'prop1': 'propvalue1'}}} - self.client.images = fixtures - self.context.auth_token = False - - expected = {'name': 'image1', 'is_public': True, - 'properties': {'prop1': 'propvalue1', 'foo': 'bar'}} - self.assertRaises(exception.ImageNotFound, - self.service.show, self.context, 'image1') - - def test_show_passes_through_to_client_with_authtoken_in_context(self): - fixtures = {'image1': {'name': 'image1', 'is_public': False, - 'foo': 'bar', - 'properties': {'prop1': 'propvalue1'}}} - self.client.images = fixtures - self.context.auth_token = True - - expected = {'name': 'image1', 'is_public': False, - 'properties': {'prop1': 'propvalue1', 'foo': 'bar'}} - - image_meta = self.service.show(self.context, 'image1') - self.assertEqual(image_meta, expected) - - def test_detail_passes_through_to_client(self): - fixtures = {'image1': {'id': '1', 'name': 'image1', 'is_public': True, - 'foo': 'bar', - 'properties': {'prop1': 'propvalue1'}}} - self.client.images = fixtures - image_meta = self.service.detail(self.context) - expected = [{'id': '1', 'name': 'image1', 'is_public': True, - 'properties': {'prop1': 'propvalue1', 'foo': 'bar'}}] - self.assertEqual(image_meta, expected) - - -class TestGetterDateTimeNoneTests(BaseGlanceTest): - - def test_show_handles_none_datetimes(self): - self.client.images = self._make_none_datetime_fixtures() - image_meta = self.service.show(self.context, 'image1') - self.assertDateTimesEmpty(image_meta) - - def test_show_handles_blank_datetimes(self): - self.client.images = self._make_blank_datetime_fixtures() - image_meta = self.service.show(self.context, 'image1') - self.assertDateTimesBlank(image_meta) - - def test_detail_handles_none_datetimes(self): - self.client.images = self._make_none_datetime_fixtures() - image_meta = self.service.detail(self.context)[0] - self.assertDateTimesEmpty(image_meta) - - def test_detail_handles_blank_datetimes(self): - self.client.images = self._make_blank_datetime_fixtures() - image_meta = self.service.detail(self.context)[0] - self.assertDateTimesBlank(image_meta) - - def test_get_handles_none_datetimes(self): - self.client.images = self._make_none_datetime_fixtures() - writer = NullWriter() - image_meta = self.service.get(self.context, 'image1', writer) - self.assertDateTimesEmpty(image_meta) - - def test_get_handles_blank_datetimes(self): - self.client.images = self._make_blank_datetime_fixtures() - writer = NullWriter() - image_meta = self.service.get(self.context, 'image1', writer) - self.assertDateTimesBlank(image_meta) - - def test_show_makes_datetimes(self): - self.client.images = self._make_datetime_fixtures() - image_meta = self.service.show(self.context, 'image1') - self.assertDateTimesFilled(image_meta) - image_meta = self.service.show(self.context, 'image2') - self.assertDateTimesFilled(image_meta) - - def test_detail_makes_datetimes(self): - self.client.images = self._make_datetime_fixtures() - image_meta = self.service.detail(self.context)[0] - self.assertDateTimesFilled(image_meta) - image_meta = self.service.detail(self.context)[1] - self.assertDateTimesFilled(image_meta) - - def test_get_makes_datetimes(self): - self.client.images = self._make_datetime_fixtures() - writer = NullWriter() - image_meta = self.service.get(self.context, 'image1', writer) - self.assertDateTimesFilled(image_meta) - image_meta = self.service.get(self.context, 'image2', writer) - self.assertDateTimesFilled(image_meta) - - def _make_datetime_fixtures(self): - fixtures = { - 'image1': { - 'id': '1', - 'name': 'image1', - 'is_public': True, - 'created_at': self.NOW_GLANCE_FORMAT, - 'updated_at': self.NOW_GLANCE_FORMAT, - 'deleted_at': self.NOW_GLANCE_FORMAT, - }, - 'image2': { - 'id': '2', - 'name': 'image2', - 'is_public': True, - 'created_at': self.NOW_GLANCE_OLD_FORMAT, - 'updated_at': self.NOW_GLANCE_OLD_FORMAT, - 'deleted_at': self.NOW_GLANCE_OLD_FORMAT, - }, - } - return fixtures - - def _make_none_datetime_fixtures(self): - fixtures = {'image1': {'id': '1', - 'name': 'image1', - 'is_public': True, - 'updated_at': None, - 'deleted_at': None}} - return fixtures - - def _make_blank_datetime_fixtures(self): - fixtures = {'image1': {'id': '1', - 'name': 'image1', - 'is_public': True, - 'updated_at': '', - 'deleted_at': ''}} - return fixtures - - -class TestMutatorDateTimeTests(BaseGlanceTest): - """Tests create(), update()""" - - def test_create_handles_datetimes(self): - self.client.add_response = self._make_datetime_fixture() - image_meta = self.service.create(self.context, {}) - self.assertDateTimesFilled(image_meta) - - def test_create_handles_none_datetimes(self): - self.client.add_response = self._make_none_datetime_fixture() - dummy_meta = {} - image_meta = self.service.create(self.context, dummy_meta) - self.assertDateTimesEmpty(image_meta) - - def test_update_handles_datetimes(self): - self.client.images = {'image1': self._make_datetime_fixture()} - self.client.update_response = self._make_datetime_fixture() - dummy_meta = {} - image_meta = self.service.update(self.context, 'image1', dummy_meta) - self.assertDateTimesFilled(image_meta) - - def test_update_handles_none_datetimes(self): - self.client.images = {'image1': self._make_datetime_fixture()} - self.client.update_response = self._make_none_datetime_fixture() - dummy_meta = {} - image_meta = self.service.update(self.context, 'image1', dummy_meta) - self.assertDateTimesEmpty(image_meta) - - def _make_datetime_fixture(self): - fixture = {'id': 'image1', 'name': 'image1', 'is_public': True, - 'created_at': self.NOW_GLANCE_FORMAT, - 'updated_at': self.NOW_GLANCE_FORMAT, - 'deleted_at': self.NOW_GLANCE_FORMAT} - return fixture - - def _make_none_datetime_fixture(self): - fixture = {'id': 'image1', 'name': 'image1', 'is_public': True, - 'updated_at': None, - 'deleted_at': None} - return fixture - - -class TestGlanceSerializer(unittest.TestCase): +class TestGlanceSerializer(test.TestCase): def test_serialize(self): metadata = {'name': 'image1', 'is_public': True, @@ -312,3 +68,386 @@ class TestGlanceSerializer(unittest.TestCase): converted = glance._convert_to_string(metadata) self.assertEqual(converted, converted_expected) self.assertEqual(glance._convert_from_string(converted), metadata) + + +class TestGlanceImageService(test.TestCase): + """ + Tests the Glance image service. + + At a high level, the translations involved are: + + 1. Glance -> ImageService - This is needed so we can support + multple ImageServices (Glance, Local, etc) + + 2. ImageService -> API - This is needed so we can support multple + APIs (OpenStack, EC2) + + """ + NOW_GLANCE_OLD_FORMAT = "2010-10-11T10:30:22" + NOW_GLANCE_FORMAT = "2010-10-11T10:30:22.000000" + NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22) + + def setUp(self): + super(TestGlanceImageService, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.stub_out_compute_api_snapshot(self.stubs) + client = glance_stubs.StubGlanceClient() + self.service = glance.GlanceImageService(client=client) + self.context = context.RequestContext('fake', 'fake', auth_token=True) + self.service.delete_all() + + def tearDown(self): + self.stubs.UnsetAll() + super(TestGlanceImageService, self).tearDown() + + @staticmethod + def _make_fixture(**kwargs): + fixture = {'name': None, + 'properties': {}, + 'status': None, + 'is_public': None} + fixture.update(kwargs) + return fixture + + def _make_datetime_fixture(self): + return self._make_fixture(created_at=self.NOW_GLANCE_FORMAT, + updated_at=self.NOW_GLANCE_FORMAT, + deleted_at=self.NOW_GLANCE_FORMAT) + + def test_create_with_instance_id(self): + """Ensure instance_id is persisted as an image-property""" + fixture = {'name': 'test image', + 'is_public': False, + 'properties': {'instance_id': '42', 'user_id': 'fake'}} + + image_id = self.service.create(self.context, fixture)['id'] + image_meta = self.service.show(self.context, image_id) + expected = { + 'id': image_id, + 'name': 'test image', + 'is_public': False, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None, + 'status': None, + 'properties': {'instance_id': '42', 'user_id': 'fake'}, + } + self.assertDictMatch(image_meta, expected) + + image_metas = self.service.detail(self.context) + self.assertDictMatch(image_metas[0], expected) + + def test_create_without_instance_id(self): + """ + Ensure we can create an image without having to specify an + instance_id. Public images are an example of an image not tied to an + instance. + """ + fixture = {'name': 'test image', 'is_public': False} + image_id = self.service.create(self.context, fixture)['id'] + + expected = { + 'id': image_id, + 'name': 'test image', + 'is_public': False, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None, + 'status': None, + 'properties': {}, + } + actual = self.service.show(self.context, image_id) + self.assertDictMatch(actual, expected) + + def test_create(self): + fixture = self._make_fixture(name='test image') + num_images = len(self.service.index(self.context)) + image_id = self.service.create(self.context, fixture)['id'] + + self.assertNotEquals(None, image_id) + self.assertEquals(num_images + 1, + len(self.service.index(self.context))) + + def test_create_and_show_non_existing_image(self): + fixture = self._make_fixture(name='test image') + image_id = self.service.create(self.context, fixture)['id'] + + self.assertNotEquals(None, image_id) + self.assertRaises(exception.NotFound, + self.service.show, + self.context, + 'bad image id') + + def test_create_and_show_non_existing_image_by_name(self): + fixture = self._make_fixture(name='test image') + image_id = self.service.create(self.context, fixture)['id'] + + self.assertNotEquals(None, image_id) + self.assertRaises(exception.ImageNotFound, + self.service.show_by_name, + self.context, + 'bad image id') + + def test_index(self): + fixture = self._make_fixture(name='test image') + image_id = self.service.create(self.context, fixture)['id'] + image_metas = self.service.index(self.context) + expected = [{'id': image_id, 'name': 'test image'}] + self.assertDictListMatch(image_metas, expected) + + def test_index_default_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.index(self.context) + i = 0 + for meta in image_metas: + expected = {'id': 'DONTCARE', + 'name': 'TestImage %d' % (i)} + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_index_marker(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.index(self.context, marker=ids[1]) + self.assertEquals(len(image_metas), 8) + i = 2 + for meta in image_metas: + expected = {'id': 'DONTCARE', + 'name': 'TestImage %d' % (i)} + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_index_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.index(self.context, limit=5) + self.assertEquals(len(image_metas), 5) + + def test_index_marker_and_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.index(self.context, marker=ids[3], limit=1) + self.assertEquals(len(image_metas), 1) + i = 4 + for meta in image_metas: + expected = {'id': ids[i], + 'name': 'TestImage %d' % (i)} + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_detail_marker(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.detail(self.context, marker=ids[1]) + self.assertEquals(len(image_metas), 8) + i = 2 + for meta in image_metas: + expected = { + 'id': ids[i], + 'status': None, + 'is_public': None, + 'name': 'TestImage %d' % (i), + 'properties': {}, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None + } + + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_detail_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.detail(self.context, limit=5) + self.assertEquals(len(image_metas), 5) + + def test_detail_marker_and_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.detail(self.context, marker=ids[3], limit=5) + self.assertEquals(len(image_metas), 5) + i = 4 + for meta in image_metas: + expected = { + 'id': ids[i], + 'status': None, + 'is_public': None, + 'name': 'TestImage %d' % (i), + 'properties': {}, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None + } + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_update(self): + fixture = self._make_fixture(name='test image') + image_id = self.service.create(self.context, fixture)['id'] + fixture['name'] = 'new image name' + self.service.update(self.context, image_id, fixture) + + new_image_data = self.service.show(self.context, image_id) + self.assertEquals('new image name', new_image_data['name']) + + def test_delete(self): + fixture1 = self._make_fixture(name='test image 1') + fixture2 = self._make_fixture(name='test image 2') + fixtures = [fixture1, fixture2] + + num_images = len(self.service.index(self.context)) + self.assertEquals(0, num_images, str(self.service.index(self.context))) + + ids = [] + for fixture in fixtures: + new_id = self.service.create(self.context, fixture)['id'] + ids.append(new_id) + + num_images = len(self.service.index(self.context)) + self.assertEquals(2, num_images, str(self.service.index(self.context))) + + self.service.delete(self.context, ids[0]) + + num_images = len(self.service.index(self.context)) + self.assertEquals(1, num_images) + + def test_show_passes_through_to_client(self): + fixture = self._make_fixture(name='image1', is_public=True) + image_id = self.service.create(self.context, fixture)['id'] + + image_meta = self.service.show(self.context, image_id) + expected = { + 'id': image_id, + 'name': 'image1', + 'is_public': True, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None, + 'status': None, + 'properties': {}, + } + self.assertEqual(image_meta, expected) + + def test_show_raises_when_no_authtoken_in_the_context(self): + fixture = self._make_fixture(name='image1', + is_public=False, + properties={'one': 'two'}) + image_id = self.service.create(self.context, fixture)['id'] + self.context.auth_token = False + self.assertRaises(exception.ImageNotFound, + self.service.show, + self.context, + image_id) + + def test_detail_passes_through_to_client(self): + fixture = self._make_fixture(name='image10', is_public=True) + image_id = self.service.create(self.context, fixture)['id'] + image_metas = self.service.detail(self.context) + expected = [ + { + 'id': image_id, + 'name': 'image10', + 'is_public': True, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None, + 'status': None, + 'properties': {}, + }, + ] + self.assertEqual(image_metas, expected) + + def test_show_makes_datetimes(self): + fixture = self._make_datetime_fixture() + image_id = self.service.create(self.context, fixture)['id'] + image_meta = self.service.show(self.context, image_id) + self.assertEqual(image_meta['created_at'], self.NOW_DATETIME) + self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME) + + def test_detail_makes_datetimes(self): + fixture = self._make_datetime_fixture() + self.service.create(self.context, fixture) + image_meta = self.service.detail(self.context)[0] + self.assertEqual(image_meta['created_at'], self.NOW_DATETIME) + self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME) + + def test_get_makes_datetimes(self): + fixture = self._make_datetime_fixture() + image_id = self.service.create(self.context, fixture)['id'] + writer = NullWriter() + image_meta = self.service.get(self.context, image_id, writer) + self.assertEqual(image_meta['created_at'], self.NOW_DATETIME) + self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME) diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index 343190427..49de9c854 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -64,7 +64,7 @@ class _IntegratedTestBase(test.TestCase): self.flags(**f) self.flags(verbose=True) - def fake_get_image_service(image_href): + def fake_get_image_service(context, image_href): image_id = int(str(image_href).split('/')[-1]) return (nova.image.fake.FakeImageService(), image_id) self.stubs.Set(nova.image, 'get_image_service', fake_get_image_service) diff --git a/nova/tests/integrated/test_xml.py b/nova/tests/integrated/test_xml.py index 74baaacc2..cf013da1d 100644 --- a/nova/tests/integrated/test_xml.py +++ b/nova/tests/integrated/test_xml.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +from lxml import etree + from nova.log import logging from nova.tests.integrated import integrated_helpers from nova.api.openstack import common @@ -34,9 +36,8 @@ class XmlTests(integrated_helpers._IntegratedTestBase): response = self.api.api_request('/limits', headers=headers) data = response.read() LOG.debug("data: %s" % data) - - prefix = '<limits xmlns="%s"' % common.XML_NS_V11 - self.assertTrue(data.startswith(prefix)) + root = etree.XML(data) + self.assertEqual(root.nsmap.get(None), common.XML_NS_V11) def test_namespace_servers(self): """/servers should have v1.1 namespace (has changed in 1.1).""" @@ -46,6 +47,5 @@ class XmlTests(integrated_helpers._IntegratedTestBase): response = self.api.api_request('/servers', headers=headers) data = response.read() LOG.debug("data: %s" % data) - - prefix = '<servers xmlns="%s"' % common.XML_NS_V11 - self.assertTrue(data.startswith(prefix)) + root = etree.XML(data) + self.assertEqual(root.nsmap.get(None), common.XML_NS_V11) diff --git a/nova/tests/scheduler/test_abstract_scheduler.py b/nova/tests/scheduler/test_abstract_scheduler.py index aa97e2344..5549ea453 100644 --- a/nova/tests/scheduler/test_abstract_scheduler.py +++ b/nova/tests/scheduler/test_abstract_scheduler.py @@ -26,6 +26,7 @@ from nova import test from nova.compute import api as compute_api from nova.scheduler import driver from nova.scheduler import abstract_scheduler +from nova.scheduler import base_scheduler from nova.scheduler import zone_manager @@ -65,6 +66,11 @@ class FakeAbstractScheduler(abstract_scheduler.AbstractScheduler): pass +class FakeBaseScheduler(base_scheduler.BaseScheduler): + # No need to stub anything at the moment + pass + + class FakeZoneManager(zone_manager.ZoneManager): def __init__(self): self.service_states = { @@ -365,3 +371,52 @@ class AbstractSchedulerTestCase(test.TestCase): self.assertEqual(fixture._decrypt_blob(test_data), json.dumps(test_data)) + + def test_empty_local_hosts(self): + """ + Create a nested set of FakeZones, try to build multiple instances + and ensure that a select call returns the appropriate build plan. + """ + sched = FakeAbstractScheduler() + self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method) + self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all) + + zm = FakeZoneManager() + # patch this to have no local hosts + zm.service_states = {} + sched.set_zone_manager(zm) + + fake_context = {} + build_plan = sched.select(fake_context, + {'instance_type': {'memory_mb': 512}, + 'num_instances': 4}) + + # 0 from local zones, 12 from remotes + self.assertEqual(12, len(build_plan)) + + +class BaseSchedulerTestCase(test.TestCase): + """Test case for Base Scheduler.""" + + def test_weigh_hosts(self): + """ + Try to weigh a short list of hosts and make sure enough + entries for a larger number instances are returned. + """ + + sched = FakeBaseScheduler() + + # Fake out a list of hosts + zm = FakeZoneManager() + hostlist = [(host, services['compute']) + for host, services in zm.service_states.items() + if 'compute' in services] + + # Call weigh_hosts() + num_instances = len(hostlist) * 2 + len(hostlist) / 2 + instlist = sched.weigh_hosts('compute', + dict(num_instances=num_instances), + hostlist) + + # Should be enough entries to cover all instances + self.assertEqual(len(instlist), num_instances) diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index a52dd041a..890348192 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -963,9 +963,14 @@ class FakeZone(object): self.password = password +ZONE_API_URL1 = "http://1.example.com" +ZONE_API_URL2 = "http://2.example.com" + + def zone_get_all(context): return [ - FakeZone(1, 'http://example.com', 'bob', 'xxx'), + FakeZone(1, ZONE_API_URL1, 'bob', 'xxx'), + FakeZone(2, ZONE_API_URL2, 'bob', 'xxx'), ] @@ -1065,7 +1070,9 @@ class ZoneRedirectTest(test.TestCase): def test_unmarshal_single_server(self): decorator = api.reroute_compute("foo") - self.assertEquals(decorator.unmarshall_result([]), {}) + decorator.item_uuid = 'fake_uuid' + self.assertRaises(exception.InstanceNotFound, + decorator.unmarshall_result, []) self.assertEquals(decorator.unmarshall_result( [FakeResource(dict(a=1, b=2)), ]), dict(server=dict(a=1, b=2))) @@ -1079,6 +1086,90 @@ class ZoneRedirectTest(test.TestCase): [FakeResource(dict(_a=1, manager=2)), ]), dict(server={})) + def test_one_zone_down_no_instances(self): + + def _fake_issue_novaclient_command(nova, zone, *args, **kwargs): + return None + + class FakeNovaClientWithFailure(object): + def __init__(self, username, password, method, api_url): + self.api_url = api_url + + def authenticate(self): + if self.api_url == ZONE_API_URL2: + raise novaclient_exceptions.BadRequest('foo') + + self.stubs.Set(api, '_issue_novaclient_command', + _fake_issue_novaclient_command) + self.stubs.Set(api.novaclient, 'Client', FakeNovaClientWithFailure) + + @api.reroute_compute("get") + def do_get(self, context, uuid): + pass + + self.assertRaises(exception.ZoneRequestError, + do_get, None, {}, FAKE_UUID) + + def test_one_zone_down_got_instance(self): + + def _fake_issue_novaclient_command(nova, zone, *args, **kwargs): + class FakeServer(object): + def __init__(self): + self.id = FAKE_UUID + self.test = '1234' + return FakeServer() + + class FakeNovaClientWithFailure(object): + def __init__(self, username, password, method, api_url): + self.api_url = api_url + + def authenticate(self): + if self.api_url == ZONE_API_URL2: + raise novaclient_exceptions.BadRequest('foo') + + self.stubs.Set(api, '_issue_novaclient_command', + _fake_issue_novaclient_command) + self.stubs.Set(api.novaclient, 'Client', FakeNovaClientWithFailure) + + @api.reroute_compute("get") + def do_get(self, context, uuid): + pass + + try: + do_get(None, {}, FAKE_UUID) + except api.RedirectResult, e: + results = e.results + self.assertIn('server', results) + self.assertEqual(results['server']['id'], FAKE_UUID) + self.assertEqual(results['server']['test'], '1234') + except Exception, e: + self.fail(_("RedirectResult should have been raised")) + else: + self.fail(_("RedirectResult should have been raised")) + + def test_zones_up_no_instances(self): + + def _fake_issue_novaclient_command(nova, zone, *args, **kwargs): + return None + + class FakeNovaClientNoFailure(object): + def __init__(self, username, password, method, api_url): + pass + + def authenticate(self): + return + + self.stubs.Set(api, '_issue_novaclient_command', + _fake_issue_novaclient_command) + self.stubs.Set(api.novaclient, 'Client', FakeNovaClientNoFailure) + + @api.reroute_compute("get") + def do_get(self, context, uuid): + pass + + self.assertRaises(exception.InstanceNotFound, + do_get, None, {}, FAKE_UUID) + class FakeServerCollection(object): def get(self, instance_id): @@ -1097,7 +1188,7 @@ class FakeEmptyServerCollection(object): class FakeNovaClient(object): - def __init__(self, collection): + def __init__(self, collection, *args, **kwargs): self.servers = collection @@ -1162,8 +1253,9 @@ class CallZoneMethodTest(test.TestCase): context = {} method = 'do_something' results = api.call_zone_method(context, method) - expected = [(1, 42)] - self.assertEqual(expected, results) + self.assertEqual(len(results), 2) + self.assertIn((1, 42), results) + self.assertIn((2, 42), results) def test_call_zone_method_not_present(self): context = {} diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 766a7da9b..4d463572b 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -161,6 +161,19 @@ class ComputeTestCase(test.TestCase): db.security_group_destroy(self.context, group['id']) db.instance_destroy(self.context, ref[0]['id']) + def test_create_instance_with_invalid_security_group_raises(self): + instance_type = instance_types.get_default_instance_type() + + pre_build_len = len(db.instance_get_all(context.get_admin_context())) + self.assertRaises(exception.SecurityGroupNotFoundForProject, + self.compute_api.create, + self.context, + instance_type=instance_type, + image_href=None, + security_group=['this_is_a_fake_sec_group']) + self.assertEqual(pre_build_len, + len(db.instance_get_all(context.get_admin_context()))) + def test_create_instance_associates_config_drive(self): """Make sure create associates a config drive.""" @@ -287,11 +300,20 @@ class ComputeTestCase(test.TestCase): self.compute.resume_instance(self.context, instance_id) self.compute.terminate_instance(self.context, instance_id) - def test_reboot(self): - """Ensure instance can be rebooted""" + def test_soft_reboot(self): + """Ensure instance can be soft rebooted""" + instance_id = self._create_instance() + reboot_type = "SOFT" + self.compute.run_instance(self.context, instance_id) + self.compute.reboot_instance(self.context, instance_id, reboot_type) + self.compute.terminate_instance(self.context, instance_id) + + def test_hard_reboot(self): + """Ensure instance can be hard rebooted""" instance_id = self._create_instance() + reboot_type = "HARD" self.compute.run_instance(self.context, instance_id) - self.compute.reboot_instance(self.context, instance_id) + self.compute.reboot_instance(self.context, instance_id, reboot_type) self.compute.terminate_instance(self.context, instance_id) def test_set_admin_password(self): diff --git a/nova/tests/test_direct.py b/nova/tests/test_direct.py index 4ed0c2aa5..8d856dc4b 100644 --- a/nova/tests/test_direct.py +++ b/nova/tests/test_direct.py @@ -30,7 +30,7 @@ from nova import test from nova import volume from nova import utils from nova.api import direct -from nova.tests import test_cloud +from nova.tests.api.ec2 import test_cloud class ArbitraryObject(object): diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 1254c3299..769bc5291 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -35,61 +35,47 @@ from nova import utils from nova.api.ec2 import cloud from nova.compute import power_state from nova.compute import vm_states +from nova.virt import driver from nova.virt.libvirt import connection from nova.virt.libvirt import firewall +from nova.tests import fake_network libvirt = None FLAGS = flags.FLAGS +_fake_network_info = fake_network.fake_get_instance_nw_info +_ipv4_like = fake_network.ipv4_like + def _concurrency(wait, done, target): wait.wait() done.send() -def _create_network_info(count=1, ipv6=None): - if ipv6 is None: - ipv6 = FLAGS.use_ipv6 - fake = 'fake' - fake_ip = '10.11.12.13' - fake_ip_2 = '0.0.0.1' - fake_ip_3 = '0.0.0.1' - fake_vlan = 100 - fake_bridge_interface = 'eth0' - network = {'bridge': fake, - 'cidr': fake_ip, - 'cidr_v6': fake_ip, - 'gateway_v6': fake, - 'vlan': fake_vlan, - 'bridge_interface': fake_bridge_interface} - mapping = {'mac': fake, - 'dhcp_server': '10.0.0.1', - 'gateway': fake, - 'gateway6': fake, - 'ips': [{'ip': fake_ip}, {'ip': fake_ip}]} - if ipv6: - mapping['ip6s'] = [{'ip': fake_ip}, - {'ip': fake_ip_2}, - {'ip': fake_ip_3}] - return [(network, mapping) for x in xrange(0, count)] - - -def _setup_networking(instance_id, ip='1.2.3.4', mac='56:12:12:12:12:12'): - ctxt = context.get_admin_context() - network_ref = db.project_get_networks(ctxt, - 'fake', - associate=True)[0] - vif = {'address': mac, - 'network_id': network_ref['id'], - 'instance_id': instance_id} - vif_ref = db.virtual_interface_create(ctxt, vif) - - fixed_ip = {'address': ip, - 'network_id': network_ref['id'], - 'virtual_interface_id': vif_ref['id']} - db.fixed_ip_create(ctxt, fixed_ip) - db.fixed_ip_update(ctxt, ip, {'allocated': True, - 'instance_id': instance_id}) +class FakeVirtDomain(object): + + def __init__(self, fake_xml=None): + if fake_xml: + self._fake_dom_xml = fake_xml + else: + self._fake_dom_xml = """ + <domain type='kvm'> + <devices> + <disk type='file'> + <source file='filename'/> + </disk> + </devices> + </domain> + """ + + def snapshotCreateXML(self, *args): + return None + + def createWithFlags(self, launch_flags): + pass + + def XMLDesc(self, *args): + return self._fake_dom_xml class CacheConcurrencyTestCase(test.TestCase): @@ -163,7 +149,6 @@ class LibvirtConnTestCase(test.TestCase): self.context = context.get_admin_context() self.flags(instances_path='') self.call_libvirt_dependant_setup = False - self.test_ip = '10.11.12.13' test_instance = {'memory_kb': '1024000', 'basepath': '/some/path', @@ -194,70 +179,24 @@ class LibvirtConnTestCase(test.TestCase): # A fake libvirt.virConnect class FakeLibvirtConnection(object): - pass - - # A fake connection.IptablesFirewallDriver - class FakeIptablesFirewallDriver(object): - - def __init__(self, **kwargs): - pass - - def setattr(self, key, val): - self.__setattr__(key, val) - - # A fake VIF driver - class FakeVIFDriver(object): - - def __init__(self, **kwargs): - pass - - def setattr(self, key, val): - self.__setattr__(key, val) - - def plug(self, instance, network, mapping): - return { - 'id': 'fake', - 'bridge_name': 'fake', - 'mac_address': 'fake', - 'ip_address': 'fake', - 'dhcp_server': 'fake', - 'extra_params': 'fake', - } + def defineXML(self, xml): + return FakeVirtDomain() # Creating mocks fake = FakeLibvirtConnection() - fakeip = FakeIptablesFirewallDriver - fakevif = FakeVIFDriver() # Customizing above fake if necessary for key, val in kwargs.items(): fake.__setattr__(key, val) - # Inevitable mocks for connection.LibvirtConnection - self.mox.StubOutWithMock(connection.utils, 'import_class') - connection.utils.import_class(mox.IgnoreArg()).AndReturn(fakeip) - self.mox.StubOutWithMock(connection.utils, 'import_object') - connection.utils.import_object(mox.IgnoreArg()).AndReturn(fakevif) + self.flags(image_service='nova.image.fake.FakeImageService') + fw_driver = "nova.tests.fake_network.FakeIptablesFirewallDriver" + self.flags(firewall_driver=fw_driver) + self.flags(libvirt_vif_driver="nova.tests.fake_network.FakeVIFDriver") + self.mox.StubOutWithMock(connection.LibvirtConnection, '_conn') connection.LibvirtConnection._conn = fake def fake_lookup(self, instance_name): - - class FakeVirtDomain(object): - - def snapshotCreateXML(self, *args): - return None - - def XMLDesc(self, *args): - return """ - <domain type='kvm'> - <devices> - <disk type='file'> - <source file='filename'/> - </disk> - </devices> - </domain> - """ - return FakeVirtDomain() def fake_execute(self, *args): @@ -277,12 +216,12 @@ class LibvirtConnTestCase(test.TestCase): instance_ref = db.instance_create(self.context, self.test_instance) result = conn._prepare_xml_info(instance_ref, - _create_network_info(), + _fake_network_info(self.stubs, 1), False) self.assertTrue(len(result['nics']) == 1) result = conn._prepare_xml_info(instance_ref, - _create_network_info(2), + _fake_network_info(self.stubs, 2), False) self.assertTrue(len(result['nics']) == 2) @@ -407,7 +346,7 @@ class LibvirtConnTestCase(test.TestCase): def test_multi_nic(self): instance_data = dict(self.test_instance) - network_info = _create_network_info(2) + network_info = _fake_network_info(self.stubs, 2) conn = connection.LibvirtConnection(True) instance_ref = db.instance_create(self.context, instance_data) xml = conn.to_xml(instance_ref, network_info, False) @@ -417,15 +356,14 @@ class LibvirtConnTestCase(test.TestCase): parameters = interfaces[0].findall('./filterref/parameter') self.assertEquals(interfaces[0].get('type'), 'bridge') self.assertEquals(parameters[0].get('name'), 'IP') - self.assertEquals(parameters[0].get('value'), '10.11.12.13') + self.assertTrue(_ipv4_like(parameters[0].get('value'), '192.168')) self.assertEquals(parameters[1].get('name'), 'DHCPSERVER') - self.assertEquals(parameters[1].get('value'), '10.0.0.1') + self.assertTrue(_ipv4_like(parameters[1].get('value'), '192.168.*.1')) def _check_xml_and_container(self, instance): user_context = context.RequestContext(self.user_id, self.project_id) instance_ref = db.instance_create(user_context, instance) - _setup_networking(instance_ref['id'], self.test_ip) self.flags(libvirt_type='lxc') conn = connection.LibvirtConnection(True) @@ -433,7 +371,7 @@ class LibvirtConnTestCase(test.TestCase): uri = conn.get_uri() self.assertEquals(uri, 'lxc:///') - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) xml = conn.to_xml(instance_ref, network_info) tree = xml_to_tree(xml) @@ -457,8 +395,6 @@ class LibvirtConnTestCase(test.TestCase): network_ref = db.project_get_networks(context.get_admin_context(), self.project_id)[0] - _setup_networking(instance_ref['id'], self.test_ip) - type_uri_map = {'qemu': ('qemu:///system', [(lambda t: t.find('.').get('type'), 'qemu'), (lambda t: t.find('./os/type').text, 'hvm'), @@ -504,9 +440,11 @@ class LibvirtConnTestCase(test.TestCase): common_checks = [ (lambda t: t.find('.').tag, 'domain'), (lambda t: t.find(parameter).get('name'), 'IP'), - (lambda t: t.find(parameter).get('value'), '10.11.12.13'), + (lambda t: _ipv4_like(t.find(parameter).get('value'), '192.168'), + True), (lambda t: t.findall(parameter)[1].get('name'), 'DHCPSERVER'), - (lambda t: t.findall(parameter)[1].get('value'), '10.0.0.1'), + (lambda t: _ipv4_like(t.findall(parameter)[1].get('value'), + '192.168.*.1'), True), (lambda t: t.find('./devices/serial/source').get( 'path').split('/')[1], 'console.log'), (lambda t: t.find('./memory').text, '2097152')] @@ -531,7 +469,7 @@ class LibvirtConnTestCase(test.TestCase): uri = conn.get_uri() self.assertEquals(uri, expected_uri) - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) xml = conn.to_xml(instance_ref, network_info, rescue) tree = xml_to_tree(xml) for i, (check, expected_result) in enumerate(checks): @@ -646,7 +584,7 @@ class LibvirtConnTestCase(test.TestCase): self.create_fake_libvirt_mock() instance_ref = db.instance_create(self.context, self.test_instance) - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) # Start test self.mox.ReplayAll() @@ -743,7 +681,7 @@ class LibvirtConnTestCase(test.TestCase): # qemu-img should be mockd since test environment might not have # large disk space. self.mox.StubOutWithMock(utils, "execute") - utils.execute('sudo', 'qemu-img', 'create', '-f', 'raw', + utils.execute('qemu-img', 'create', '-f', 'raw', '%s/%s/disk' % (tmpdir, instance_ref.name), '10G') self.mox.ReplayAll() @@ -795,7 +733,7 @@ class LibvirtConnTestCase(test.TestCase): os.path.getsize("/test/disk").AndReturn(10 * 1024 * 1024 * 1024) # another is qcow image, so qemu-img should be mocked. self.mox.StubOutWithMock(utils, "execute") - utils.execute('sudo', 'qemu-img', 'info', '/test/disk.local').\ + utils.execute('qemu-img', 'info', '/test/disk.local').\ AndReturn((ret, '')) self.mox.ReplayAll() @@ -830,7 +768,7 @@ class LibvirtConnTestCase(test.TestCase): conn.firewall_driver.setattr('setup_basic_filtering', fake_none) conn.firewall_driver.setattr('prepare_instance_filter', fake_none) - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) try: conn.spawn(self.context, instance, network_info) @@ -840,8 +778,6 @@ class LibvirtConnTestCase(test.TestCase): shutil.rmtree(os.path.join(FLAGS.instances_path, instance.name)) shutil.rmtree(os.path.join(FLAGS.instances_path, '_base')) - self.assertTrue(count) - def test_get_host_ip_addr(self): conn = connection.LibvirtConnection(False) ip = conn.get_host_ip_addr() @@ -883,6 +819,50 @@ class LibvirtConnTestCase(test.TestCase): _assert_volume_in_mapping('sdg', False) _assert_volume_in_mapping('sdh1', False) + def test_reboot_signature(self): + """Test that libvirt driver method sig matches interface""" + def fake_reboot_with_correct_sig(ignore, instance, + network_info, reboot_type): + pass + + def fake_destroy(instance, network_info, cleanup=False): + pass + + def fake_plug_vifs(instance, network_info): + pass + + def fake_create_new_domain(xml): + return + + def fake_none(self, instance): + return + + instance = db.instance_create(self.context, self.test_instance) + network_info = _fake_network_info(self.stubs, 1) + + self.mox.StubOutWithMock(connection.LibvirtConnection, '_conn') + connection.LibvirtConnection._conn.lookupByName = self.fake_lookup + + conn = connection.LibvirtConnection(False) + self.stubs.Set(conn, 'destroy', fake_destroy) + self.stubs.Set(conn, 'plug_vifs', fake_plug_vifs) + self.stubs.Set(conn.firewall_driver, + 'setup_basic_filtering', + fake_none) + self.stubs.Set(conn.firewall_driver, + 'prepare_instance_filter', + fake_none) + self.stubs.Set(conn, '_create_new_domain', fake_create_new_domain) + self.stubs.Set(conn.firewall_driver, + 'apply_instance_filter', + fake_none) + + args = [instance, network_info, 'SOFT'] + conn.reboot(*args) + + compute_driver = driver.ComputeDriver() + self.assertRaises(NotImplementedError, compute_driver.reboot, *args) + class NWFilterFakes: def __init__(self): @@ -923,7 +903,6 @@ class IptablesFirewallTestCase(test.TestCase): """setup_basic_rules in nwfilter calls this.""" pass self.fake_libvirt_connection = FakeLibvirtConnection() - self.test_ip = '10.11.12.13' self.fw = firewall.IptablesFirewallDriver( get_connection=lambda: self.fake_libvirt_connection) @@ -987,10 +966,6 @@ class IptablesFirewallTestCase(test.TestCase): def test_static_filters(self): instance_ref = self._create_instance_ref() src_instance_ref = self._create_instance_ref() - src_ip = '10.11.12.14' - src_mac = '56:12:12:12:12:13' - _setup_networking(instance_ref['id'], self.test_ip, src_mac) - _setup_networking(src_instance_ref['id'], src_ip) admin_ctxt = context.get_admin_context() secgroup = db.security_group_create(admin_ctxt, @@ -1065,10 +1040,17 @@ class IptablesFirewallTestCase(test.TestCase): return '', '' print cmd, kwargs + def get_fixed_ips(*args, **kwargs): + ips = [] + for network, info in network_info: + ips.extend(info['ips']) + return [ip['ip'] for ip in ips] + from nova.network import linux_net linux_net.iptables_manager.execute = fake_iptables_execute - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) + self.stubs.Set(db, 'instance_get_fixed_addresses', get_fixed_ips) self.fw.prepare_instance_filter(instance_ref, network_info) self.fw.apply_instance_filter(instance_ref, network_info) @@ -1082,7 +1064,8 @@ class IptablesFirewallTestCase(test.TestCase): instance_chain = None for rule in self.out_rules: # This is pretty crude, but it'll do for now - if '-d 10.11.12.13 -j' in rule: + # last two octets change + if re.search('-d 192.168.[0-9]{1,3}.[0-9]{1,3} -j', rule): instance_chain = rule.split(' ')[-1] break self.assertTrue(instance_chain, "The instance chain wasn't added") @@ -1105,10 +1088,11 @@ class IptablesFirewallTestCase(test.TestCase): self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, "ICMP Echo Request acceptance rule wasn't added") - regex = re.compile('-A .* -j ACCEPT -p tcp -m multiport ' - '--dports 80:81 -s %s' % (src_ip,)) - self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, - "TCP port 80/81 acceptance rule wasn't added") + for ip in get_fixed_ips(): + regex = re.compile('-A .* -j ACCEPT -p tcp -m multiport ' + '--dports 80:81 -s %s' % ip) + self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, + "TCP port 80/81 acceptance rule wasn't added") regex = re.compile('-A .* -j ACCEPT -s %s' % (src_ip,)) self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, @@ -1122,24 +1106,27 @@ class IptablesFirewallTestCase(test.TestCase): def test_filters_for_instance_with_ip_v6(self): self.flags(use_ipv6=True) - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) rulesv4, rulesv6 = self.fw._filters_for_instance("fake", network_info) self.assertEquals(len(rulesv4), 2) - self.assertEquals(len(rulesv6), 3) + self.assertEquals(len(rulesv6), 1) def test_filters_for_instance_without_ip_v6(self): self.flags(use_ipv6=False) - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) rulesv4, rulesv6 = self.fw._filters_for_instance("fake", network_info) self.assertEquals(len(rulesv4), 2) self.assertEquals(len(rulesv6), 0) def test_multinic_iptables(self): - ipv4_rules_per_network = 2 - ipv6_rules_per_network = 3 + ipv4_rules_per_addr = 1 + ipv4_addr_per_network = 2 + ipv6_rules_per_addr = 1 + ipv6_addr_per_network = 1 networks_count = 5 instance_ref = self._create_instance_ref() - network_info = _create_network_info(networks_count) + network_info = _fake_network_info(self.stubs, networks_count, + ipv4_addr_per_network) ipv4_len = len(self.fw.iptables.ipv4['filter'].rules) ipv6_len = len(self.fw.iptables.ipv6['filter'].rules) inst_ipv4, inst_ipv6 = self.fw.instance_rules(instance_ref, @@ -1150,9 +1137,9 @@ class IptablesFirewallTestCase(test.TestCase): ipv4_network_rules = len(ipv4) - len(inst_ipv4) - ipv4_len ipv6_network_rules = len(ipv6) - len(inst_ipv6) - ipv6_len self.assertEquals(ipv4_network_rules, - ipv4_rules_per_network * networks_count) + ipv4_rules_per_addr * ipv4_addr_per_network * networks_count) self.assertEquals(ipv6_network_rules, - ipv6_rules_per_network * networks_count) + ipv6_rules_per_addr * ipv6_addr_per_network * networks_count) def test_do_refresh_security_group_rules(self): instance_ref = self._create_instance_ref() @@ -1178,8 +1165,7 @@ class IptablesFirewallTestCase(test.TestCase): fakefilter.nwfilterLookupByName instance_ref = self._create_instance_ref() - _setup_networking(instance_ref['id'], self.test_ip) - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) self.fw.setup_basic_filtering(instance_ref, network_info) self.fw.prepare_instance_filter(instance_ref, network_info) self.fw.apply_instance_filter(instance_ref, network_info) @@ -1194,13 +1180,12 @@ class IptablesFirewallTestCase(test.TestCase): def test_provider_firewall_rules(self): # setup basic instance data instance_ref = self._create_instance_ref() - _setup_networking(instance_ref['id'], self.test_ip) # FRAGILE: peeks at how the firewall names chains chain_name = 'inst-%s' % instance_ref['id'] # create a firewall via setup_basic_filtering like libvirt_conn.spawn # should have a chain with 0 rules - network_info = _create_network_info(1) + network_info = _fake_network_info(self.stubs, 1) self.fw.setup_basic_filtering(instance_ref, network_info) self.assertTrue('provider' in self.fw.iptables.ipv4['filter'].chains) rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules @@ -1264,7 +1249,6 @@ class NWFilterTestCase(test.TestCase): self.fake_libvirt_connection = Mock() - self.test_ip = '10.11.12.13' self.fw = firewall.NWFilterFirewall( lambda: self.fake_libvirt_connection) @@ -1380,11 +1364,9 @@ class NWFilterTestCase(test.TestCase): instance_ref = self._create_instance() inst_id = instance_ref['id'] - _setup_networking(instance_ref['id'], self.test_ip) - - def _ensure_all_called(): + def _ensure_all_called(mac): instance_filter = 'nova-instance-%s-%s' % (instance_ref['name'], - 'fake') + mac.translate(None, ':')) secgroup_filter = 'nova-secgroup-%s' % self.security_group['id'] for required in [secgroup_filter, 'allow-dhcp-server', 'no-arp-spoofing', 'no-ip-spoofing', @@ -1400,17 +1382,22 @@ class NWFilterTestCase(test.TestCase): self.security_group.id) instance = db.instance_get(self.context, inst_id) - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) + # since there is one (network_info) there is one vif + # pass this vif's mac to _ensure_all_called() + # to set the instance_filter properly + mac = network_info[0][1]['mac'] + self.fw.setup_basic_filtering(instance, network_info) self.fw.prepare_instance_filter(instance, network_info) self.fw.apply_instance_filter(instance, network_info) - _ensure_all_called() + _ensure_all_called(mac) self.teardown_security_group() db.instance_destroy(context.get_admin_context(), instance_ref['id']) def test_create_network_filters(self): instance_ref = self._create_instance() - network_info = _create_network_info(3) + network_info = _fake_network_info(self.stubs, 3) result = self.fw._create_network_filters(instance_ref, network_info, "fake") @@ -1433,8 +1420,7 @@ class NWFilterTestCase(test.TestCase): instance = db.instance_get(self.context, inst_id) - _setup_networking(instance_ref['id'], self.test_ip) - network_info = _create_network_info() + network_info = _fake_network_info(self.stubs, 1) self.fw.setup_basic_filtering(instance, network_info) self.fw.prepare_instance_filter(instance, network_info) self.fw.apply_instance_filter(instance, network_info) diff --git a/nova/tests/test_linux_net.py b/nova/tests/test_linux_net.py new file mode 100755 index 000000000..99577b88e --- /dev/null +++ b/nova/tests/test_linux_net.py @@ -0,0 +1,347 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 NTT
+# 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 nova import context
+from nova import db
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import test
+from nova import utils
+from nova.network import manager as network_manager
+from nova.network import linux_net
+
+import mox
+
+FLAGS = flags.FLAGS
+
+LOG = logging.getLogger('nova.tests.network')
+
+
+HOST = "testhost"
+
+instances = [{'id': 0,
+ 'host': 'fake_instance00',
+ 'hostname': 'fake_instance00'},
+ {'id': 1,
+ 'host': 'fake_instance01',
+ 'hostname': 'fake_instance01'}]
+
+
+addresses = [{"address": "10.0.0.1"},
+ {"address": "10.0.0.2"},
+ {"address": "10.0.0.3"},
+ {"address": "10.0.0.4"},
+ {"address": "10.0.0.5"},
+ {"address": "10.0.0.6"}]
+
+
+networks = [{'id': 0,
+ 'uuid': "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
+ 'label': 'test0',
+ 'injected': False,
+ 'multi_host': False,
+ 'cidr': '192.168.0.0/24',
+ 'cidr_v6': '2001:db8::/64',
+ 'gateway_v6': '2001:db8::1',
+ 'netmask_v6': '64',
+ 'netmask': '255.255.255.0',
+ 'bridge': 'fa0',
+ 'bridge_interface': 'fake_fa0',
+ 'gateway': '192.168.0.1',
+ 'broadcast': '192.168.0.255',
+ 'dns1': '192.168.0.1',
+ 'dns2': '192.168.0.2',
+ 'dhcp_server': '0.0.0.0',
+ 'dhcp_start': '192.168.100.1',
+ 'vlan': None,
+ 'host': None,
+ 'project_id': 'fake_project',
+ 'vpn_public_address': '192.168.0.2'},
+ {'id': 1,
+ 'uuid': "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
+ 'label': 'test1',
+ 'injected': False,
+ 'multi_host': False,
+ 'cidr': '192.168.1.0/24',
+ 'cidr_v6': '2001:db9::/64',
+ 'gateway_v6': '2001:db9::1',
+ 'netmask_v6': '64',
+ 'netmask': '255.255.255.0',
+ 'bridge': 'fa1',
+ 'bridge_interface': 'fake_fa1',
+ 'gateway': '192.168.1.1',
+ 'broadcast': '192.168.1.255',
+ 'dns1': '192.168.0.1',
+ 'dns2': '192.168.0.2',
+ 'dhcp_server': '0.0.0.0',
+ 'dhcp_start': '192.168.100.1',
+ 'vlan': None,
+ 'host': None,
+ 'project_id': 'fake_project',
+ 'vpn_public_address': '192.168.1.2'}]
+
+
+fixed_ips = [{'id': 0,
+ 'network_id': 0,
+ 'address': '192.168.0.100',
+ 'instance_id': 0,
+ 'allocated': True,
+ 'virtual_interface_id': 0,
+ 'virtual_interface': addresses[0],
+ 'instance': instances[0],
+ 'floating_ips': []},
+ {'id': 1,
+ 'network_id': 1,
+ 'address': '192.168.1.100',
+ 'instance_id': 0,
+ 'allocated': True,
+ 'virtual_interface_id': 1,
+ 'virtual_interface': addresses[1],
+ 'instance': instances[0],
+ 'floating_ips': []},
+ {'id': 2,
+ 'network_id': 1,
+ 'address': '192.168.0.101',
+ 'instance_id': 1,
+ 'allocated': True,
+ 'virtual_interface_id': 2,
+ 'virtual_interface': addresses[2],
+ 'instance': instances[1],
+ 'floating_ips': []},
+ {'id': 3,
+ 'network_id': 0,
+ 'address': '192.168.1.101',
+ 'instance_id': 1,
+ 'allocated': True,
+ 'virtual_interface_id': 3,
+ 'virtual_interface': addresses[3],
+ 'instance': instances[1],
+ 'floating_ips': []},
+ {'id': 4,
+ 'network_id': 0,
+ 'address': '192.168.0.102',
+ 'instance_id': 0,
+ 'allocated': True,
+ 'virtual_interface_id': 4,
+ 'virtual_interface': addresses[4],
+ 'instance': instances[0],
+ 'floating_ips': []},
+ {'id': 5,
+ 'network_id': 1,
+ 'address': '192.168.1.102',
+ 'instance_id': 1,
+ 'allocated': True,
+ 'virtual_interface_id': 5,
+ 'virtual_interface': addresses[5],
+ 'instance': instances[1],
+ 'floating_ips': []}]
+
+
+vifs = [{'id': 0,
+ 'address': 'DE:AD:BE:EF:00:00',
+ 'uuid': '00000000-0000-0000-0000-0000000000000000',
+ 'network_id': 0,
+ 'network': networks[0],
+ 'instance_id': 0},
+ {'id': 1,
+ 'address': 'DE:AD:BE:EF:00:01',
+ 'uuid': '00000000-0000-0000-0000-0000000000000001',
+ 'network_id': 1,
+ 'network': networks[1],
+ 'instance_id': 0},
+ {'id': 2,
+ 'address': 'DE:AD:BE:EF:00:02',
+ 'uuid': '00000000-0000-0000-0000-0000000000000002',
+ 'network_id': 1,
+ 'network': networks[1],
+ 'instance_id': 1},
+ {'id': 3,
+ 'address': 'DE:AD:BE:EF:00:03',
+ 'uuid': '00000000-0000-0000-0000-0000000000000003',
+ 'network_id': 0,
+ 'network': networks[0],
+ 'instance_id': 1},
+ {'id': 4,
+ 'address': 'DE:AD:BE:EF:00:04',
+ 'uuid': '00000000-0000-0000-0000-0000000000000004',
+ 'network_id': 0,
+ 'network': networks[0],
+ 'instance_id': 0},
+ {'id': 5,
+ 'address': 'DE:AD:BE:EF:00:05',
+ 'uuid': '00000000-0000-0000-0000-0000000000000005',
+ 'network_id': 1,
+ 'network': networks[1],
+ 'instance_id': 1}]
+
+
+class LinuxNetworkTestCase(test.TestCase):
+
+ def setUp(self):
+ super(LinuxNetworkTestCase, self).setUp()
+ network_driver = FLAGS.network_driver
+ self.driver = utils.import_object(network_driver)
+ self.driver.db = db
+
+ def test_update_dhcp_for_nw00(self):
+ self.flags(use_single_default_gateway=True)
+ self.mox.StubOutWithMock(db, 'network_get_associated_fixed_ips')
+ self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance')
+
+ db.network_get_associated_fixed_ips(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([fixed_ips[0],
+ fixed_ips[3]])
+
+ db.network_get_associated_fixed_ips(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([fixed_ips[0],
+ fixed_ips[3]])
+ db.virtual_interface_get_by_instance(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([vifs[0], vifs[1]])
+ db.virtual_interface_get_by_instance(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([vifs[2], vifs[3]])
+ self.mox.ReplayAll()
+
+ self.driver.update_dhcp(None, "eth0", networks[0])
+
+ def test_update_dhcp_for_nw01(self):
+ self.flags(use_single_default_gateway=True)
+ self.mox.StubOutWithMock(db, 'network_get_associated_fixed_ips')
+ self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance')
+
+ db.network_get_associated_fixed_ips(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([fixed_ips[1],
+ fixed_ips[2]])
+
+ db.network_get_associated_fixed_ips(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([fixed_ips[1],
+ fixed_ips[2]])
+ db.virtual_interface_get_by_instance(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([vifs[0], vifs[1]])
+ db.virtual_interface_get_by_instance(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([vifs[2], vifs[3]])
+ self.mox.ReplayAll()
+
+ self.driver.update_dhcp(None, "eth0", networks[0])
+
+ def test_get_dhcp_hosts_for_nw00(self):
+ self.flags(use_single_default_gateway=True)
+ self.mox.StubOutWithMock(db, 'network_get_associated_fixed_ips')
+
+ db.network_get_associated_fixed_ips(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([fixed_ips[0],
+ fixed_ips[3]])
+ self.mox.ReplayAll()
+
+ expected = \
+ "10.0.0.1,fake_instance00.novalocal,"\
+ "192.168.0.100,net:NW-i00000000-0\n"\
+ "10.0.0.4,fake_instance01.novalocal,"\
+ "192.168.1.101,net:NW-i00000001-0"
+ actual_hosts = self.driver.get_dhcp_hosts(None, networks[1])
+
+ self.assertEquals(actual_hosts, expected)
+
+ def test_get_dhcp_hosts_for_nw01(self):
+ self.flags(use_single_default_gateway=True)
+ self.mox.StubOutWithMock(db, 'network_get_associated_fixed_ips')
+
+ db.network_get_associated_fixed_ips(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([fixed_ips[1],
+ fixed_ips[2]])
+ self.mox.ReplayAll()
+
+ expected = \
+ "10.0.0.2,fake_instance00.novalocal,"\
+ "192.168.1.100,net:NW-i00000000-1\n"\
+ "10.0.0.3,fake_instance01.novalocal,"\
+ "192.168.0.101,net:NW-i00000001-1"
+ actual_hosts = self.driver.get_dhcp_hosts(None, networks[0])
+
+ self.assertEquals(actual_hosts, expected)
+
+ def test_get_dhcp_opts_for_nw00(self):
+ self.mox.StubOutWithMock(db, 'network_get_associated_fixed_ips')
+ self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance')
+
+ db.network_get_associated_fixed_ips(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([fixed_ips[0],
+ fixed_ips[3],
+ fixed_ips[4]])
+ db.virtual_interface_get_by_instance(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([vifs[0],
+ vifs[1],
+ vifs[4]])
+ db.virtual_interface_get_by_instance(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([vifs[2],
+ vifs[3],
+ vifs[5]])
+ self.mox.ReplayAll()
+
+ expected_opts = 'NW-i00000001-0,3'
+ actual_opts = self.driver.get_dhcp_opts(None, networks[0])
+
+ self.assertEquals(actual_opts, expected_opts)
+
+ def test_get_dhcp_opts_for_nw01(self):
+ self.mox.StubOutWithMock(db, 'network_get_associated_fixed_ips')
+ self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance')
+
+ db.network_get_associated_fixed_ips(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([fixed_ips[1],
+ fixed_ips[2],
+ fixed_ips[5]])
+ db.virtual_interface_get_by_instance(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([vifs[0],
+ vifs[1],
+ vifs[4]])
+ db.virtual_interface_get_by_instance(mox.IgnoreArg(),
+ mox.IgnoreArg())\
+ .AndReturn([vifs[2],
+ vifs[3],
+ vifs[5]])
+ self.mox.ReplayAll()
+
+ expected_opts = "NW-i00000000-1,3"
+ actual_opts = self.driver.get_dhcp_opts(None, networks[1])
+
+ self.assertEquals(actual_opts, expected_opts)
+
+ def test_dhcp_opts_not_default_gateway_network(self):
+ expected = "NW-i00000000-0,3"
+ actual = self.driver._host_dhcp_opts(fixed_ips[0])
+ self.assertEquals(actual, expected)
+
+ def test_host_dhcp_without_default_gateway_network(self):
+ expected = ("10.0.0.1,fake_instance00.novalocal,192.168.0.100")
+ actual = self.driver._host_dhcp(fixed_ips[0])
+ self.assertEquals(actual, expected)
diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 25ff940f0..926ea065a 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -14,6 +14,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import mox from nova import context from nova import db @@ -21,9 +22,7 @@ from nova import exception from nova import log as logging from nova import test from nova.network import manager as network_manager - - -import mox +from nova.tests import fake_network LOG = logging.getLogger('nova.tests.network') @@ -58,7 +57,7 @@ networks = [{'id': 0, 'dns1': '192.168.0.1', 'dns2': '192.168.0.2', 'vlan': None, - 'host': None, + 'host': HOST, 'project_id': 'fake_project', 'vpn_public_address': '192.168.0.2'}, {'id': 1, @@ -78,7 +77,7 @@ networks = [{'id': 0, 'dns1': '192.168.0.1', 'dns2': '192.168.0.2', 'vlan': None, - 'host': None, + 'host': HOST, 'project_id': 'fake_project', 'vpn_public_address': '192.168.1.2'}] @@ -118,9 +117,14 @@ vifs = [{'id': 0, {'id': 1, 'address': 'DE:AD:BE:EF:00:01', 'uuid': '00000000-0000-0000-0000-0000000000000001', - 'network_id': 0, 'network_id': 1, 'network': FakeModel(**networks[1]), + 'instance_id': 0}, + {'id': 2, + 'address': 'DE:AD:BE:EF:00:02', + 'uuid': '00000000-0000-0000-0000-0000000000000002', + 'network_id': 2, + 'network': None, 'instance_id': 0}] @@ -133,60 +137,50 @@ class FlatNetworkTestCase(test.TestCase): is_admin=False) def test_get_instance_nw_info(self): - self.mox.StubOutWithMock(db, 'fixed_ip_get_by_instance') - self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance') - self.mox.StubOutWithMock(db, 'instance_type_get') - - db.fixed_ip_get_by_instance(mox.IgnoreArg(), - mox.IgnoreArg()).AndReturn(fixed_ips) - db.virtual_interface_get_by_instance(mox.IgnoreArg(), - mox.IgnoreArg()).AndReturn(vifs) - db.instance_type_get(mox.IgnoreArg(), - mox.IgnoreArg()).AndReturn(flavor) - self.mox.ReplayAll() - - nw_info = self.network.get_instance_nw_info(None, 0, 0, None) + fake_get_instance_nw_info = fake_network.fake_get_instance_nw_info - self.assertTrue(nw_info) + nw_info = fake_get_instance_nw_info(self.stubs, 0, 2) + self.assertFalse(nw_info) - for i, nw in enumerate(nw_info): - i8 = i + 8 - check = {'bridge': 'fa%s' % i, + for i, (nw, info) in enumerate(nw_info): + check = {'bridge': 'fake_br%d' % i, 'cidr': '192.168.%s.0/24' % i, - 'cidr_v6': '2001:db%s::/64' % i8, + 'cidr_v6': '2001:db8:0:%x::/64' % i, 'id': i, 'multi_host': False, - 'injected': 'DONTCARE', - 'bridge_interface': 'fake_fa%s' % i, + 'injected': False, + 'bridge_interface': 'fake_eth%d' % i, 'vlan': None} - self.assertDictMatch(nw[0], check) + self.assertDictMatch(nw, check) - check = {'broadcast': '192.168.%s.255' % i, - 'dhcp_server': '192.168.%s.1' % i, - 'dns': 'DONTCARE', - 'gateway': '192.168.%s.1' % i, - 'gateway6': '2001:db%s::1' % i8, + check = {'broadcast': '192.168.%d.255' % i, + 'dhcp_server': '192.168.%d.1' % i, + 'dns': ['192.168.%d.3' % n, '192.168.%d.4' % n], + 'gateway': '192.168.%d.1' % i, + 'gateway6': '2001:db8:0:%x::1' % i, 'ip6s': 'DONTCARE', 'ips': 'DONTCARE', - 'label': 'test%s' % i, - 'mac': 'DE:AD:BE:EF:00:0%s' % i, - 'vif_uuid': ('00000000-0000-0000-0000-000000000000000%s' % - i), - 'rxtx_cap': 'DONTCARE', + 'label': 'test%d' % i, + 'mac': 'DE:AD:BE:EF:00:%02x' % i, + 'vif_uuid': + '00000000-0000-0000-0000-00000000000000%02d' % i, + 'rxtx_cap': 3, 'should_create_vlan': False, 'should_create_bridge': False} - self.assertDictMatch(nw[1], check) + self.assertDictMatch(info, check) check = [{'enabled': 'DONTCARE', - 'ip': '2001:db%s::dcad:beff:feef:%s' % (i8, i), + 'ip': '2001:db8::dcad:beff:feef:%s' % i, 'netmask': '64'}] - self.assertDictListMatch(nw[1]['ip6s'], check) + self.assertDictListMatch(info['ip6s'], check) - check = [{'enabled': '1', - 'ip': '192.168.%s.100' % i, - 'netmask': '255.255.255.0'}] - self.assertDictListMatch(nw[1]['ips'], check) + num_fixed_ips = len(info['ips']) + check = [{'enabled': 'DONTCARE', + 'ip': '192.168.%d.1%02d' % (i, ip_num), + 'netmask': '255.255.255.0'} + for ip_num in xrange(num_fixed_ips)] + self.assertDictListMatch(info['ips'], check) def test_validate_networks(self): self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') @@ -247,6 +241,34 @@ class FlatNetworkTestCase(test.TestCase): self.network.validate_networks(None, requested_networks) + def test_add_fixed_ip_instance_without_vpn_requested_networks(self): + self.mox.StubOutWithMock(db, 'network_get') + self.mox.StubOutWithMock(db, 'network_update') + self.mox.StubOutWithMock(db, 'fixed_ip_associate_pool') + self.mox.StubOutWithMock(db, 'instance_get') + self.mox.StubOutWithMock(db, + 'virtual_interface_get_by_instance_and_network') + self.mox.StubOutWithMock(db, 'fixed_ip_update') + + db.fixed_ip_update(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + db.virtual_interface_get_by_instance_and_network(mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn({'id': 0}) + + db.instance_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn({'security_groups': + [{'id': 0}]}) + db.fixed_ip_associate_pool(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn('192.168.0.101') + db.network_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks[0]) + db.network_update(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.ReplayAll() + self.network.add_fixed_ip_to_instance(self.context, 1, HOST, + networks[0]['id']) + class VlanNetworkTestCase(test.TestCase): def setUp(self): @@ -264,7 +286,8 @@ class VlanNetworkTestCase(test.TestCase): db.fixed_ip_associate(mox.IgnoreArg(), mox.IgnoreArg(), - mox.IgnoreArg()).AndReturn('192.168.0.1') + mox.IgnoreArg(), + reserved=True).AndReturn('192.168.0.1') db.fixed_ip_update(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) @@ -387,6 +410,32 @@ class VlanNetworkTestCase(test.TestCase): mox.IgnoreArg(), mox.IgnoreArg()) + def test_add_fixed_ip_instance_without_vpn_requested_networks(self): + self.mox.StubOutWithMock(db, 'network_get') + self.mox.StubOutWithMock(db, 'fixed_ip_associate_pool') + self.mox.StubOutWithMock(db, 'instance_get') + self.mox.StubOutWithMock(db, + 'virtual_interface_get_by_instance_and_network') + self.mox.StubOutWithMock(db, 'fixed_ip_update') + + db.fixed_ip_update(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + db.virtual_interface_get_by_instance_and_network(mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn({'id': 0}) + + db.instance_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn({'security_groups': + [{'id': 0}]}) + db.fixed_ip_associate_pool(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn('192.168.0.101') + db.network_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks[0]) + self.mox.ReplayAll() + self.network.add_fixed_ip_to_instance(self.context, 1, HOST, + networks[0]['id']) + class CommonNetworkTestCase(test.TestCase): diff --git a/nova/tests/test_quantum.py b/nova/tests/test_quantum.py new file mode 100644 index 000000000..0feec9b99 --- /dev/null +++ b/nova/tests/test_quantum.py @@ -0,0 +1,323 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira, Inc. +# 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 nova import context +from nova import db +from nova.db.sqlalchemy import models +from nova.db.sqlalchemy.session import get_session +from nova import exception +from nova import ipv6 +from nova import log as logging +from nova.network.quantum import manager as quantum_manager +from nova import test +from nova import utils + +LOG = logging.getLogger('nova.tests.quantum_network') + + +# this class can be used for unit functional/testing on nova, +# as it does not actually make remote calls to the Quantum service +class FakeQuantumClientConnection(object): + + def __init__(self): + self.nets = {} + + def get_networks_for_tenant(self, tenant_id): + net_ids = [] + for net_id, n in self.nets.items(): + if n['tenant-id'] == tenant_id: + net_ids.append(net_id) + return net_ids + + def create_network(self, tenant_id, network_name): + + uuid = str(utils.gen_uuid()) + self.nets[uuid] = {'net-name': network_name, + 'tenant-id': tenant_id, + 'ports': {}} + return uuid + + def delete_network(self, tenant_id, net_id): + if self.nets[net_id]['tenant-id'] == tenant_id: + del self.nets[net_id] + + def network_exists(self, tenant_id, net_id): + try: + return self.nets[net_id]['tenant-id'] == tenant_id + except KeyError: + return False + + def _confirm_not_attached(self, interface_id): + for n in self.nets.values(): + for p in n['ports'].values(): + if p['attachment-id'] == interface_id: + raise Exception(_("interface '%s' is already attached" % + interface_id)) + + def create_and_attach_port(self, tenant_id, net_id, interface_id): + if not self.network_exists(tenant_id, net_id): + raise Exception( + _("network %(net_id)s does not exist for tenant %(tenant_id)" + % locals())) + + self._confirm_not_attached(interface_id) + uuid = str(utils.gen_uuid()) + self.nets[net_id]['ports'][uuid] = \ + {"port-state": "ACTIVE", + "attachment-id": interface_id} + + def detach_and_delete_port(self, tenant_id, net_id, port_id): + if not self.network_exists(tenant_id, net_id): + raise exception.NotFound( + _("network %(net_id)s does not exist " + "for tenant %(tenant_id)s" % locals())) + del self.nets[net_id]['ports'][port_id] + + def get_port_by_attachment(self, tenant_id, attachment_id): + for net_id, n in self.nets.items(): + if n['tenant-id'] == tenant_id: + for port_id, p in n['ports'].items(): + if p['attachment-id'] == attachment_id: + return (net_id, port_id) + + return (None, None) + +networks = [{'label': 'project1-net1', + 'injected': False, + 'multi_host': False, + 'cidr': '192.168.0.0/24', + 'cidr_v6': '2001:1db8::/64', + 'gateway_v6': '2001:1db8::1', + 'netmask_v6': '64', + 'netmask': '255.255.255.0', + 'bridge': None, + 'bridge_interface': None, + 'gateway': '192.168.0.1', + 'broadcast': '192.168.0.255', + 'dns1': '192.168.0.1', + 'dns2': '192.168.0.2', + 'vlan': None, + 'host': None, + 'vpn_public_address': None, + 'project_id': 'fake_project1', + 'priority': 1}, + {'label': 'project2-net1', + 'injected': False, + 'multi_host': False, + 'cidr': '192.168.1.0/24', + 'cidr_v6': '2001:1db9::/64', + 'gateway_v6': '2001:1db9::1', + 'netmask_v6': '64', + 'netmask': '255.255.255.0', + 'bridge': None, + 'bridge_interface': None, + 'gateway': '192.168.1.1', + 'broadcast': '192.168.1.255', + 'dns1': '192.168.0.1', + 'dns2': '192.168.0.2', + 'vlan': None, + 'host': None, + 'project_id': 'fake_project2', + 'priority': 1}, + {'label': "public", + 'injected': False, + 'multi_host': False, + 'cidr': '10.0.0.0/24', + 'cidr_v6': '2001:1dba::/64', + 'gateway_v6': '2001:1dba::1', + 'netmask_v6': '64', + 'netmask': '255.255.255.0', + 'bridge': None, + 'bridge_interface': None, + 'gateway': '10.0.0.1', + 'broadcast': '10.0.0.255', + 'dns1': '10.0.0.1', + 'dns2': '10.0.0.2', + 'vlan': None, + 'host': None, + 'project_id': None, + 'priority': 0}, + {'label': "project2-net2", + 'injected': False, + 'multi_host': False, + 'cidr': '9.0.0.0/24', + 'cidr_v6': '2001:1dbb::/64', + 'gateway_v6': '2001:1dbb::1', + 'netmask_v6': '64', + 'netmask': '255.255.255.0', + 'bridge': None, + 'bridge_interface': None, + 'gateway': '9.0.0.1', + 'broadcast': '9.0.0.255', + 'dns1': '9.0.0.1', + 'dns2': '9.0.0.2', + 'vlan': None, + 'host': None, + 'project_id': "fake_project2", + 'priority': 2}] + + +# this is a base class to be used by all other Quantum Test classes +class QuantumTestCaseBase(object): + + def test_create_and_delete_nets(self): + self._create_nets() + self._delete_nets() + + def _create_nets(self): + for n in networks: + ctx = context.RequestContext('user1', n['project_id']) + self.net_man.create_networks(ctx, + label=n['label'], cidr=n['cidr'], + multi_host=n['multi_host'], + num_networks=1, network_size=256, cidr_v6=n['cidr_v6'], + gateway_v6=n['gateway_v6'], bridge=None, + bridge_interface=None, dns1=n['dns1'], + dns2=n['dns2'], project_id=n['project_id'], + priority=n['priority']) + + def _delete_nets(self): + for n in networks: + ctx = context.RequestContext('user1', n['project_id']) + self.net_man.delete_network(ctx, n['cidr']) + + def test_allocate_and_deallocate_instance_static(self): + self._create_nets() + + project_id = "fake_project1" + ctx = context.RequestContext('user1', project_id) + + instance_ref = db.api.instance_create(ctx, + {"project_id": project_id}) + nw_info = self.net_man.allocate_for_instance(ctx, + instance_id=instance_ref['id'], host="", + instance_type_id=instance_ref['instance_type_id'], + project_id=project_id) + + self.assertEquals(len(nw_info), 2) + + # we don't know which order the NICs will be in until we + # introduce the notion of priority + # v4 cidr + self.assertTrue(nw_info[0][0]['cidr'].startswith("10.")) + self.assertTrue(nw_info[1][0]['cidr'].startswith("192.")) + + # v4 address + self.assertTrue(nw_info[0][1]['ips'][0]['ip'].startswith("10.")) + self.assertTrue(nw_info[1][1]['ips'][0]['ip'].startswith("192.")) + + # v6 cidr + self.assertTrue(nw_info[0][0]['cidr_v6'].startswith("2001:1dba:")) + self.assertTrue(nw_info[1][0]['cidr_v6'].startswith("2001:1db8:")) + + # v6 address + self.assertTrue( + nw_info[0][1]['ip6s'][0]['ip'].startswith("2001:1dba:")) + self.assertTrue( + nw_info[1][1]['ip6s'][0]['ip'].startswith("2001:1db8:")) + + self.net_man.deallocate_for_instance(ctx, + instance_id=instance_ref['id'], + project_id=project_id) + + self._delete_nets() + + def test_allocate_and_deallocate_instance_dynamic(self): + self._create_nets() + project_id = "fake_project2" + ctx = context.RequestContext('user1', project_id) + + net_ids = self.net_man.q_conn.get_networks_for_tenant(project_id) + requested_networks = [(net_id, None) for net_id in net_ids] + + self.net_man.validate_networks(ctx, requested_networks) + + instance_ref = db.api.instance_create(ctx, + {"project_id": project_id}) + nw_info = self.net_man.allocate_for_instance(ctx, + instance_id=instance_ref['id'], host="", + instance_type_id=instance_ref['instance_type_id'], + project_id=project_id, + requested_networks=requested_networks) + + self.assertEquals(len(nw_info), 2) + + # we don't know which order the NICs will be in until we + # introduce the notion of priority + # v4 cidr + self.assertTrue(nw_info[0][0]['cidr'].startswith("9.") or + nw_info[1][0]['cidr'].startswith("9.")) + self.assertTrue(nw_info[0][0]['cidr'].startswith("192.") or + nw_info[1][0]['cidr'].startswith("192.")) + + # v4 address + self.assertTrue(nw_info[0][1]['ips'][0]['ip'].startswith("9.") or + nw_info[1][1]['ips'][0]['ip'].startswith("9.")) + self.assertTrue(nw_info[0][1]['ips'][0]['ip'].startswith("192.") or + nw_info[1][1]['ips'][0]['ip'].startswith("192.")) + + # v6 cidr + self.assertTrue(nw_info[0][0]['cidr_v6'].startswith("2001:1dbb:") or + nw_info[1][0]['cidr_v6'].startswith("2001:1dbb:")) + self.assertTrue(nw_info[0][0]['cidr_v6'].startswith("2001:1db9:") or + nw_info[1][0]['cidr_v6'].startswith("2001:1db9:")) + + # v6 address + self.assertTrue( + nw_info[0][1]['ip6s'][0]['ip'].startswith("2001:1dbb:") or + nw_info[1][1]['ip6s'][0]['ip'].startswith("2001:1dbb:")) + self.assertTrue( + nw_info[0][1]['ip6s'][0]['ip'].startswith("2001:1db9:") or + nw_info[1][1]['ip6s'][0]['ip'].startswith("2001:1db9:")) + + self.net_man.deallocate_for_instance(ctx, + instance_id=instance_ref['id'], + project_id=project_id) + + self._delete_nets() + + def test_validate_bad_network(self): + ctx = context.RequestContext('user1', 'fake_project1') + self.assertRaises(exception.NetworkNotFound, + self.net_man.validate_networks, ctx, [("", None)]) + + +class QuantumNovaIPAMTestCase(QuantumTestCaseBase, test.TestCase): + + def setUp(self): + super(QuantumNovaIPAMTestCase, self).setUp() + + self.net_man = quantum_manager.QuantumManager( + ipam_lib="nova.network.quantum.nova_ipam_lib", + q_conn=FakeQuantumClientConnection()) + + # Tests seem to create some networks by default, which + # we don't want. So we delete them. + + ctx = context.RequestContext('user1', 'fake_project1').elevated() + for n in db.network_get_all(ctx): + db.network_delete_safe(ctx, n['id']) + + # Other unit tests (e.g., test_compute.py) have a nasty + # habit of of creating fixed IPs and not cleaning up, which + # can confuse these tests, so we remove all existing fixed + # ips before starting. + session = get_session() + result = session.query(models.FixedIp).all() + with session.begin(): + for fip_ref in result: + session.delete(fip_ref) diff --git a/nova/tests/test_virt_drivers.py b/nova/tests/test_virt_drivers.py index 480247c91..440d3401b 100644 --- a/nova/tests/test_virt_drivers.py +++ b/nova/tests/test_virt_drivers.py @@ -103,8 +103,9 @@ class _VirtDriverTestCase(test.TestCase): def test_reboot(self): instance_ref = test_utils.get_test_instance() network_info = test_utils.get_test_network_info() + reboot_type = "SOFT" self.connection.spawn(self.ctxt, instance_ref, network_info) - self.connection.reboot(instance_ref, network_info) + self.connection.reboot(instance_ref, network_info, reboot_type) @catch_notimplementederror def test_get_host_ip_addr(self): diff --git a/nova/tests/test_vmwareapi.py b/nova/tests/test_vmwareapi.py index 06daf46e8..e6da1690f 100644 --- a/nova/tests/test_vmwareapi.py +++ b/nova/tests/test_vmwareapi.py @@ -170,7 +170,8 @@ class VMWareAPIVMTestCase(test.TestCase): self._create_vm() info = self.conn.get_info(1) self._check_vm_info(info, power_state.RUNNING) - self.conn.reboot(self.instance, self.network_info) + reboot_type = "SOFT" + self.conn.reboot(self.instance, self.network_info, reboot_type) info = self.conn.get_info(1) self._check_vm_info(info, power_state.RUNNING) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 45dad3516..47c6a3c95 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -364,7 +364,7 @@ class XenAPIVMTestCase(test.TestCase): def _test_spawn(self, image_ref, kernel_id, ramdisk_id, instance_type_id="3", os_type="linux", - architecture="x86-64", instance_id=1, + hostname="test", architecture="x86-64", instance_id=1, check_injection=False, create_record=True, empty_dns=False): stubs.stubout_loopingcall_start(self.stubs) @@ -377,6 +377,7 @@ class XenAPIVMTestCase(test.TestCase): 'ramdisk_id': ramdisk_id, 'instance_type_id': instance_type_id, 'os_type': os_type, + 'hostname': hostname, 'architecture': architecture} instance = db.instance_create(self.context, values) else: @@ -494,6 +495,7 @@ class XenAPIVMTestCase(test.TestCase): self.check_vm_params_for_linux_with_external_kernel() def test_spawn_netinject_file(self): + self.flags(flat_injected=True) db_fakes.stub_out_db_instance_api(self.stubs, injected=True) self._tee_executed = False @@ -611,7 +613,6 @@ class XenAPIVMTestCase(test.TestCase): str(3 * 1024)) def test_rescue(self): - self.flags(flat_injected=False) instance = self._create_instance() conn = xenapi_conn.get_connection(False) conn.rescue(self.context, instance, None, []) @@ -932,8 +933,9 @@ class XenAPIDetermineDiskImageTestCase(test.TestCase): self.fake_instance.architecture = 'x86-64' def assert_disk_type(self, disk_type): + ctx = context.RequestContext('fake', 'fake') dt = vm_utils.VMHelper.determine_disk_image_type( - self.fake_instance) + self.fake_instance, ctx) self.assertEqual(disk_type, dt) def test_instance_disk(self): diff --git a/nova/tests/vmwareapi/stubs.py b/nova/tests/vmwareapi/stubs.py index 0ed5e9b68..7de10e612 100644 --- a/nova/tests/vmwareapi/stubs.py +++ b/nova/tests/vmwareapi/stubs.py @@ -47,7 +47,5 @@ def set_stubs(stubs): stubs.Set(vmware_images, 'upload_image', fake.fake_upload_image)
stubs.Set(vmwareapi_conn.VMWareAPISession, "_get_vim_object",
fake_get_vim_object)
- stubs.Set(vmwareapi_conn.VMWareAPISession, "_get_vim_object",
- fake_get_vim_object)
stubs.Set(vmwareapi_conn.VMWareAPISession, "_is_vim_object",
fake_is_vim_object)
diff --git a/nova/utils.py b/nova/utils.py index 21e6221b2..81157a4cd 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -901,3 +901,12 @@ def monkey_patch(): func = import_class("%s.%s" % (module, key)) setattr(sys.modules[module], key,\ decorator("%s.%s" % (module, key), func)) + + +def convert_to_list_dict(lst, label): + """Convert a value or list into a list of dicts""" + if not lst: + return None + if not isinstance(lst, list): + lst = [lst] + return [{label: x} for x in lst] diff --git a/nova/version.py b/nova/version.py index 1f8d08e8c..06810df46 100644 --- a/nova/version.py +++ b/nova/version.py @@ -22,7 +22,7 @@ except ImportError: 'revno': 0} -NOVA_VERSION = ['2011', '3'] +NOVA_VERSION = ['2012', '1'] YEAR, COUNT = NOVA_VERSION FINAL = False # This becomes true at Release Candidate time diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 52b2881e8..9fe164cfb 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -52,13 +52,54 @@ flags.DEFINE_integer('timeout_nbd', 10, flags.DEFINE_integer('max_nbd_devices', 16, 'maximum number of possible nbd devices') +# NOTE(yamahata): DEFINE_list() doesn't work because the command may +# include ','. For example, +# mkfs.ext3 -O dir_index,extent -E stride=8,stripe-width=16 +# --label %(fs_label)s %(target)s +# +# DEFINE_list() parses its argument by +# [s.strip() for s in argument.split(self._token)] +# where self._token = ',' +# No escape nor exceptional handling for ','. +# DEFINE_list() doesn't give us what we need. +flags.DEFINE_multistring('virt_mkfs', + ['windows=mkfs.ntfs --fast --label %(fs_label)s ' + '%(target)s', + # NOTE(yamahata): vfat case + #'windows=mkfs.vfat -n %(fs_label)s %(target)s', + 'linux=mkfs.ext3 -L %(fs_label)s -F %(target)s', + 'default=mkfs.ext3 -L %(fs_label)s -F %(target)s'], + 'mkfs commands for ephemeral device. The format is' + '<os_type>=<mkfs command>') + + +_MKFS_COMMAND = {} +_DEFAULT_MKFS_COMMAND = None + + +for s in FLAGS.virt_mkfs: + # NOTE(yamahata): mkfs command may includes '=' for its options. + # So item.partition('=') doesn't work here + os_type, mkfs_command = s.split('=', 1) + if os_type: + _MKFS_COMMAND[os_type] = mkfs_command + if os_type == 'default': + _DEFAULT_MKFS_COMMAND = mkfs_command + + +def mkfs(os_type, fs_label, target): + mkfs_command = (_MKFS_COMMAND.get(os_type, _DEFAULT_MKFS_COMMAND) or + '') % locals() + if mkfs_command: + utils.execute(*mkfs_command.split()) + def extend(image, size): """Increase image to size""" file_size = os.path.getsize(image) if file_size >= size: return - utils.execute('truncate', '-s', size, image) + utils.execute('qemu-img', 'resize', image, size) # NOTE(vish): attempts to resize filesystem utils.execute('e2fsck', '-fp', image, check_exit_code=False) utils.execute('resize2fs', image, check_exit_code=False) @@ -148,15 +189,17 @@ def destroy_container(target, instance, nbd=False): LXC does not support qcow2 images yet. """ + out, err = utils.execute('mount', run_as_root=True) + for loop in out.splitlines(): + if instance['name'] in loop: + device = loop.split()[0] + try: container_dir = '%s/rootfs' % target utils.execute('umount', container_dir, run_as_root=True) - finally: - out, err = utils.execute('losetup', '-a', run_as_root=True) - for loop in out.splitlines(): - if instance['name'] in loop: - device = loop.split(loop, ':') - _unlink_device(device, nbd) + _unlink_device(device, nbd) + except Exception, exn: + LOG.exception(_('Failed to remove container: %s'), exn) def _link_device(image, nbd): @@ -228,8 +271,8 @@ def _inject_metadata_into_fs(metadata, fs, execute=None): metadata_path = os.path.join(fs, "meta.js") metadata = dict([(m.key, m.value) for m in metadata]) - utils.execute('sudo', 'tee', metadata_path, - process_input=json.dumps(metadata)) + utils.execute('tee', metadata_path, + process_input=json.dumps(metadata), run_as_root=True) def _inject_key_into_fs(key, fs, execute=None): diff --git a/nova/virt/driver.py b/nova/virt/driver.py index d05b51bd9..301346c6b 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -165,12 +165,13 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() - def reboot(self, instance, network_info): + def reboot(self, instance, network_info, reboot_type): """Reboot the specified instance. :param instance: Instance object as returned by DB layer. :param network_info: :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param reboot_type: Either a HARD or SOFT reboot """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index d5e2bf31b..3596d8353 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -103,7 +103,7 @@ class FakeConnection(driver.ComputeDriver): if not instance['name'] in self.instances: raise exception.InstanceNotRunning() - def reboot(self, instance, network_info): + def reboot(self, instance, network_info, reboot_type): pass def get_host_ip_addr(self): diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 03a78db1f..76925b405 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -367,7 +367,7 @@ class HyperVConnection(driver.ComputeDriver): wmi_obj.Properties_.Item(prop).Value return newinst - def reboot(self, instance, network_info): + def reboot(self, instance, network_info, reboot_type): """Reboot the specified instance.""" vm = self._lookup(instance.name) if vm is None: diff --git a/nova/virt/images.py b/nova/virt/images.py index 54c691a40..810b359d9 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -37,7 +37,8 @@ def fetch(context, image_href, path, _user_id, _project_id): # when it is added to glance. Right now there is no # auth checking in glance, so we assume that access was # checked before we got here. - (image_service, image_id) = nova.image.get_image_service(image_href) + (image_service, image_id) = nova.image.get_image_service(context, + image_href) with open(path, "wb") as image_file: metadata = image_service.get(context, image_id, image_file) return metadata diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 363a20ed0..64cd2c47f 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -38,6 +38,7 @@ Supports KVM, LXC, QEMU, UML, and XEN. """ import hashlib +import functools import multiprocessing import netaddr import os @@ -196,7 +197,7 @@ class LibvirtConnection(driver.ComputeDriver): def _test_connection(self): try: - self._wrapped_conn.getInfo() + self._wrapped_conn.getCapabilities() return True except libvirt.libvirtError as e: if e.get_error_code() == libvirt.VIR_ERR_SYSTEM_ERROR and \ @@ -398,10 +399,10 @@ class LibvirtConnection(driver.ComputeDriver): virt_dom = self._lookup_by_name(instance['name']) (image_service, image_id) = nova.image.get_image_service( - instance['image_ref']) + context, instance['image_ref']) base = image_service.show(context, image_id) (snapshot_image_service, snapshot_image_id) = \ - nova.image.get_image_service(image_href) + nova.image.get_image_service(context, image_href) snapshot = snapshot_image_service.show(context, snapshot_image_id) metadata = {'is_public': False, @@ -464,9 +465,10 @@ class LibvirtConnection(driver.ComputeDriver): # Clean up shutil.rmtree(temp_dir) + snapshot_ptr.delete(0) @exception.wrap_exception() - def reboot(self, instance, network_info): + def reboot(self, instance, network_info, reboot_type): """Reboot a virtual machine, given an instance reference. This method actually destroys and re-creates the domain to ensure the @@ -778,6 +780,10 @@ class LibvirtConnection(driver.ComputeDriver): if fs_format: utils.execute('mkfs', '-t', fs_format, target) + def _create_ephemeral(self, target, local_size, fs_label, os_type): + self._create_local(target, local_size) + disk.mkfs(os_type, fs_label, target) + def _create_swap(self, target, swap_gb): """Create a swap file of specified size""" self._create_local(target, swap_gb) @@ -866,9 +872,13 @@ class LibvirtConnection(driver.ComputeDriver): local_size=local_gb) for eph in driver.block_device_info_get_ephemerals(block_device_info): - self._cache_image(fn=self._create_local, + fn = functools.partial(self._create_ephemeral, + fs_label='ephemeral%d' % eph['num'], + os_type=inst.os_type) + self._cache_image(fn=fn, target=basepath(_get_eph_disk(eph)), - fname="local_%s" % eph['size'], + fname="ephemeral_%s_%s_%s" % + (eph['num'], eph['size'], inst.os_type), cow=FLAGS.use_cow_images, local_size=eph['size']) @@ -1102,6 +1112,11 @@ class LibvirtConnection(driver.ComputeDriver): nova_context.get_admin_context(), instance['id'], {'root_device_name': '/dev/' + self.default_root_device}) + if local_device: + db.instance_update( + nova_context.get_admin_context(), instance['id'], + {'default_local_device': '/dev/' + self.default_local_device}) + swap = driver.block_device_info_get_swap(block_device_info) if driver.swap_is_usable(swap): xml_info['swap_device'] = block_device.strip_dev( @@ -1110,6 +1125,9 @@ class LibvirtConnection(driver.ComputeDriver): not self._volume_in_mapping(self.default_swap_device, block_device_info)): xml_info['swap_device'] = self.default_swap_device + db.instance_update( + nova_context.get_admin_context(), instance['id'], + {'default_swap_device': '/dev/' + self.default_swap_device}) config_drive = False if instance.get('config_drive') or instance.get('config_drive_id'): @@ -1696,7 +1714,7 @@ class LibvirtConnection(driver.ComputeDriver): base = os.path.basename(info['path']) # Get image type and create empty disk image. instance_disk = os.path.join(instance_dir, base) - utils.execute('sudo', 'qemu-img', 'create', '-f', info['type'], + utils.execute('qemu-img', 'create', '-f', info['type'], instance_disk, info['local_gb']) # if image has kernel and ramdisk, just download @@ -1788,7 +1806,7 @@ class LibvirtConnection(driver.ComputeDriver): if disk_type == 'raw': size = int(os.path.getsize(path)) else: - out, err = utils.execute('sudo', 'qemu-img', 'info', path) + out, err = utils.execute('qemu-img', 'info', path) size = [i.split('(')[1].split()[0] for i in out.split('\n') if i.strip().find('virtual size') >= 0] size = int(size[0]) diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index dfa1deba7..c6253511e 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -338,8 +338,8 @@ class NWFilterFirewall(FirewallDriver): 'nova-allow-dhcp-server'] if FLAGS.use_ipv6: - networks = [network for (network, _m) in network_info if - network['gateway_v6']] + networks = [network for (network, info) in network_info if + info['gateway6']] if networks: instance_secgroup_filter_children.\ diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 0b7438011..077c32474 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -101,7 +101,7 @@ class LibvirtOpenVswitchDriver(VIFDriver): """VIF driver for Open vSwitch.""" def get_dev_name(_self, iface_id): - return "tap-" + iface_id[0:15] + return "tap" + iface_id[0:11] def plug(self, instance, network, mapping): iface_id = mapping['vif_uuid'] diff --git a/nova/virt/vmwareapi/fake.py b/nova/virt/vmwareapi/fake.py index 4c62d18bb..ae00bca0f 100644 --- a/nova/virt/vmwareapi/fake.py +++ b/nova/virt/vmwareapi/fake.py @@ -409,10 +409,10 @@ def fake_plug_vifs(*args, **kwargs): def fake_get_network(*args, **kwargs):
"""Fake get network."""
- return [{'type': 'fake'}]
+ return {'type': 'fake'}
-def fake_fetch_image(image, instance, **kwargs):
+def fake_fetch_image(context, image, instance, **kwargs):
"""Fakes fetch image call. Just adds a reference to the db for the file."""
ds_name = kwargs.get("datastore_name")
file_path = kwargs.get("file_path")
@@ -420,12 +420,12 @@ def fake_fetch_image(image, instance, **kwargs): _add_file(ds_file_path)
-def fake_upload_image(image, instance, **kwargs):
+def fake_upload_image(context, image, instance, **kwargs):
"""Fakes the upload of an image."""
pass
-def fake_get_vmdk_size_and_properties(image_id, instance):
+def fake_get_vmdk_size_and_properties(context, image_id, instance):
"""Fakes the file size and properties fetch for the image file."""
props = {"vmware_ostype": "otherGuest",
"vmware_adaptertype": "lsiLogic"}
diff --git a/nova/virt/vmwareapi/vif.py b/nova/virt/vmwareapi/vif.py index fb6548b34..9906b89e1 100644 --- a/nova/virt/vmwareapi/vif.py +++ b/nova/virt/vmwareapi/vif.py @@ -17,42 +17,35 @@ """VIF drivers for VMWare.""" -from nova import db from nova import exception from nova import flags from nova import log as logging -from nova import utils from nova.virt.vif import VIFDriver -from nova.virt.vmwareapi_conn import VMWareAPISession from nova.virt.vmwareapi import network_utils LOG = logging.getLogger("nova.virt.vmwareapi.vif") FLAGS = flags.FLAGS +FLAGS['vmwareapi_vlan_interface'].SetDefault('vmnic0') class VMWareVlanBridgeDriver(VIFDriver): """VIF Driver to setup bridge/VLAN networking using VMWare API.""" def plug(self, instance, network, mapping): + """Plug the VIF to specified instance using information passed. + Currently we are plugging the VIF(s) during instance creation itself. + We can use this method when we add support to add additional NIC to + an existing instance.""" + pass + + def ensure_vlan_bridge(self, session, network): """Create a vlan and bridge unless they already exist.""" vlan_num = network['vlan'] bridge = network['bridge'] - bridge_interface = network['bridge_interface'] + vlan_interface = FLAGS.vmwareapi_vlan_interface - # Open vmwareapi session - host_ip = FLAGS.vmwareapi_host_ip - host_username = FLAGS.vmwareapi_host_username - host_password = FLAGS.vmwareapi_host_password - if not host_ip or host_username is None or host_password is None: - raise Exception(_('Must specify vmwareapi_host_ip, ' - 'vmwareapi_host_username ' - 'and vmwareapi_host_password to use ' - 'connection_type=vmwareapi')) - session = VMWareAPISession(host_ip, host_username, host_password, - FLAGS.vmwareapi_api_retry_count) - vlan_interface = bridge_interface # Check if the vlan_interface physical network adapter exists on the # host. if not network_utils.check_if_vlan_interface_exists(session, @@ -92,4 +85,6 @@ class VMWareVlanBridgeDriver(VIFDriver): pgroup=pg_vlanid) def unplug(self, instance, network, mapping): + """Cleanup operations like deleting port group if no instance + is associated with it.""" pass diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index 82b5f7214..dd1c81196 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -39,8 +39,7 @@ def split_datastore_path(datastore_path): def get_vm_create_spec(client_factory, instance, data_store_name,
- network_name="vmnet0",
- os_type="otherGuest", network_ref=None):
+ vif_infos, os_type="otherGuest"):
"""Builds the VM Create spec."""
config_spec = client_factory.create('ns0:VirtualMachineConfigSpec')
config_spec.name = instance.name
@@ -61,14 +60,12 @@ def get_vm_create_spec(client_factory, instance, data_store_name, config_spec.numCPUs = int(instance.vcpus)
config_spec.memoryMB = int(instance.memory_mb)
- mac_address = None
- if instance['mac_addresses']:
- mac_address = instance['mac_addresses'][0]['address']
+ vif_spec_list = []
+ for vif_info in vif_infos:
+ vif_spec = create_network_spec(client_factory, vif_info)
+ vif_spec_list.append(vif_spec)
- nic_spec = create_network_spec(client_factory,
- network_name, mac_address)
-
- device_config_spec = [nic_spec]
+ device_config_spec = vif_spec_list
config_spec.deviceChange = device_config_spec
return config_spec
@@ -93,8 +90,7 @@ def create_controller_spec(client_factory, key): return virtual_device_config
-def create_network_spec(client_factory, network_name, mac_address,
- network_ref=None):
+def create_network_spec(client_factory, vif_info):
"""
Builds a config spec for the addition of a new network
adapter to the VM.
@@ -109,6 +105,9 @@ def create_network_spec(client_factory, network_name, mac_address, # NOTE(asomya): Only works on ESXi if the portgroup binding is set to
# ephemeral. Invalid configuration if set to static and the NIC does
# not come up on boot if set to dynamic.
+ network_ref = vif_info['network_ref']
+ network_name = vif_info['network_name']
+ mac_address = vif_info['mac_address']
backing = None
if (network_ref and
network_ref['type'] == "DistributedVirtualPortgroup"):
@@ -295,11 +294,8 @@ def get_dummy_vm_create_spec(client_factory, name, data_store_name): return config_spec
-def get_machine_id_change_spec(client_factory, mac, ip_addr, netmask,
- gateway, broadcast, dns):
+def get_machine_id_change_spec(client_factory, machine_id_str):
"""Builds the machine id change config spec."""
- machine_id_str = "%s;%s;%s;%s;%s;%s" % (mac, ip_addr, netmask,
- gateway, broadcast, dns)
virtual_machine_config_spec = \
client_factory.create('ns0:VirtualMachineConfigSpec')
diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index 07a6ba6ab..063b84a62 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -27,7 +27,6 @@ import urllib2 import uuid
from nova import context as nova_context
-from nova import db
from nova import exception
from nova import flags
from nova import log as logging
@@ -111,22 +110,6 @@ class VMWareVMOps(object): client_factory = self._session._get_vim().client.factory
service_content = self._session._get_vim().get_service_content()
- network = db.network_get_by_instance(nova_context.get_admin_context(),
- instance['id'])
-
- net_name = network['bridge']
-
- def _check_if_network_bridge_exists():
- network_ref = \
- network_utils.get_network_with_the_name(self._session,
- net_name)
- if network_ref is None:
- raise exception.NetworkNotFoundForBridge(bridge=net_name)
- return network_ref
-
- self.plug_vifs(instance, network_info)
- network_obj = _check_if_network_bridge_exists()
-
def _get_datastore_ref():
"""Get the datastore list and choose the first local storage."""
data_stores = self._session._call_method(vim_util, "get_objects",
@@ -157,7 +140,7 @@ class VMWareVMOps(object): repository.
"""
image_size, image_properties = \
- vmware_images.get_vmdk_size_and_properties(
+ vmware_images.get_vmdk_size_and_properties(context,
instance.image_ref, instance)
vmdk_file_size_in_kb = int(image_size) / 1024
os_type = image_properties.get("vmware_ostype", "otherGuest")
@@ -182,11 +165,36 @@ class VMWareVMOps(object): vm_folder_mor, res_pool_mor = _get_vmfolder_and_res_pool_mors()
+ def _check_if_network_bridge_exists(network_name):
+ network_ref = \
+ network_utils.get_network_with_the_name(self._session,
+ network_name)
+ if network_ref is None:
+ raise exception.NetworkNotFoundForBridge(bridge=network_name)
+ return network_ref
+
+ def _get_vif_infos():
+ vif_infos = []
+ for (network, mapping) in network_info:
+ mac_address = mapping['mac']
+ network_name = network['bridge']
+ if mapping.get('should_create_vlan'):
+ network_ref = self._vif_driver.ensure_vlan_bridge(
+ self._session, network)
+ else:
+ network_ref = _check_if_network_bridge_exists(network_name)
+ vif_infos.append({'network_name': network_name,
+ 'mac_address': mac_address,
+ 'network_ref': network_ref,
+ })
+ return vif_infos
+
+ vif_infos = _get_vif_infos()
+
# Get the create vm config spec
config_spec = vm_util.get_vm_create_spec(
client_factory, instance,
- data_store_name, net_name, os_type,
- network_obj)
+ data_store_name, vif_infos, os_type)
def _execute_create_vm():
"""Create VM on ESX host."""
@@ -204,8 +212,10 @@ class VMWareVMOps(object): _execute_create_vm()
- # Set the machine id for the VM for setting the IP
- self._set_machine_id(client_factory, instance)
+ # Set the machine.id parameter of the instance to inject
+ # the NIC configuration inside the VM
+ if FLAGS.flat_injected:
+ self._set_machine_id(client_factory, instance, network_info)
# Naming the VM files in correspondence with the VM instance name
# The flat vmdk file name
@@ -282,6 +292,7 @@ class VMWareVMOps(object): # Upload the -flat.vmdk file whose meta-data file we just created
# above
vmware_images.fetch_image(
+ context,
instance.image_ref,
instance,
host=self._session._host_ip,
@@ -448,6 +459,7 @@ class VMWareVMOps(object): # Upload the contents of -flat.vmdk file which has the disk data.
LOG.debug(_("Uploading image %s") % snapshot_name)
vmware_images.upload_image(
+ context,
snapshot_name,
instance,
os_type=os_type,
@@ -716,39 +728,45 @@ class VMWareVMOps(object): """Return link to instance's ajax console."""
return 'http://fakeajaxconsole/fake_url'
- def _set_machine_id(self, client_factory, instance):
+ def _set_machine_id(self, client_factory, instance, network_info):
"""
- Set the machine id of the VM for guest tools to pick up and change
- the IP.
+ Set the machine id of the VM for guest tools to pick up and reconfigure
+ the network interfaces.
"""
- admin_context = nova_context.get_admin_context()
vm_ref = self._get_vm_ref_from_the_name(instance.name)
if vm_ref is None:
raise exception.InstanceNotFound(instance_id=instance.id)
- network = db.network_get_by_instance(nova_context.get_admin_context(),
- instance['id'])
- mac_address = None
- if instance['mac_addresses']:
- mac_address = instance['mac_addresses'][0]['address']
-
- net_mask = network["netmask"]
- gateway = network["gateway"]
- broadcast = network["broadcast"]
- # TODO(vish): add support for dns2
- dns = network["dns1"]
-
- addresses = db.instance_get_fixed_addresses(admin_context,
- instance['id'])
- ip_addr = addresses[0] if addresses else None
+
+ machine_id_str = ''
+ for (network, info) in network_info:
+ # TODO(vish): add support for dns2
+ # TODO(sateesh): add support for injection of ipv6 configuration
+ ip_v4 = ip_v6 = None
+ if 'ips' in info and len(info['ips']) > 0:
+ ip_v4 = info['ips'][0]
+ if 'ip6s' in info and len(info['ip6s']) > 0:
+ ip_v6 = info['ip6s'][0]
+ if len(info['dns']) > 0:
+ dns = info['dns'][0]
+ else:
+ dns = ''
+
+ interface_str = "%s;%s;%s;%s;%s;%s" % \
+ (info['mac'],
+ ip_v4 and ip_v4['ip'] or '',
+ ip_v4 and ip_v4['netmask'] or '',
+ info['gateway'],
+ info['broadcast'],
+ dns)
+ machine_id_str = machine_id_str + interface_str + '#'
machine_id_change_spec = \
- vm_util.get_machine_id_change_spec(client_factory, mac_address,
- ip_addr, net_mask, gateway,
- broadcast, dns)
+ vm_util.get_machine_id_change_spec(client_factory, machine_id_str)
+
LOG.debug(_("Reconfiguring VM instance %(name)s to set the machine id "
"with ip - %(ip_addr)s") %
({'name': instance.name,
- 'ip_addr': ip_addr}))
+ 'ip_addr': ip_v4['ip']}))
reconfig_task = self._session._call_method(self._session._get_vim(),
"ReconfigVM_Task", vm_ref,
spec=machine_id_change_spec)
@@ -756,7 +774,7 @@ class VMWareVMOps(object): LOG.debug(_("Reconfigured VM instance %(name)s to set the machine id "
"with ip - %(ip_addr)s") %
({'name': instance.name,
- 'ip_addr': ip_addr}))
+ 'ip_addr': ip_v4['ip']}))
def _get_datacenter_name_and_ref(self):
"""Get the datacenter name and the reference."""
diff --git a/nova/virt/vmwareapi/vmware_images.py b/nova/virt/vmwareapi/vmware_images.py index f5f75dae2..53f2d372e 100644 --- a/nova/virt/vmwareapi/vmware_images.py +++ b/nova/virt/vmwareapi/vmware_images.py @@ -20,15 +20,13 @@ Utility functions for Image transfer. from nova import exception
from nova import flags
-import nova.image
+from nova.image import glance
from nova import log as logging
from nova.virt.vmwareapi import io_util
from nova.virt.vmwareapi import read_write_util
LOG = logging.getLogger("nova.virt.vmwareapi.vmware_images")
-FLAGS = flags.FLAGS
-
QUEUE_BUFFER_SIZE = 10
@@ -87,36 +85,10 @@ def start_transfer(read_file_handle, data_size, write_file_handle=None, write_file_handle.close()
-def fetch_image(image, instance, **kwargs):
- """Fetch an image for attaching to the newly created VM."""
- # Depending upon the image service, make appropriate image service call
- if FLAGS.image_service == "nova.image.glance.GlanceImageService":
- func = _get_glance_image
- elif FLAGS.image_service == "nova.image.s3.S3ImageService":
- func = _get_s3_image
- else:
- raise NotImplementedError(_("The Image Service %s is not implemented")
- % FLAGS.image_service)
- return func(image, instance, **kwargs)
-
-
-def upload_image(image, instance, **kwargs):
- """Upload the newly snapshotted VM disk file."""
- # Depending upon the image service, make appropriate image service call
- if FLAGS.image_service == "nova.image.glance.GlanceImageService":
- func = _put_glance_image
- elif FLAGS.image_service == "nova.image.s3.S3ImageService":
- func = _put_s3_image
- else:
- raise NotImplementedError(_("The Image Service %s is not implemented")
- % FLAGS.image_service)
- return func(image, instance, **kwargs)
-
-
-def _get_glance_image(image, instance, **kwargs):
+def fetch_image(context, image, instance, **kwargs):
"""Download image from the glance image server."""
LOG.debug(_("Downloading image %s from glance image server") % image)
- (glance_client, image_id) = nova.image.get_glance_client(image)
+ (glance_client, image_id) = glance.get_glance_client(context, image)
metadata, read_iter = glance_client.get_image(image_id)
read_file_handle = read_write_util.GlanceFileRead(read_iter)
file_size = int(metadata['size'])
@@ -132,17 +104,7 @@ def _get_glance_image(image, instance, **kwargs): LOG.debug(_("Downloaded image %s from glance image server") % image)
-def _get_s3_image(image, instance, **kwargs):
- """Download image from the S3 image server."""
- raise NotImplementedError
-
-
-def _get_local_image(image, instance, **kwargs):
- """Download image from the local nova compute node."""
- raise NotImplementedError
-
-
-def _put_glance_image(image, instance, **kwargs):
+def upload_image(context, image, instance, **kwargs):
"""Upload the snapshotted vm disk file to Glance image server."""
LOG.debug(_("Uploading image %s to the Glance image server") % image)
read_file_handle = read_write_util.VmWareHTTPReadFile(
@@ -152,7 +114,7 @@ def _put_glance_image(image, instance, **kwargs): kwargs.get("cookies"),
kwargs.get("file_path"))
file_size = read_file_handle.get_size()
- (glance_client, image_id) = nova.image.get_glance_client(image)
+ (glance_client, image_id) = glance.get_glance_client(context, image)
# The properties and other fields that we need to set for the image.
image_metadata = {"is_public": True,
"disk_format": "vmdk",
@@ -168,17 +130,7 @@ def _put_glance_image(image, instance, **kwargs): LOG.debug(_("Uploaded image %s to the Glance image server") % image)
-def _put_local_image(image, instance, **kwargs):
- """Upload the snapshotted vm disk file to the local nova compute node."""
- raise NotImplementedError
-
-
-def _put_s3_image(image, instance, **kwargs):
- """Upload the snapshotted vm disk file to S3 image server."""
- raise NotImplementedError
-
-
-def get_vmdk_size_and_properties(image, instance):
+def get_vmdk_size_and_properties(context, image, instance):
"""
Get size of the vmdk file that is to be downloaded for attach in spawn.
Need this to create the dummy virtual disk for the meta-data file. The
@@ -186,12 +138,9 @@ def get_vmdk_size_and_properties(image, instance): """
LOG.debug(_("Getting image size for the image %s") % image)
- if FLAGS.image_service == "nova.image.glance.GlanceImageService":
- (glance_client, image_id) = nova.image.get_glance_client(image)
- meta_data = glance_client.get_image_meta(image_id)
- size, properties = meta_data["size"], meta_data["properties"]
- elif FLAGS.image_service == "nova.image.s3.S3ImageService":
- raise NotImplementedError
+ (glance_client, image_id) = glance.get_glance_client(context, image)
+ meta_data = glance_client.get_image_meta(image_id)
+ size, properties = meta_data["size"], meta_data["properties"]
LOG.debug(_("Got image size of %(size)s for the image %(image)s") %
locals())
return size, properties
diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index 243ee64f5..fa89a8f45 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -133,7 +133,7 @@ class VMWareESXConnection(driver.ComputeDriver): """Create snapshot from a running VM instance."""
self._vmops.snapshot(context, instance, name)
- def reboot(self, instance, network_info):
+ def reboot(self, instance, network_info, reboot_type):
"""Reboot VM instance."""
self._vmops.reboot(instance, network_info)
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index efbea7076..302238c98 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -31,12 +31,10 @@ import urllib import uuid from xml.dom import minidom -import glance.client from nova import db from nova import exception from nova import flags -import nova.image -from nova.image import glance as glance_image_service +from nova.image import glance from nova import log as logging from nova import utils from nova.compute import instance_types @@ -383,8 +381,7 @@ class VMHelper(HelperBase): os_type = instance.os_type or FLAGS.default_os_type - glance_host, glance_port = \ - glance_image_service.pick_glance_api_server() + glance_host, glance_port = glance.pick_glance_api_server() params = {'vdi_uuids': vdi_uuids, 'image_id': image_id, 'glance_host': glance_host, @@ -447,8 +444,7 @@ class VMHelper(HelperBase): # pass them as arguments uuid_stack = [str(uuid.uuid4()) for i in xrange(2)] - glance_host, glance_port = \ - glance_image_service.pick_glance_api_server() + glance_host, glance_port = glance.pick_glance_api_server() params = {'image_id': image, 'glance_host': glance_host, 'glance_port': glance_port, @@ -546,7 +542,7 @@ class VMHelper(HelperBase): else: sr_ref = safe_find_sr(session) - glance_client, image_id = nova.image.get_glance_client(image) + glance_client, image_id = glance.get_glance_client(context, image) glance_client.set_auth_token(getattr(context, 'auth_token', None)) meta, image_file = glance_client.get_image(image_id) virtual_size = int(meta['size']) @@ -606,7 +602,7 @@ class VMHelper(HelperBase): raise e @classmethod - def determine_disk_image_type(cls, instance): + def determine_disk_image_type(cls, instance, context): """Disk Image Types are used to determine where the kernel will reside within an image. To figure out which type we're dealing with, we use the following rules: @@ -639,7 +635,8 @@ class VMHelper(HelperBase): 'vhd': ImageType.DISK_VHD, 'iso': ImageType.DISK_ISO} image_ref = instance.image_ref - glance_client, image_id = nova.image.get_glance_client(image_ref) + glance_client, image_id = glance.get_glance_client(context, + image_ref) meta = glance_client.get_image_meta(image_id) disk_format = meta['disk_format'] try: diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index c5f105f40..6b56d668e 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -135,7 +135,7 @@ class VMOps(object): self._session.call_xenapi('VM.start', vm_ref, False, False) def _create_disks(self, context, instance): - disk_image_type = VMHelper.determine_disk_image_type(instance) + disk_image_type = VMHelper.determine_disk_image_type(instance, context) vdis = VMHelper.fetch_image(context, self._session, instance, instance.image_ref, instance.user_id, instance.project_id, @@ -176,7 +176,7 @@ class VMOps(object): power_state.SHUTDOWN) return - disk_image_type = VMHelper.determine_disk_image_type(instance) + disk_image_type = VMHelper.determine_disk_image_type(instance, context) kernel = None ramdisk = None try: @@ -188,9 +188,16 @@ class VMOps(object): ramdisk = VMHelper.fetch_image(context, self._session, instance, instance.ramdisk_id, instance.user_id, instance.project_id, ImageType.RAMDISK)[0] - # Create the VM ref and attach the first disk - first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', - vdis[0]['vdi_uuid']) + + # NOTE(jk0): Since vdi_type may contain either 'os' or 'swap', we + # need to ensure that the 'swap' VDI is not chosen as the mount + # point for file injection. + first_vdi_ref = None + for vdi in vdis: + if vdi.get('vdi_type') != 'swap': + # Create the VM ref and attach the first disk + first_vdi_ref = self._session.call_xenapi( + 'VDI.get_by_uuid', vdi['vdi_uuid']) vm_mode = instance.vm_mode and instance.vm_mode.lower() if vm_mode == 'pv': @@ -246,6 +253,8 @@ class VMOps(object): self.create_vifs(vm_ref, instance, network_info) self.inject_network_info(instance, network_info, vm_ref) + self.inject_hostname(instance, vm_ref, instance['hostname']) + return vm_ref def _attach_disks(self, instance, disk_image_type, vm_ref, first_vdi_ref, @@ -617,10 +626,15 @@ class VMOps(object): str(new_disk_size)) LOG.debug(_("Resize instance %s complete") % (instance.name)) - def reboot(self, instance): + def reboot(self, instance, reboot_type): """Reboot VM instance.""" vm_ref = self._get_vm_opaque_ref(instance) - task = self._session.call_xenapi('Async.VM.clean_reboot', vm_ref) + + if reboot_type == "HARD": + task = self._session.call_xenapi('Async.VM.hard_reboot', vm_ref) + else: + task = self._session.call_xenapi('Async.VM.clean_reboot', vm_ref) + self._session.wait_for_task(task, instance.id) def get_agent_version(self, instance, timeout=None): @@ -1146,6 +1160,16 @@ class VMOps(object): resp = self._make_plugin_call('agent', 'resetnetwork', instance, '', args, vm_ref) + def inject_hostname(self, instance, vm_ref, hostname): + """Inject the hostname of the instance into the xenstore.""" + if instance.os_type == "windows": + # NOTE(jk0): Windows hostnames can only be <= 15 chars. + hostname = hostname[:15] + + logging.debug(_("injecting hostname to xs for vm: |%s|"), vm_ref) + self._session.call_xenapi_request("VM.add_to_xenstore_data", + (vm_ref, "vm-data/hostname", hostname)) + def list_from_xenstore(self, vm, path): """ Runs the xenstore-ls command to get a listing of all records diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 0d23e7689..f6dbc19f8 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -203,9 +203,9 @@ class XenAPIConnection(driver.ComputeDriver): """ Create snapshot from a running VM instance """ self._vmops.snapshot(context, instance, image_id) - def reboot(self, instance, network_info): + def reboot(self, instance, network_info, reboot_type): """Reboot VM instance""" - self._vmops.reboot(instance) + self._vmops.reboot(instance, reboot_type) def set_admin_password(self, instance, new_pass): """Set the root/admin password on the VM instance""" diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 35e3ea8d0..e5bb498ed 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -221,7 +221,14 @@ class VolumeDriver(object): class AOEDriver(VolumeDriver): - """Implements AOE specific volume commands.""" + """WARNING! Deprecated. This driver will be removed in Essex. Its use + is not recommended. + + Implements AOE specific volume commands.""" + + def __init__(self, *args, **kwargs): + LOG.warn(_("AOEDriver is deprecated and will be removed in Essex")) + super(AOEDriver, self).__init__(*args, **kwargs) def ensure_export(self, context, volume): # NOTE(vish): we depend on vblade-persist for recreating exports diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance index a06312890..1a9ac37e9 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance @@ -252,7 +252,11 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type, # NOTE(dprince): We need to resend any existing Glance meta/property # headers so they are preserved in Glance. We obtain them here with a # HEAD request. - conn.request('HEAD', '/v1/images/%s' % image_id) + conn.putrequest('HEAD', '/v1/images/%s' % image_id) + if auth_token: + conn.putheader('x-auth-token', auth_token) + conn.endheaders() + resp = conn.getresponse() if resp.status != httplib.OK: raise Exception("Unexpected response from Glance %i" % resp.status) @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-02-07 12:45+0000\n" -"Last-Translator: David Pravec <Unknown>\n" +"PO-Revision-Date: 2011-08-23 11:22+0000\n" +"Last-Translator: Thierry Carrez <thierry.carrez+lp@gmail.com>\n" "Language-Team: Czech <cs@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-03 04:43+0000\n" -"X-Generator: Launchpad (build 13573)\n" +"X-Launchpad-Export-Date: 2011-08-24 04:47+0000\n" +"X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2789,21 +2789,3 @@ msgstr "" #, python-format msgid "Removing user %(user)s from project %(project)s" msgstr "" - -#, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "AMQP server na %s:%d není dosažitelný. Zkusím znovu za %d sekund." - -#, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" -#~ msgstr "" -#~ "%s\n" -#~ "Příkaz: %s\n" -#~ "Vrácená hodnota: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-06-06 07:58+0000\n" -"Last-Translator: Christian Berendt <Unknown>\n" +"PO-Revision-Date: 2011-08-23 11:23+0000\n" +"Last-Translator: Thierry Carrez <thierry.carrez+lp@gmail.com>\n" "Language-Team: German <de@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" -"X-Generator: Launchpad (build 13573)\n" +"X-Launchpad-Export-Date: 2011-08-24 04:47+0000\n" +"X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2798,42 +2798,6 @@ msgstr "" msgid "Removing user %(user)s from project %(project)s" msgstr "" -#, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" -#~ msgstr "" -#~ "%s\n" -#~ "Kommando: %s\n" -#~ "Exit Code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" - -#, python-format -#~ msgid "(%s) publish (key: %s) %s" -#~ msgstr "(%s) öffentlich (Schlüssel: %s) %s" - -#, python-format -#~ msgid "Getting from %s: %s" -#~ msgstr "Beziehe von %s: %s" - -#, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "" -#~ "Der AMQP server %s:%d ist nicht erreichbar. Erneuter Versuch in %d Sekunden." - -#, python-format -#~ msgid "volume %s: creating lv of size %sG" -#~ msgstr "Volume %s: erstelle LV mit %sG" - -#, python-format -#~ msgid "Data store %s is unreachable. Trying again in %d seconds." -#~ msgstr "" -#~ "Datastore %s ist nicht erreichbar. Versuche es erneut in %d Sekunden." - #~ msgid "Full set of FLAGS:" #~ msgstr "Alle vorhandenen FLAGS:" @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-08-01 03:23+0000\n" -"Last-Translator: Juan Alfredo Salas Santillana <Unknown>\n" +"PO-Revision-Date: 2011-08-23 11:22+0000\n" +"Last-Translator: Thierry Carrez <thierry.carrez+lp@gmail.com>\n" "Language-Team: Spanish <es@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" -"X-Generator: Launchpad (build 13573)\n" +"X-Launchpad-Export-Date: 2011-08-24 04:47+0000\n" +"X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2839,210 +2839,9 @@ msgid "Removing user %(user)s from project %(project)s" msgstr "Eliminando el usuario %(user)s del proyecto %(project)s" #, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" -#~ msgstr "" -#~ "%s\n" -#~ "Comando: %s\n" -#~ "Código de salida: %s\n" -#~ "Stdout: %s\n" -#~ "Stderr: %r" - -#, python-format -#~ msgid "(%s) publish (key: %s) %s" -#~ msgstr "(%s) públicar (clave: %s) %s" - -#, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "" -#~ "El servidor AMQP en %s:%d no se puede alcanzar. Se reintentará en %d " -#~ "segundos." - -#, python-format -#~ msgid "Binding %s to %s with key %s" -#~ msgstr "Asociando %s a %s con clave %s" - -#, python-format -#~ msgid "Getting from %s: %s" -#~ msgstr "Obteniendo desde %s: %s" - -#, python-format -#~ msgid "Starting %s node" -#~ msgstr "Inciando nodo %s" - -#, python-format -#~ msgid "Data store %s is unreachable. Trying again in %d seconds." -#~ msgstr "" -#~ "El almacen de datos %s es inalcanzable. Reintentandolo en %d segundos." - -#, python-format #~ msgid "Serving %s" #~ msgstr "Sirviendo %s" -#, python-format -#~ msgid "Couldn't get IP, using 127.0.0.1 %s" -#~ msgstr "No puedo obtener IP, usando 127.0.0.1 %s" - -#, python-format -#~ msgid "" -#~ "Access key %s has had %d failed authentications and will be locked out for " -#~ "%d minutes." -#~ msgstr "" -#~ "La clave de acceso %s ha tenido %d fallos de autenticación y se bloqueará " -#~ "por %d minutos." - -#, python-format -#~ msgid "arg: %s\t\tval: %s" -#~ msgstr "arg: %s \t \t val: %s" - -#, python-format -#~ msgid "Authenticated Request For %s:%s)" -#~ msgstr "Solicitud de autenticación para %s:%s" - -#, python-format -#~ msgid "Adding role %s to user %s for project %s" -#~ msgstr "Añadiendo rol %s al usuario %s para el proyecto %s" - -#, python-format -#~ msgid "Removing role %s from user %s for project %s" -#~ msgstr "Eliminando rol %s del usuario %s para el proyecto %s" - -#, python-format -#~ msgid "Unauthorized request for controller=%s and action=%s" -#~ msgstr "Solicitud no autorizada para controller=%s y action=%s" - -#, python-format -#~ msgid "Getting x509 for user: %s on project: %s" -#~ msgstr "Obteniendo x509 para el usuario: %s en el proyecto %s" - -#, python-format -#~ msgid "Create project %s managed by %s" -#~ msgstr "Creación del proyecto %s gestionada por %s" - -#, python-format -#~ msgid "Removing user %s from project %s" -#~ msgstr "Eliminando usuario %s del proyecto %s" - -#, python-format -#~ msgid "Adding user %s to project %s" -#~ msgstr "Añadiendo usuario %s al proyecto %s" - -#, python-format -#~ msgid "Unsupported API request: controller = %s,action = %s" -#~ msgstr "Solicitud de API no soportada: controller=%s,action=%s" - -#, python-format -#~ msgid "Associate address %s to instance %s" -#~ msgstr "Asociar dirección %s a la instancia %s" - -#, python-format -#~ msgid "Attach volume %s to instacne %s at %s" -#~ msgstr "Asociar volumen %s a la instancia %s en %s" - -#, python-format -#~ msgid "Registered image %s with id %s" -#~ msgstr "Registrada imagen %s con id %s" - -#, python-format -#~ msgid "User %s is already a member of the group %s" -#~ msgstr "El usuario %s ya es miembro de el grupo %s" - -#, python-format -#~ msgid "User %s is not a member of project %s" -#~ msgstr "El usuario %s no es miembro del proyecto %s" - -#, python-format -#~ msgid "failed authorization: no project named %s (user=%s)" -#~ msgstr "" -#~ "fallo de autorización: no existe proyecto con el nombre %s (usuario=%s)" - -#, python-format -#~ msgid "Failed authorization: user %s not admin and not member of project %s" -#~ msgstr "" -#~ "Fallo de autorización: el usuario %s no es administrador y no es miembro del " -#~ "proyecto %s" - -#, python-format -#~ msgid "Created user %s (admin: %r)" -#~ msgstr "Creado usuario %s (administrador: %r)" - -#, python-format -#~ msgid "Created project %s with manager %s" -#~ msgstr "Proyecto %s creado con administrador %s" - -#, python-format -#~ msgid "Removing role %s from user %s on project %s" -#~ msgstr "Eliminando rol %s al usuario %s en el proyecto %s" - -#, python-format -#~ msgid "Adding role %s to user %s in project %s" -#~ msgstr "Añadiendo rol %s al usuario %s en el proyecto %s" - -#, python-format -#~ msgid "Remove user %s from project %s" -#~ msgstr "Eliminar usuario %s del proyecto %s" - -#, python-format -#~ msgid "Admin status set to %r for user %s" -#~ msgstr "El estado del administrador se ha fijado a %r para el usuario %s" - -#, python-format -#~ msgid "Going to try and terminate %s" -#~ msgstr "Se va a probar y terminar %s" - -#, python-format -#~ msgid "Casting to scheduler for %s/%s's instance %s" -#~ msgstr "Llamando al planificar para %s/%s insntancia %s" - -#, python-format -#~ msgid "Quota exceeeded for %s, tried to run %s instances" -#~ msgstr "Quota superada por %s, intentando lanzar %s instancias" - -#, python-format -#~ msgid "check_instance_lock: arguments: |%s| |%s| |%s|" -#~ msgstr "check_instance_lock: arguments: |%s| |%s| |%s|" - -#, python-format -#~ msgid "Input partition size not evenly divisible by sector size: %d / %d" -#~ msgstr "" -#~ "El tamaño de la partición de entrada no es divisible de forma uniforme por " -#~ "el tamaño del sector: %d / %d" - -#, python-format -#~ msgid "Bytes for local storage not evenly divisible by sector size: %d / %d" -#~ msgstr "" -#~ "Los bytes del almacenamiento local no son divisibles de forma uniforme por " -#~ "el tamaño del sector: %d / %d" - -#, python-format -#~ msgid "volume %s: creating lv of size %sG" -#~ msgstr "volumen %s: creando lv de tamaño %sG" - -#, python-format -#~ msgid "Disassociating address %s" -#~ msgstr "Desasociando la dirección %s" - -#, python-format -#~ msgid "trying to reboot a non-running instance: %s (state: %s excepted: %s)" -#~ msgstr "" -#~ "intentando reiniciar una instancia que no está en ejecución: %s (estado: %s " -#~ "esperado: %s)" - -#, python-format -#~ msgid "" -#~ "trying to snapshot a non-running instance: %s (state: %s excepted: %s)" -#~ msgstr "" -#~ "intentando crear un snapshot de una instancia que no está en ejecución: %s " -#~ "(estado: %s esperado: %s)" - -#, python-format -#~ msgid "Detach volume %s from mountpoint %s on instance %s" -#~ msgstr "Desvinculando volumen %s del punto de montaje %s en la instancia %s" - #~ msgid "unexpected exception getting connection" #~ msgstr "excepción inexperada al obtener la conexión" @@ -3050,10 +2849,6 @@ msgstr "Eliminando el usuario %(user)s del proyecto %(project)s" #~ msgstr "error inesperado durante la actualización" #, python-format -#~ msgid "Cannot get blockstats for \"%s\" on \"%s\"" -#~ msgstr "No puedo obtener estadísticas del bloque para \"%s\" en \"%s\"" - -#, python-format #~ msgid "updating %s..." #~ msgstr "actualizando %s..." @@ -3062,284 +2857,13 @@ msgstr "Eliminando el usuario %(user)s del proyecto %(project)s" #~ msgstr "Encontrada interfaz: %s" #, python-format -#~ msgid "Cannot get ifstats for \"%s\" on \"%s\"" -#~ msgstr "No puedo obtener estadísticas de la interfaz para \"%s\" en \"%s\"" - -#, python-format -#~ msgid "No instance for id %s" -#~ msgstr "No hay instancia con id %s" - -#, python-format -#~ msgid "no keypair for user %s, name %s" -#~ msgstr "no hay par de claves para el usuario %s, nombre %s" - -#, python-format -#~ msgid "No service for %s, %s" -#~ msgstr "No hay servicio para %s, %s" - -#, python-format -#~ msgid "No volume for id %s" -#~ msgstr "No hay volumen para el id %s" - -#, python-format -#~ msgid "No security group named %s for project: %s" -#~ msgstr "No hay un grupo de seguridad con nombre %s para el proyecto: %s" - -#, python-format -#~ msgid "Parallax returned HTTP error %d from request for /images/detail" -#~ msgstr "" -#~ "Parallax ha devuelto un error HTTP %d para la petición para /images/detail" - -#, python-format -#~ msgid "Parallax returned HTTP error %d from request for /images" -#~ msgstr "Parallax ha devuelto un error HTTP %d a la petición para /images" - -#, python-format -#~ msgid "IP %s leased to bad mac %s vs %s" -#~ msgstr "IP %s asociada a una mac incorrecta %s vs %s" - -#, python-format -#~ msgid "Unauthorized attempt to get object %s from bucket %s" -#~ msgstr "Intento no autorizado de obtener el objeto %s en el cubo %s" - -#, python-format -#~ msgid "Getting object: %s / %s" -#~ msgstr "Obteniendo objeto: %s / %s" - -#, python-format -#~ msgid "Putting object: %s / %s" -#~ msgstr "Colocando objeto: %s / %s" - -#, python-format -#~ msgid "Unauthorized attempt to upload object %s to bucket %s" -#~ msgstr "Intento no autorizado de subir el objeto %s al cubo %s" - -#, python-format -#~ msgid "Deleting object: %s / %s" -#~ msgstr "Eliminando objeto: %s / %s" - -#, python-format -#~ msgid "Toggling publicity flag of image %s %r" -#~ msgstr "Cambiando los atributos de publicidad de la imagen %s %r" - -#, python-format -#~ msgid "Creating disk for %s by attaching disk file %s" -#~ msgstr "" -#~ "Creando disco para %s a través de la asignación del fichero de disco %s" - -#, python-format -#~ msgid "WMI job succeeded: %s, Elapsed=%s " -#~ msgstr "Trabajo WMI ha tenido exito: %s, Transcurrido=%s " - -#, python-format -#~ msgid "Created switch port %s on switch %s" -#~ msgstr "Creado puerto %s en el switch %s" - -#, python-format -#~ msgid "instance %s: deleting instance files %s" -#~ msgstr "instancia %s: eliminando los ficheros de la instancia %s" - -#, python-format -#~ msgid "Finished retreving %s -- placed in %s" -#~ msgstr "Finalizada la obtención de %s -- coloado en %s" - -#, python-format -#~ msgid "Failed to change vm state of %s to %s" -#~ msgstr "Fallo al cambiar el estado de la vm de %s a %s" - -#, python-format -#~ msgid "Successfully changed vm state of %s to %s" -#~ msgstr "Cambio de estado de la vm con éxito de %s a %s" - -#, python-format -#~ msgid "" -#~ "Got Info for vm %s: state=%s, mem=%s, num_cpu=%s, " -#~ "cpu_time=%s" -#~ msgstr "" -#~ "Obtenida información para vm %s: state=%s, mem=%s, num_cpu=%s, cpu_time=%s" - -#, python-format -#~ msgid "instance %s: ignoring error injecting data into image %s (%s)" -#~ msgstr "" -#~ "instancia %s: ignorando el error al inyectar datos en la imagen %s (%s)" - -#, python-format -#~ msgid "Contents of file %s: %r" -#~ msgstr "Contenidos del fichero %s: %r" - -#, python-format -#~ msgid "instance %s: injecting net into image %s" -#~ msgstr "instancia %s: inyectando red en la imagen %s" - -#, python-format -#~ msgid "instance %s: injecting key into image %s" -#~ msgstr "instancia %s: inyectando clave en la imagen %s" - -#, python-format -#~ msgid "data: %r, fpath: %r" -#~ msgstr "datos: %r, fpath: %r" - -#, python-format -#~ msgid "Task [%s] %s status: %s %s" -#~ msgstr "Tarea [%s] %s estado: %s %s" - -#, python-format -#~ msgid "Task [%s] %s status: success %s" -#~ msgstr "Tarea [%s] %s estado: éxito %s" - -#, python-format -#~ msgid "Calling %s %s" -#~ msgstr "Llamando %s %s" - -#, python-format -#~ msgid "%s: _db_content => %s" -#~ msgstr "%s: _db_content => %s" - -#, python-format -#~ msgid "Created VBD %s for VM %s, VDI %s." -#~ msgstr "Creado VBD %s for VM %s, VDI %s." - -#, python-format -#~ msgid "Creating VBD for VM %s, VDI %s ... " -#~ msgstr "Creando VBD para VM %s, VDI %s... " - -#, python-format -#~ msgid "Created VIF %s for VM %s, network %s." -#~ msgstr "Creado VIF %s para VM %s, red %s." - -#, python-format -#~ msgid "Creating VIF for VM %s, network %s." -#~ msgstr "Creando VIF para VM %s, red %s." - -#, python-format -#~ msgid "Created VM %s as %s." -#~ msgstr "Creada VM %s cómo %s" - -#, python-format -#~ msgid "Asking xapi to upload %s as '%s'" -#~ msgstr "Solicitando a xapi la subida de %s cómo %s'" - -#, python-format -#~ msgid "VHD %s has parent %s" -#~ msgstr "VHD %s tiene cómo padre a %s" - -#, python-format -#~ msgid "Asking xapi to fetch %s as %s" -#~ msgstr "Solicitando a xapi obtener %s cómo %s" - -#, python-format #~ msgid "PV Kernel in VDI:%d" #~ msgstr "PV Kernel en VDI:%d" #, python-format -#~ msgid "Unexpected number of VDIs (%s) found for VM %s" -#~ msgstr "Número no esperado de VDIs (%s) encontrados para VM %s" - -#, python-format -#~ msgid "Parent %s doesn't match original parent %s, waiting for coalesce..." -#~ msgstr "" -#~ "El padre %s no concuerda con el padre original %s, esperando la unión..." - -#, python-format -#~ msgid "suspend: instance not present %s" -#~ msgstr "suspendido: instancia no encontrada: %s" - -#, python-format -#~ msgid "Introduced %s as %s." -#~ msgstr "Introducido %s cómo %s." - -#, python-format -#~ msgid "resume: instance not present %s" -#~ msgstr "reanudar: instancia no encontrada %s" - -#, python-format -#~ msgid "Instance not found %s" -#~ msgstr "instancia no encontrada %s" - -#, python-format -#~ msgid "Ignoring exception %s when getting PBDs for %s" -#~ msgstr "Ignorando excepción %s al obtener PBDs de %s" - -#, python-format -#~ msgid "Unable to create VDI on SR %s for instance %s" -#~ msgstr "Inpoisble crear VDI en SR %s para la instancia %s" - -#, python-format -#~ msgid "Unable to obtain target information %s, %s" -#~ msgstr "Imposible obtener información del destino %s, %s" - -#, python-format -#~ msgid "Ignoring exception %s when forgetting SR %s" -#~ msgstr "Ignorando excepción %s al olvidar SR %s" - -#, python-format -#~ msgid "Ignoring exception %s when unplugging PBD %s" -#~ msgstr "Ignorando excepción %s al desconectar PBD %s" - -#, python-format -#~ msgid "Attach_volume: %s, %s, %s" -#~ msgstr "Attach_volume: %s, %s, %s" - -#, python-format -#~ msgid "Unable to use SR %s for instance %s" -#~ msgstr "Imposible utilizar SR %s para la instancia %s" - -#, python-format -#~ msgid "Mountpoint %s attached to instance %s" -#~ msgstr "Punto de montaje %s unido a la instancia %s" - -#, python-format -#~ msgid "Detach_volume: %s, %s" -#~ msgstr "Detach_volume: %s, %s" - -#, python-format -#~ msgid "Mountpoint %s detached from instance %s" -#~ msgstr "Punto d emontaje %s desasociado de la instancia %s" - -#, python-format -#~ msgid "Quota exceeeded for %s, tried to create %sG volume" -#~ msgstr "Quota excedida para %s, intentando crear el volumen %sG" - -#, python-format #~ msgid "Volume quota exceeded. You cannot create a volume of size %s" #~ msgstr "Quota de volumen superada. No puedes crear un volumen de tamaño %s" -#, python-format -#~ msgid "instance %s: attach failed %s, removing" -#~ msgstr "instalación %s: asociación fallida %s, eliminando" - -#, python-format -#~ msgid "instance %s: attaching volume %s to %s" -#~ msgstr "instancia %s: asociando volumen %s a %s" - -#, python-format -#~ msgid "Snapshotting VM %s with label '%s'..." -#~ msgstr "Creando snapshot de la VM %s con la etiqueta '%s'..." - -#, python-format -#~ msgid "Created snapshot %s from VM %s." -#~ msgstr "Creando snapshot %s de la VM %s" - -#, python-format -#~ msgid "Unable to Snapshot %s: %s" -#~ msgstr "Incapaz de realizar snapshot %s: %s" - -#, python-format -#~ msgid "Adding sitewide role %s to user %s" -#~ msgstr "Añadiendo rol global %s al usuario %s" - -#, python-format -#~ msgid "Removing sitewide role %s from user %s" -#~ msgstr "Eliminando rol global %s del usuario %s" - -#, python-format -#~ msgid "Del: disk %s vm %s" -#~ msgstr "Del: disco %s vm %s" - -#, python-format -#~ msgid "Spawning VM %s created %s." -#~ msgstr "Iniciando VM %s creado %s." - #~ msgid "No such process" #~ msgstr "No existe el proceso" @@ -14,7 +14,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-22 04:48+0000\n" +"X-Launchpad-Export-Date: 2011-08-23 05:21+0000\n" "X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-05-10 10:26+0000\n" -"Last-Translator: Akira YOSHIYAMA <Unknown>\n" +"PO-Revision-Date: 2011-08-23 11:22+0000\n" +"Last-Translator: Thierry Carrez <thierry.carrez+lp@gmail.com>\n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" -"X-Generator: Launchpad (build 13573)\n" +"X-Launchpad-Export-Date: 2011-08-24 04:47+0000\n" +"X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2841,44 +2841,6 @@ msgid "Removing user %(user)s from project %(project)s" msgstr "ユーザ %(user)s をプロジェクト %(project)s から削除します。" #, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" -#~ msgstr "" -#~ "%s\n" -#~ "コマンド: %s\n" -#~ "終了コード: %s\n" -#~ "標準出力: %r\n" -#~ "標準エラー出力: %r" - -#, python-format -#~ msgid "(%s) publish (key: %s) %s" -#~ msgstr "(%s) パブリッシュ (key: %s) %s" - -#, python-format -#~ msgid "Binding %s to %s with key %s" -#~ msgstr "%s を %s にキー %s でバインドします。" - -#, python-format -#~ msgid "Getting from %s: %s" -#~ msgstr "%s から %s を取得" - -#, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "AMQPサーバ %s:%d に接続できません。 %d 秒後に再度試みます。" - -#, python-format -#~ msgid "Starting %s node" -#~ msgstr "ノード %s を開始します。" - -#, python-format -#~ msgid "Data store %s is unreachable. Trying again in %d seconds." -#~ msgstr "データストア %s に接続できません。 %d 秒後に再接続します。" - -#, python-format #~ msgid "Serving %s" #~ msgstr "%s サービスの開始" @@ -2890,180 +2852,12 @@ msgstr "ユーザ %(user)s をプロジェクト %(project)s から削除しま #~ msgstr "pidfile %s が存在しません。デーモンは実行中ですか?\n" #, python-format -#~ msgid "Couldn't get IP, using 127.0.0.1 %s" -#~ msgstr "IPを取得できません。127.0.0.1 を %s として使います。" - -#, python-format -#~ msgid "" -#~ "Access key %s has had %d failed authentications and will be locked out for " -#~ "%d minutes." -#~ msgstr "アクセスキー %s は %d 回認証に失敗したため、%d 分間ロックされます。" - -#, python-format -#~ msgid "Authenticated Request For %s:%s)" -#~ msgstr "リクエストを認証しました: %s:%s" - -#, python-format -#~ msgid "arg: %s\t\tval: %s" -#~ msgstr "引数(arg): %s\t値(val): %s" - -#, python-format -#~ msgid "Unauthorized request for controller=%s and action=%s" -#~ msgstr "許可されていないリクエスト: controller=%s, action %sです。" - -#, python-format -#~ msgid "Adding role %s to user %s for project %s" -#~ msgstr "Adding role: ロール %s をユーザ %s、プロジェクト %s に追加します。" - -#, python-format -#~ msgid "Adding sitewide role %s to user %s" -#~ msgstr "Adding sitewide role: サイトワイドのロール %s をユーザ %s に追加します。" - -#, python-format -#~ msgid "Removing role %s from user %s for project %s" -#~ msgstr "Removing role: ロール %s をユーザ %s プロジェクト %s から削除します。" - -#, python-format -#~ msgid "Removing sitewide role %s from user %s" -#~ msgstr "Removing sitewide role: サイトワイドのロール %s をユーザ %s から削除します。" - -#, python-format -#~ msgid "Getting x509 for user: %s on project: %s" -#~ msgstr "Getting X509: x509の取得: ユーザ %s, プロジェクト %s" - -#, python-format -#~ msgid "Create project %s managed by %s" -#~ msgstr "Create project: プロジェクト %s (%s により管理される)を作成します。" - -#, python-format -#~ msgid "Adding user %s to project %s" -#~ msgstr "Adding user: ユーザ %s をプロジェクト %s に追加します。" - -#, python-format -#~ msgid "Removing user %s from project %s" -#~ msgstr "Removing user: ユーザ %s をプロジェクト %s から削除します。" - -#, python-format -#~ msgid "Unsupported API request: controller = %s,action = %s" -#~ msgstr "サポートされていないAPIリクエストです。 controller = %s,action = %s" - -#, python-format -#~ msgid "Attach volume %s to instacne %s at %s" -#~ msgstr "Attach volume: ボリューム%s をインスタンス %s にデバイス %s でアタッチします。" - -#, python-format -#~ msgid "Associate address %s to instance %s" -#~ msgstr "Associate address: アドレス %s をインスタンス %s に関連付けます。" - -#, python-format -#~ msgid "Registered image %s with id %s" -#~ msgstr "Registered image: イメージ %s をid %s で登録します。" - -#, python-format -#~ msgid "User %s is already a member of the group %s" -#~ msgstr "ユーザ %s は既にグループ %s のメンバーです。" - -#, python-format -#~ msgid "failed authorization: no project named %s (user=%s)" -#~ msgstr "Failed authorization: 認証に失敗しました。プロジェクト名 %s (ユーザ = %s) は存在しません。" - -#, python-format -#~ msgid "Failed authorization: user %s not admin and not member of project %s" -#~ msgstr "" -#~ "Failed authorization: 認証に失敗しました: ユーザ %s は管理者ではなくかつプロジェクト %s のメンバーではありません。" - -#, python-format -#~ msgid "User %s is not a member of project %s" -#~ msgstr "ユーザ %s はプロジェクト %s のメンバーではありません。" - -#, python-format -#~ msgid "Adding role %s to user %s in project %s" -#~ msgstr "Adding role: ロール %s をユーザ %s (プロジェクト %s の) に追加します。" - -#, python-format -#~ msgid "Removing role %s from user %s on project %s" -#~ msgstr "Removing role: ロール %s をユーザ %s (プロジェクト %s の)から削除します。" - -#, python-format -#~ msgid "Created project %s with manager %s" -#~ msgstr "Created project: プロジェクト %s (マネージャ %s)を作成します。" - -#, python-format -#~ msgid "Remove user %s from project %s" -#~ msgstr "Remove user: ユーザ %s をプロジェクト %s から削除します。" - -#, python-format -#~ msgid "Created user %s (admin: %r)" -#~ msgstr "Created user: ユーザ %s (admin: %r) を作成しました。" - -#, python-format -#~ msgid "Admin status set to %r for user %s" -#~ msgstr "Admin status set: 管理者ステータス %r をユーザ %s に設定します。" - -#, python-format -#~ msgid "Quota exceeeded for %s, tried to run %s instances" -#~ msgstr "%s のクオータ上限を超えました。%s インスタンスを実行しようとしました。" - -#, python-format -#~ msgid "Casting to scheduler for %s/%s's instance %s" -#~ msgstr "スケジューラに対して %s/%s のインスタンス %s を送信します。" - -#, python-format -#~ msgid "Going to try and terminate %s" -#~ msgstr "%s を終了します。" - -#, python-format -#~ msgid "Input partition size not evenly divisible by sector size: %d / %d" -#~ msgstr "インプットパーティションサイズがセクターサイズで割り切れません。 %d / %d" - -#, python-format -#~ msgid "Bytes for local storage not evenly divisible by sector size: %d / %d" -#~ msgstr "ローカルストレージのバイト数がセクターサイズで割り切れません: %d / %d" - -#, python-format -#~ msgid "check_instance_lock: arguments: |%s| |%s| |%s|" -#~ msgstr "check_instance_lock: arguments: |%s| |%s| |%s|" - -#, python-format -#~ msgid "Disassociating address %s" -#~ msgstr "アドレス %s の関連付けを解除(disassociate)しています。" - -#, python-format -#~ msgid "trying to reboot a non-running instance: %s (state: %s excepted: %s)" -#~ msgstr "実行していないインスタンスの再起動を試みます。%s (状態: %s 期待する状態: %s)" - -#, python-format -#~ msgid "" -#~ "trying to snapshot a non-running instance: %s (state: %s excepted: %s)" -#~ msgstr "実行していないインスタンスのスナップショット取得を試みます。%s (状態: %s 期待する状態: %s)" - -#, python-format -#~ msgid "instance %s: attaching volume %s to %s" -#~ msgstr "attaching volume: インスタンス %s についてボリューム %s を %s にアタッチします。" - -#, python-format -#~ msgid "instance %s: attach failed %s, removing" -#~ msgstr "インスタンス %s: %sのアタッチに失敗しました。リムーブします。" - -#, python-format -#~ msgid "Detach volume %s from mountpoint %s on instance %s" -#~ msgstr "Detach volume: ボリューム %s をマウントポイント %s (インスタンス%s)からデタッチします。" - -#, python-format #~ msgid "updating %s..." #~ msgstr "%s の情報の更新…" #~ msgid "unexpected error during update" #~ msgstr "更新の最中に予期しないエラーが発生しました。" -#, python-format -#~ msgid "Cannot get blockstats for \"%s\" on \"%s\"" -#~ msgstr "ブロックデバイス \"%s\" の統計を \"%s\" について取得できません。" - -#, python-format -#~ msgid "Cannot get ifstats for \"%s\" on \"%s\"" -#~ msgstr "インタフェース \"%s\" の統計を \"%s\" について取得できません。" - #~ msgid "unexpected exception getting connection" #~ msgstr "接続に際し予期しないエラーが発生しました。" @@ -3072,278 +2866,13 @@ msgstr "ユーザ %(user)s をプロジェクト %(project)s から削除しま #~ msgstr "インスタンス %s が見つかりました。" #, python-format -#~ msgid "No service for %s, %s" -#~ msgstr "%s, %s のserviceが存在しません。" - -#, python-format -#~ msgid "No instance for id %s" -#~ msgstr "id %s のinstanceが存在しません。" - -#, python-format -#~ msgid "no keypair for user %s, name %s" -#~ msgstr "ユーザ %s, ネーム%s に該当するキーペアが存在しません。" - -#, python-format -#~ msgid "No volume for id %s" -#~ msgstr "id %s に該当するボリュームが存在しません。" - -#, python-format -#~ msgid "No security group named %s for project: %s" -#~ msgstr "セキュリティグループ名 %s がプロジェクト %s に存在しません。" - -#, python-format -#~ msgid "Parallax returned HTTP error %d from request for /images" -#~ msgstr "Parallax がHTTPエラー%d を /images に対するリクエストに対して返しました。" - -#, python-format -#~ msgid "Parallax returned HTTP error %d from request for /images/detail" -#~ msgstr "Parallax がHTTPエラー %d を /images/detail に対するリクエストに対して返しました" - -#, python-format -#~ msgid "IP %s leased to bad mac %s vs %s" -#~ msgstr "IP %s が期待した mac %s ではなく %s にリースされました。" - -#, python-format -#~ msgid "IP %s released from bad mac %s vs %s" -#~ msgstr "IP %s がmac %s ではない mac %s への割当から開放されました。" - -#, python-format -#~ msgid "Getting object: %s / %s" -#~ msgstr "オブジェクトの取得: %s / %s" - -#, python-format -#~ msgid "Unauthorized attempt to get object %s from bucket %s" -#~ msgstr "" -#~ "Unauthorized attempt to get object: オブジェクト %s のバケット %s からの取得は許可されていません。" - -#, python-format -#~ msgid "Putting object: %s / %s" -#~ msgstr "オブジェクトの格納:: %s / %s" - -#, python-format -#~ msgid "Unauthorized attempt to upload object %s to bucket %s" -#~ msgstr "" -#~ "Unauthorized attempt to upload: オブジェクト %s のバケット %s へのアップロードは許可されていません。" - -#, python-format -#~ msgid "Deleting object: %s / %s" -#~ msgstr "オブジェクトを削除しています。: %s / %s" - -#, python-format -#~ msgid "Toggling publicity flag of image %s %r" -#~ msgstr "Toggling publicity flag: イメージ %s の公開フラグを %r に更新します。" - -#, python-format -#~ msgid "Casting to %s %s for %s" -#~ msgstr "メッセージのcast: %s %s for %s" - -#, python-format -#~ msgid "Nested received %s, %s" -#~ msgstr "ネスとした受信: %s, %s" - -#, python-format -#~ msgid "Creating disk for %s by attaching disk file %s" -#~ msgstr "%s のディスクをディスクファイル %s をアタッチして作成します。" - -#, python-format -#~ msgid "Created switch port %s on switch %s" -#~ msgstr "スイッチポート %s をスイッチ %s に作成しました。" - -#, python-format -#~ msgid "WMI job succeeded: %s, Elapsed=%s " -#~ msgstr "WMIジョブが成功しました: %s, 経過時間=%s " - -#, python-format -#~ msgid "Del: disk %s vm %s" -#~ msgstr "Del: 削除: disk %s vm %s" - -#, python-format -#~ msgid "" -#~ "Got Info for vm %s: state=%s, mem=%s, num_cpu=%s, " -#~ "cpu_time=%s" -#~ msgstr "" -#~ "vm %s の情報の取得: state=%s, mem=%s, num_cpu=%s, cpu_time=%s" - -#, python-format -#~ msgid "Successfully changed vm state of %s to %s" -#~ msgstr "vmの状態の %s から %s への変更に成功しました。" - -#, python-format -#~ msgid "Failed to change vm state of %s to %s" -#~ msgstr "VMの状態の %s から %s への変更に失敗しました。" - -#, python-format -#~ msgid "Finished retreving %s -- placed in %s" -#~ msgstr "%s を取得しました。格納先: %s" - -#, python-format -#~ msgid "instance %s: deleting instance files %s" -#~ msgstr "インスタンス %s: インスタンスファイル %s を削除しています。" - -#, python-format -#~ msgid "data: %r, fpath: %r" -#~ msgstr "データ:%r ファイルパス: %r" - -#, python-format -#~ msgid "Contents of file %s: %r" -#~ msgstr "ファイル %s の中身: %r" - -#, python-format -#~ msgid "instance %s: injecting key into image %s" -#~ msgstr "インスタンス %s にキー %s をインジェクトします。" - -#, python-format -#~ msgid "instance %s: injecting net into image %s" -#~ msgstr "インスタンス %s のネットワーク設定をイメージ %s にインジェクトします。" - -#, python-format -#~ msgid "instance %s: ignoring error injecting data into image %s (%s)" -#~ msgstr "インスタンス %s: データをイメージ %s にインジェクトする際にエラーが発生しました。(%s)" - -#, python-format -#~ msgid "Task [%s] %s status: success %s" -#~ msgstr "タスク [%s] %s ステータス: success %s" - -#, python-format -#~ msgid "Task [%s] %s status: %s %s" -#~ msgstr "タスク [%s] %s ステータス: %s %s" - -#, python-format -#~ msgid "%s: _db_content => %s" -#~ msgstr "%s: _db_content => %s" - -#, python-format -#~ msgid "Calling %s %s" -#~ msgstr "呼び出し: %s %s" - -#, python-format -#~ msgid "Created VM %s as %s." -#~ msgstr "VM %s を %s として作成しました。" - -#, python-format -#~ msgid "Creating VBD for VM %s, VDI %s ... " -#~ msgstr "VM %s, VDI %s のVBDを作成します… " - -#, python-format -#~ msgid "Created VBD %s for VM %s, VDI %s." -#~ msgstr "VBD %s を VM %s, VDI %s に対して作成しました。" - -#, python-format -#~ msgid "Creating VIF for VM %s, network %s." -#~ msgstr "VM %s, ネットワーク %s を作成します。" - -#, python-format -#~ msgid "Created VIF %s for VM %s, network %s." -#~ msgstr "VIF %s を VM %s, ネットワーク %s に作成しました。" - -#, python-format -#~ msgid "Snapshotting VM %s with label '%s'..." -#~ msgstr "VM %s のスナップショットをラベル '%s' で作成します。" - -#, python-format -#~ msgid "Created snapshot %s from VM %s." -#~ msgstr "スナップショット %s を VM %s について作成しました。" - -#, python-format -#~ msgid "Asking xapi to upload %s as '%s'" -#~ msgstr "xapiに対して %s を '%s' としてアップロードするように指示します。" - -#, python-format -#~ msgid "Asking xapi to fetch %s as %s" -#~ msgstr "xapi に対して %s を %s として取得するように指示します。" - -#, python-format #~ msgid "PV Kernel in VDI:%d" #~ msgstr "VDIのPV Kernel: %d" #, python-format -#~ msgid "VHD %s has parent %s" -#~ msgstr "VHD %s のペアレントは %s です。" - -#, python-format -#~ msgid "Parent %s doesn't match original parent %s, waiting for coalesce..." -#~ msgstr "ペアレント %s がオリジナルのペアレント %s と一致しません。合致するのを待ちます…" - -#, python-format -#~ msgid "Unexpected number of VDIs (%s) found for VM %s" -#~ msgstr "予期しない数 (%s) のVDIがVM %s に存在します。" - -#, python-format -#~ msgid "Spawning VM %s created %s." -#~ msgstr "VM %s の生成(spawning) により %s を作成しました。" - -#, python-format -#~ msgid "Unable to Snapshot %s: %s" -#~ msgstr "%s のスナップショットに失敗しました: %s" - -#, python-format -#~ msgid "suspend: instance not present %s" -#~ msgstr "suspend: インスタンス %s は存在しません。" - -#, python-format -#~ msgid "resume: instance not present %s" -#~ msgstr "resume: インスタンス %s は存在しません。" - -#, python-format -#~ msgid "Instance not found %s" -#~ msgstr "インスタンス %s が見つかりません。" - -#, python-format -#~ msgid "Introduced %s as %s." -#~ msgstr "%s を %s として introduce しました。" - -#, python-format -#~ msgid "Ignoring exception %s when getting PBDs for %s" -#~ msgstr "例外 %s が %s のPBDを取得する際に発生しましたが無視します。" - -#, python-format -#~ msgid "Ignoring exception %s when unplugging PBD %s" -#~ msgstr "例外 %s が %s のPBDをunplugする際に発生しましたが無視します。" - -#, python-format -#~ msgid "Ignoring exception %s when forgetting SR %s" -#~ msgstr "例外 %s がSR %s をforgetする際に発生しましたが無視します。" - -#, python-format -#~ msgid "Unable to obtain target information %s, %s" -#~ msgstr "ターゲットの情報を取得できません。 %s, %s" - -#, python-format -#~ msgid "Attach_volume: %s, %s, %s" -#~ msgstr "Attach_volume: ボリュームのアタッチ: %s, %s, %s" - -#, python-format -#~ msgid "Unable to create VDI on SR %s for instance %s" -#~ msgstr "SR %s にインスタンス %s のVDIを作成できません。" - -#, python-format -#~ msgid "Unable to use SR %s for instance %s" -#~ msgstr "SR %s をインスタンス %s に対して利用できません。" - -#, python-format -#~ msgid "Mountpoint %s attached to instance %s" -#~ msgstr "マウントポイント %s をインスタンス %s にアタッチしました。" - -#, python-format -#~ msgid "Detach_volume: %s, %s" -#~ msgstr "Detach_volume: ボリュームのデタッチ: %s, %s" - -#, python-format -#~ msgid "Mountpoint %s detached from instance %s" -#~ msgstr "マウントポイント %s をインスタンス %s からデタッチしました。" - -#, python-format -#~ msgid "Quota exceeeded for %s, tried to create %sG volume" -#~ msgstr "%sのクオータを超えています。サイズ %sG のボリュームの作成を行おうとしました。" - -#, python-format #~ msgid "Volume quota exceeded. You cannot create a volume of size %s" #~ msgstr "ボリュームのクオータを超えています。%sの大きさのボリュームは作成できません。" -#, python-format -#~ msgid "volume %s: creating lv of size %sG" -#~ msgstr "ボリューム%sの%sGのlv (論理ボリューム) を作成します。" - #~ msgid "Wrong number of arguments." #~ msgstr "引数の数が異なります。" diff --git a/po/pt_BR.po b/po/pt_BR.po index d6d57a9b1..48a718808 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-07-25 17:40+0000\n" -"Last-Translator: msinhore <msinhore@gmail.com>\n" +"PO-Revision-Date: 2011-09-02 12:17+0000\n" +"Last-Translator: Robson Negreiros Bezerra <Unknown>\n" "Language-Team: Brazilian Portuguese <pt_BR@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" -"X-Generator: Launchpad (build 13573)\n" +"X-Launchpad-Export-Date: 2011-09-03 05:50+0000\n" +"X-Generator: Launchpad (build 13830)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -453,11 +453,13 @@ msgid "" "Detach volume %(volume_id)s from mountpoint %(mp)s on instance " "%(instance_id)s" msgstr "" +"Desconectando volume %(volume_id)s do ponto de montagem %(mp)s na instância " +"%(instance_id)s" #: ../nova/compute/manager.py:588 #, python-format msgid "Detaching volume from unknown instance %s" -msgstr "" +msgstr "Desconectando volume da instância desconhecida %s" #: ../nova/scheduler/simple.py:53 #, python-format @@ -708,7 +710,7 @@ msgstr "Ligação %(queue)s para %(exchange)s com chave %(routing_key)s" #: ../nova/fakerabbit.py:121 #, python-format msgid "Getting from %(queue)s: %(message)s" -msgstr "" +msgstr "Recebendo de %(queue)s: %(message)s" #: ../nova/virt/xenapi/vm_utils.py:135 ../nova/virt/hyperv.py:171 #, python-format @@ -808,7 +810,7 @@ msgstr "Kernel/Ramdisk %s destruidos" #: ../nova/virt/xenapi/vm_utils.py:361 #, python-format msgid "Asking xapi to fetch %(url)s as %(access)s" -msgstr "" +msgstr "Requisitando à xapi a busca da url %(url)s como %(access)s" #: ../nova/virt/xenapi/vm_utils.py:386 ../nova/virt/xenapi/vm_utils.py:402 #, python-format @@ -880,58 +882,59 @@ msgstr "" #: ../nova/virt/xenapi/vm_utils.py:590 #, python-format msgid "No VDIs found for VM %s" -msgstr "" +msgstr "Nenhum VDIs encontrado para MV %s" #: ../nova/virt/xenapi/vm_utils.py:594 #, python-format msgid "Unexpected number of VDIs (%(num_vdis)s) found for VM %(vm_ref)s" msgstr "" +"Número de VDIs inesperado (%(num_vdis)s) encontrado para MV %(vm_ref)s" #: ../nova/virt/xenapi/vm_utils.py:653 #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:188 #, python-format msgid "Creating VBD for VDI %s ... " -msgstr "" +msgstr "Criando VBD para VDI %s ... " #: ../nova/virt/xenapi/vm_utils.py:655 #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:190 #, python-format msgid "Creating VBD for VDI %s done." -msgstr "" +msgstr "O VBD para VDI %s foi criado." #: ../nova/virt/xenapi/vm_utils.py:657 #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:192 #, python-format msgid "Plugging VBD %s ... " -msgstr "" +msgstr "Conectando VBD %s ... " #: ../nova/virt/xenapi/vm_utils.py:659 #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:194 #, python-format msgid "Plugging VBD %s done." -msgstr "" +msgstr "O VDB %s foi conectado." #: ../nova/virt/xenapi/vm_utils.py:661 #, python-format msgid "VBD %(vbd)s plugged as %(orig_dev)s" -msgstr "" +msgstr "VBD %(vbd)s conectado como %(orig_dev)s" #: ../nova/virt/xenapi/vm_utils.py:664 #, python-format msgid "VBD %(vbd)s plugged into wrong dev, remapping to %(dev)s" -msgstr "" +msgstr "VBD %(vbd)s conectado no device errado, remapeando para %(dev)s" #: ../nova/virt/xenapi/vm_utils.py:668 #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:197 #, python-format msgid "Destroying VBD for VDI %s ... " -msgstr "" +msgstr "Destruindo VBD para o VDI %s ... " #: ../nova/virt/xenapi/vm_utils.py:671 #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:200 #, python-format msgid "Destroying VBD for VDI %s done." -msgstr "" +msgstr "O VBD para o VDI %s foi destruído." #: ../nova/virt/xenapi/vm_utils.py:683 #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:211 @@ -952,7 +955,7 @@ msgstr "" #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:223 #, python-format msgid "Ignoring XenAPI.Failure in VBD.unplug: %s" -msgstr "" +msgstr "Ignorando XenAPI.Failure em VBD.unplug: %s" #: ../nova/virt/xenapi/vm_utils.py:704 #: ../plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py:66 @@ -2824,47 +2827,6 @@ msgstr "" msgid "Removing user %(user)s from project %(project)s" msgstr "" -#, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" -#~ msgstr "" -#~ "%s\n" -#~ "Comando: %s\n" -#~ "Código de retorno: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" - -#, python-format -#~ msgid "(%s) publish (key: %s) %s" -#~ msgstr "(%s) publicar (key: %s) %s" - -#, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "" -#~ "Servidor AMQP em %s:%d inatingível. Tentando novamente em %d segundos." - -#, python-format -#~ msgid "Binding %s to %s with key %s" -#~ msgstr "Atribuindo %s para %s com chave %s" - -#, python-format -#~ msgid "Getting from %s: %s" -#~ msgstr "Obtendo de %s: %s" - -#, python-format -#~ msgid "Starting %s node" -#~ msgstr "Iniciando nó %s" - -#, python-format -#~ msgid "Data store %s is unreachable. Trying again in %d seconds." -#~ msgstr "" -#~ "Repositório de dados %s não pode ser atingido. Tentando novamente em %d " -#~ "segundos." - #~ msgid "Full set of FLAGS:" #~ msgstr "Conjunto completo de FLAGS:" @@ -2876,115 +2838,6 @@ msgstr "" #~ msgid "Serving %s" #~ msgstr "Servindo %s" -#, python-format -#~ msgid "Couldn't get IP, using 127.0.0.1 %s" -#~ msgstr "Não foi possível obter IP, usando 127.0.0.1 %s" - -#, python-format -#~ msgid "" -#~ "Access key %s has had %d failed authentications and will be locked out for " -#~ "%d minutes." -#~ msgstr "" -#~ "Chave de acesso %s tem %d falhas de autenticação e vai ser bloqueada por %d " -#~ "minutos." - -#, python-format -#~ msgid "arg: %s\t\tval: %s" -#~ msgstr "argumento: %s\t\tvalor: %s" - -#, python-format -#~ msgid "Authenticated Request For %s:%s)" -#~ msgstr "Pedido de Autenticação Para: %s:%s" - -#, python-format -#~ msgid "Adding sitewide role %s to user %s" -#~ msgstr "Adicionando papel em todo site %s ao usuário %s" - -#, python-format -#~ msgid "Adding role %s to user %s for project %s" -#~ msgstr "Adicionando papel %s ao usuário %s para o projeto %s" - -#, python-format -#~ msgid "Unauthorized request for controller=%s and action=%s" -#~ msgstr "Requisição não autorizada para controlador=%s e ação=%s" - -#, python-format -#~ msgid "Removing role %s from user %s for project %s" -#~ msgstr "Removendo papel %s do usuário %s para o projeto %s" - -#, python-format -#~ msgid "Getting x509 for user: %s on project: %s" -#~ msgstr "Obtendo x509 para usuário: %s do projeto: %s" - -#, python-format -#~ msgid "Create project %s managed by %s" -#~ msgstr "Criar projeto %s gerenciado por %s" - -#, python-format -#~ msgid "Removing user %s from project %s" -#~ msgstr "Excluindo usuário %s do projeto %s" - -#, python-format -#~ msgid "Adding user %s to project %s" -#~ msgstr "Adicionando usuário %s ao projeto %s" - -#, python-format -#~ msgid "Unsupported API request: controller = %s,action = %s" -#~ msgstr "Requisição de API não suportada: controlador = %s,ação = %s" - -#, python-format -#~ msgid "Removing sitewide role %s from user %s" -#~ msgstr "Removendo papel %s em todo site do usuário %s" - -#, python-format -#~ msgid "Associate address %s to instance %s" -#~ msgstr "Atribuir endereço %s à instância %s" - -#, python-format -#~ msgid "Attach volume %s to instacne %s at %s" -#~ msgstr "Anexar volume %s para instância %s em %s" - -#, python-format -#~ msgid "Registered image %s with id %s" -#~ msgstr "Registrada imagem %s com id %s" - -#, python-format -#~ msgid "User %s is already a member of the group %s" -#~ msgstr "Usuário %s já pertence ao grupo %s" - -#, python-format -#~ msgid "User %s is not a member of project %s" -#~ msgstr "Usuário %s não é membro do projeto %s" - -#, python-format -#~ msgid "failed authorization: no project named %s (user=%s)" -#~ msgstr "falha de autorização: nenhum projeto de nome %s (usuário=%s)" - -#, python-format -#~ msgid "Failed authorization: user %s not admin and not member of project %s" -#~ msgstr "" -#~ "Falha de autorização: usuário %s não é administrador nem membro do projeto %s" - -#, python-format -#~ msgid "Created project %s with manager %s" -#~ msgstr "Criado projeto %s com gerente %s" - -#, python-format -#~ msgid "Removing role %s from user %s on project %s" -#~ msgstr "Removendo papel %s do usuário %s no projeto %s" - -#, python-format -#~ msgid "Adding role %s to user %s in project %s" -#~ msgstr "Adicionando papel %s ao usuário %s no projeto %s" - -#, python-format -#~ msgid "Remove user %s from project %s" -#~ msgstr "Remover usuário %s do projeto %s" - -#, python-format -#~ msgid "Created user %s (admin: %r)" -#~ msgstr "Criado usuário %s (administrador: %r)" - #~ msgid "No such process" #~ msgstr "Processo inexistente" @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-07-09 07:20+0000\n" -"Last-Translator: ilya kislicyn <Unknown>\n" +"PO-Revision-Date: 2011-08-23 11:22+0000\n" +"Last-Translator: Thierry Carrez <thierry.carrez+lp@gmail.com>\n" "Language-Team: Russian <ru@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" -"X-Generator: Launchpad (build 13573)\n" +"X-Launchpad-Export-Date: 2011-08-24 04:47+0000\n" +"X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2793,58 +2793,6 @@ msgstr "" #~ msgid "Starting %s" #~ msgstr "Запускается %s" -#, python-format -#~ msgid "arg: %s\t\tval: %s" -#~ msgstr "arg: %s\t\tval: %s" - -#, python-format -#~ msgid "Adding role %s to user %s for project %s" -#~ msgstr "Добавление роли %s для пользователя %s для проекта %s" - -#, python-format -#~ msgid "Removing role %s from user %s for project %s" -#~ msgstr "Удаление роли %s пользователя %s для проекта %s" - -#, python-format -#~ msgid "Create project %s managed by %s" -#~ msgstr "Создать проект %s под управлением %s" - -#, python-format -#~ msgid "Removing user %s from project %s" -#~ msgstr "Удаление пользователя %s с проекта %s" - -#, python-format -#~ msgid "Adding user %s to project %s" -#~ msgstr "Добавление пользователя %s к проекту %s" - -#, python-format -#~ msgid "User %s is already a member of the group %s" -#~ msgstr "Пользователь %s уже член группы %s" - -#, python-format -#~ msgid "User %s is not a member of project %s" -#~ msgstr "Пользователь %s не является членом группы %s" - -#, python-format -#~ msgid "Created project %s with manager %s" -#~ msgstr "Создан проект %s под управлением %s" - -#, python-format -#~ msgid "Removing role %s from user %s on project %s" -#~ msgstr "Удаление роли %s пользователя %s в проекте %s" - -#, python-format -#~ msgid "Remove user %s from project %s" -#~ msgstr "Удалить пользователя %s из проекта %s" - -#, python-format -#~ msgid "Created user %s (admin: %r)" -#~ msgstr "Создан пользователь %s (администратор: %r)" - -#, python-format -#~ msgid "Adding role %s to user %s in project %s" -#~ msgstr "Добавление роли %s для пользователя %s в проект %s" - #~ msgid "unexpected error during update" #~ msgstr "неожиданная ошибка во время обновления" @@ -2853,74 +2801,8 @@ msgstr "" #~ msgstr "обновление %s..." #, python-format -#~ msgid "Getting object: %s / %s" -#~ msgstr "Получение объекта: %s / %s" - -#, python-format -#~ msgid "Deleting object: %s / %s" -#~ msgstr "Удаление объекта: %s / %s" - -#, python-format -#~ msgid "%s: _db_content => %s" -#~ msgstr "%s: _db_content => %s" - -#, python-format -#~ msgid "Calling %s %s" -#~ msgstr "Звонок %s %s" - -#, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" -#~ msgstr "" -#~ "%s\n" -#~ "Команда: %s\n" -#~ "Код завершения: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" - -#, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "AMQP сервер %s:%d недоступен. Повторная попытка через %d секунд." - -#, python-format -#~ msgid "Putting object: %s / %s" -#~ msgstr "Вставка объекта: %s / %s" - -#, python-format -#~ msgid "Starting %s node" -#~ msgstr "Запускается нода %s" - -#, python-format -#~ msgid "Data store %s is unreachable. Trying again in %d seconds." -#~ msgstr "Хранилище данных %s недоступно. Повторная попытка через %d секунд." - -#, python-format -#~ msgid "Couldn't get IP, using 127.0.0.1 %s" -#~ msgstr "Не удалось получить IP, используем 127.0.0.1 %s" - -#, python-format #~ msgid "pidfile %s does not exist. Daemon not running?\n" #~ msgstr "pidfile %s не обнаружен. Демон не запущен?\n" -#, python-format -#~ msgid "Getting from %s: %s" -#~ msgstr "Получение из %s: %s" - -#, python-format -#~ msgid "" -#~ "Access key %s has had %d failed authentications and will be locked out for " -#~ "%d minutes." -#~ msgstr "" -#~ "Ключ доступа %s имеет %d неудачных попыток аутентификации и будет " -#~ "заблокирован на %d минут." - -#, python-format -#~ msgid "Authenticated Request For %s:%s)" -#~ msgstr "Запрос аутентификации для %s:%s)" - #~ msgid "Wrong number of arguments." #~ msgstr "Неверное число аргументов." @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-02-17 03:24+0000\n" -"Last-Translator: John Michael Baterna <Unknown>\n" +"PO-Revision-Date: 2011-08-23 11:21+0000\n" +"Last-Translator: Thierry Carrez <thierry.carrez+lp@gmail.com>\n" "Language-Team: Tagalog <tl@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" -"X-Generator: Launchpad (build 13573)\n" +"X-Launchpad-Export-Date: 2011-08-24 04:47+0000\n" +"X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2789,8 +2789,3 @@ msgstr "" #, python-format msgid "Removing user %(user)s from project %(project)s" msgstr "" - -#, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "" -#~ "Hindi makita o maabot ang AMQP server sa %s:%d. Muling subukan sa %d segundo." @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-02-03 22:02+0000\n" -"Last-Translator: Wladimir Rossinski <Unknown>\n" +"PO-Revision-Date: 2011-08-23 11:21+0000\n" +"Last-Translator: Thierry Carrez <thierry.carrez+lp@gmail.com>\n" "Language-Team: Ukrainian <uk@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" -"X-Generator: Launchpad (build 13573)\n" +"X-Launchpad-Export-Date: 2011-08-24 04:47+0000\n" +"X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2789,43 +2789,9 @@ msgid "Removing user %(user)s from project %(project)s" msgstr "" #, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "AMQP сервер %s:%d недоступний. Спроба під'єднання через %d секунд." - -#, python-format #~ msgid "Starting %s" #~ msgstr "Запускається %s" #, python-format #~ msgid "Serving %s" #~ msgstr "Обслуговування %s" - -#, python-format -#~ msgid "Couldn't get IP, using 127.0.0.1 %s" -#~ msgstr "Не вдалось отримати IP, використовуючи 127.0.0.1 %s" - -#, python-format -#~ msgid "Removing user %s from project %s" -#~ msgstr "Вилучення користувача %s з проекту %s" - -#, python-format -#~ msgid "Adding user %s to project %s" -#~ msgstr "Долучення користувача %s до проекту %s" - -#, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" -#~ msgstr "" -#~ "%s\n" -#~ "Команда: %s\n" -#~ "Код завершення: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" - -#, python-format -#~ msgid "Getting from %s: %s" -#~ msgstr "Отримання з %s: %s" diff --git a/tools/esx/guest_tool.py b/tools/esx/guest_tool.py index 97b5302ba..5158d883a 100644 --- a/tools/esx/guest_tool.py +++ b/tools/esx/guest_tool.py @@ -81,28 +81,34 @@ def _bytes2int(bytes): def _parse_network_details(machine_id):
"""
- Parse the machine.id field to get MAC, IP, Netmask and Gateway fields
- machine.id is of the form MAC;IP;Netmask;Gateway;Broadcast;DNS1,DNS2
- where ';' is the separator.
+ Parse the machine_id to get MAC, IP, Netmask and Gateway fields per NIC.
+ machine_id is of the form ('NIC_record#NIC_record#', '')
+ Each of the NIC will have record NIC_record in the form
+ 'MAC;IP;Netmask;Gateway;Broadcast;DNS' where ';' is field separator.
+ Each record is separated by '#' from next record.
"""
+ logging.debug(_("Received machine_id from vmtools : %s") % machine_id[0])
network_details = []
if machine_id[1].strip() == "1":
pass
else:
- network_info_list = machine_id[0].split(';')
- assert len(network_info_list) % 6 == 0
- no_grps = len(network_info_list) / 6
- i = 0
- while i < no_grps:
- k = i * 6
- network_details.append((
- network_info_list[k].strip().lower(),
- network_info_list[k + 1].strip(),
- network_info_list[k + 2].strip(),
- network_info_list[k + 3].strip(),
- network_info_list[k + 4].strip(),
- network_info_list[k + 5].strip().split(',')))
- i += 1
+ for machine_id_str in machine_id[0].split('#'):
+ network_info_list = machine_id_str.split(';')
+ if len(network_info_list) % 6 != 0:
+ break
+ no_grps = len(network_info_list) / 6
+ i = 0
+ while i < no_grps:
+ k = i * 6
+ network_details.append((
+ network_info_list[k].strip().lower(),
+ network_info_list[k + 1].strip(),
+ network_info_list[k + 2].strip(),
+ network_info_list[k + 3].strip(),
+ network_info_list[k + 4].strip(),
+ network_info_list[k + 5].strip().split(',')))
+ i += 1
+ logging.debug(_("NIC information from vmtools : %s") % network_details)
return network_details
@@ -279,6 +285,7 @@ def _filter_duplicates(all_entries): def _set_rhel_networking(network_details=None):
+ """Set IPv4 network settings for RHEL distros."""
network_details = network_details or []
all_dns_servers = []
for network_detail in network_details:
@@ -320,31 +327,33 @@ def _set_rhel_networking(network_details=None): def _set_ubuntu_networking(network_details=None):
+ """Set IPv4 network settings for Ubuntu."""
network_details = network_details or []
- """ Set IPv4 network settings for Ubuntu """
all_dns_servers = []
- for network_detail in network_details:
+ interface_file_name = '/etc/network/interfaces'
+ # Remove file
+ os.remove(interface_file_name)
+ # Touch file
+ _execute(['touch', interface_file_name])
+ interface_file = open(interface_file_name, 'w')
+ for device, network_detail in enumerate(network_details):
mac_address, ip_address, subnet_mask, gateway, broadcast,\
dns_servers = network_detail
all_dns_servers.extend(dns_servers)
adapter_name, current_ip_address = \
_get_linux_adapter_name_and_ip_address(mac_address)
- if adapter_name and not ip_address == current_ip_address:
- interface_file_name = \
- '/etc/network/interfaces'
- # Remove file
- os.remove(interface_file_name)
- # Touch file
- _execute(['touch', interface_file_name])
- interface_file = open(interface_file_name, 'w')
+ if adapter_name:
interface_file.write('\nauto %s' % adapter_name)
interface_file.write('\niface %s inet static' % adapter_name)
interface_file.write('\nbroadcast %s' % broadcast)
interface_file.write('\ngateway %s' % gateway)
interface_file.write('\nnetmask %s' % subnet_mask)
- interface_file.write('\naddress %s' % ip_address)
- interface_file.close()
+ interface_file.write('\naddress %s\n' % ip_address)
+ logging.debug(_("Successfully configured NIC %d with "
+ "NIC info %s") % (device, network_detail))
+ interface_file.close()
+
if all_dns_servers:
dns_file_name = "/etc/resolv.conf"
os.remove(dns_file_name)
@@ -355,7 +364,8 @@ def _set_ubuntu_networking(network_details=None): for dns_server in unique_entries:
dns_file.write("\nnameserver %s" % dns_server)
dns_file.close()
- print "\nRestarting networking....\n"
+
+ logging.debug(_("Restarting networking....\n"))
_execute(['/etc/init.d/networking', 'restart'])
diff --git a/tools/pip-requires b/tools/pip-requires index 66d6a48d9..a4af326dc 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -35,3 +35,4 @@ coverage nosexcover GitPython paramiko +feedparser |
