diff options
| author | Chris Behrens <cbehrens@codestud.com> | 2011-09-21 09:00:18 +0000 |
|---|---|---|
| committer | Chris Behrens <cbehrens@codestud.com> | 2011-09-21 09:00:18 +0000 |
| commit | a63ca427e75c73611dd129bc7b5a625f6c06fa44 (patch) | |
| tree | d80bef65fe63bb45fbf201f0c0475d3d699fa045 | |
| parent | 4d0bb8730a076b44d0a37fd0770c743b834e5751 (diff) | |
| parent | d9752d46554ffa87360bfd740177b40871cfbea6 (diff) | |
| download | nova-a63ca427e75c73611dd129bc7b5a625f6c06fa44.tar.gz nova-a63ca427e75c73611dd129bc7b5a625f6c06fa44.tar.xz nova-a63ca427e75c73611dd129bc7b5a625f6c06fa44.zip | |
Merged trunk
125 files changed, 5446 insertions, 3817 deletions
@@ -25,6 +25,7 @@ <jmckenty@gmail.com> <jmckenty@joshua-mckentys-macbook-pro.local> <jmckenty@gmail.com> <jmckenty@yyj-dhcp171.corp.flock.com> <jmckenty@gmail.com> <joshua.mckenty@nasa.gov> +<johannes.erdfelt@rackspace.com> <johannes@compute3.221.st> <josh@jk0.org> <josh.kearney@rackspace.com> <justin@fathomdb.com> <justinsb@justinsb-desktop> <justin@fathomdb.com> <superstack@superstack.org> @@ -12,6 +12,7 @@ Armando Migliaccio <Armando.Migliaccio@eu.citrix.com> Arvind Somya <asomya@cisco.com> Bilal Akhtar <bilalakhtar@ubuntu.com> Brad Hall <brad@nicira.com> +Brad McConnell <bmcconne@rackspace.com> Brian Lamar <brian.lamar@rackspace.com> Brian Schott <bschott@isi.edu> Brian Waldon <brian.waldon@rackspace.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 bc191b2f0..8a162028b 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -61,6 +61,7 @@ import math import netaddr from optparse import OptionParser import os +import StringIO import sys import time @@ -274,6 +275,58 @@ class ShellCommands(object): arguments: path""" exec(compile(open(path).read(), path, 'exec'), locals(), globals()) + @args('--filename', dest='filename', metavar='<path>', default=False, + help='Export file path') + def export(self, filename): + """Export Nova users into a file that can be consumed by Keystone""" + + def create_file(filename): + data = generate_data() + with open(filename, 'w') as f: + f.write(data.getvalue()) + + def tenants(data, am): + for project in am.get_projects(): + print >> data, ("tenant add '%s'" % + (project.name)) + for u in project.member_ids: + user = am.get_user(u) + print >> data, ("user add '%s' '%s' '%s'" % + (user.name, user.access, project.name)) + print >> data, ("credentials add 'EC2' '%s:%s' '%s' '%s'" % + (user.access, project.id, user.secret, project.id)) + + def roles(data, am): + for role in am.get_roles(): + print >> data, ("role add '%s'" % (role)) + + def grant_roles(data, am): + roles = am.get_roles() + for project in am.get_projects(): + for u in project.member_ids: + user = am.get_user(u) + for role in db.user_get_roles_for_project(ctxt, u, + project.id): + print >> data, ("role grant '%s', '%s', '%s')," % + (user.name, role, project.name)) + print >> data + + def generate_data(): + data = StringIO.StringIO() + am = manager.AuthManager() + tenants(data, am) + roles(data, am) + grant_roles(data, am) + data.seek(0) + return data + + ctxt = context.get_admin_context() + if filename: + create_file(filename) + else: + data = generate_data() + print data.getvalue() + class RoleCommands(object): """Class for managing roles.""" @@ -685,7 +738,7 @@ 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>", + @args('--uuid', dest="uuid", metavar="<network uuid>", help='Network UUID') @args('--project_id', dest="project_id", metavar="<project id>", help='Project id') @@ -710,22 +763,12 @@ class NetworkCommands(object): bridge_required = ['nova.network.manager.FlatManager', 'nova.network.manager.FlatDHCPManager'] if FLAGS.network_manager in bridge_required: - # TODO(tr3buchet) - swap print statement and following line for - # raise statement in diablo 4 - print _('--bridge parameter required or FLAG ' - 'flat_network_bridge must be set to create networks\n' - 'WARNING! ACHTUNG! Setting the bridge to br100 ' - 'automatically is deprecated and will be removed in ' - 'Diablo milestone 4. Prepare yourself accordingly.') - time.sleep(10) - bridge = 'br100' - #raise exception.NetworkNotCreated(req='--bridge') + raise exception.NetworkNotCreated(req='--bridge') 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') @@ -1649,7 +1692,7 @@ class InstanceTypeCommands(object): def _print_instance_types(self, name, val): deleted = ('', ', inactive')[val["deleted"] == 1] print ("%s: Memory: %sMB, VCPUS: %s, Storage: %sGB, FlavorID: %s, " - "Swap: %sGB, RXTX Quota: %sGB, RXTX Cap: %sMB%s") % ( + "Swap: %sMB, RXTX Quota: %sGB, RXTX Cap: %sMB%s") % ( name, val["memory_mb"], val["vcpus"], val["local_gb"], val["flavorid"], val["swap"], val["rxtx_quota"], val["rxtx_cap"], deleted) 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/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index cd24efb13..8555f6ce5 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -22,15 +22,11 @@ use = egg:Paste#urlmap pipeline = logrequest ec2noauth cloudrequest authorizer ec2executor # NOTE(vish): use the following pipeline for deprecated auth #pipeline = logrequest authenticate cloudrequest authorizer ec2executor -# NOTE(vish): use the following pipeline for keystone -# pipeline = logrequest totoken authtoken keystonecontext cloudrequest authorizer ec2executor [pipeline:ec2admin] pipeline = logrequest ec2noauth adminrequest authorizer ec2executor # NOTE(vish): use the following pipeline for deprecated auth #pipeline = logrequest authenticate adminrequest authorizer ec2executor -# NOTE(vish): use the following pipeline for keystone -#pipeline = logrequest totoken authtoken keystonecontext adminrequest authorizer ec2executor [pipeline:ec2metadata] pipeline = logrequest ec2md @@ -44,9 +40,6 @@ paste.filter_factory = nova.api.ec2:RequestLogging.factory [filter:ec2lockout] paste.filter_factory = nova.api.ec2:Lockout.factory -[filter:totoken] -paste.filter_factory = nova.api.ec2:ToToken.factory - [filter:ec2noauth] paste.filter_factory = nova.api.ec2:NoAuth.factory @@ -87,15 +80,11 @@ use = egg:Paste#urlmap pipeline = faultwrap noauth ratelimit osapiapp10 # NOTE(vish): use the following pipeline for deprecated auth # pipeline = faultwrap auth ratelimit osapiapp10 -# NOTE(vish): use the following pipeline for keystone -#pipeline = faultwrap authtoken keystonecontext ratelimit osapiapp10 [pipeline:openstackapi11] pipeline = faultwrap noauth ratelimit extensions osapiapp11 # NOTE(vish): use the following pipeline for deprecated auth # pipeline = faultwrap auth ratelimit extensions osapiapp11 -# NOTE(vish): use the following pipeline for keystone -# pipeline = faultwrap authtoken keystonecontext ratelimit extensions osapiapp11 [filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory @@ -123,22 +112,3 @@ pipeline = faultwrap osversionapp [app:osversionapp] paste.app_factory = nova.api.openstack.versions:Versions.factory - -########## -# Shared # -########## - -[filter:keystonecontext] -paste.filter_factory = nova.api.auth:KeystoneContext.factory - -[filter:authtoken] -paste.filter_factory = keystone.middleware.auth_token:filter_factory -service_protocol = http -service_host = 127.0.0.1 -service_port = 808 -auth_host = 127.0.0.1 -auth_port = 5001 -auth_protocol = http -auth_uri = http://127.0.0.1:5000/ -admin_token = 999888777666 - diff --git a/nova/api/auth.py b/nova/api/auth.py index cd0d38b3f..a94f28739 100644 --- a/nova/api/auth.py +++ b/nova/api/auth.py @@ -43,34 +43,3 @@ class InjectContext(wsgi.Middleware): def __call__(self, req): req.environ['nova.context'] = self.context return self.application - - -class KeystoneContext(wsgi.Middleware): - """Make a request context from keystone headers""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - try: - user_id = req.headers['X_USER'] - except KeyError: - return webob.exc.HTTPUnauthorized() - # get the roles - roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')] - project_id = req.headers['X_TENANT'] - # Get the auth token - auth_token = req.headers.get('X_AUTH_TOKEN', - req.headers.get('X_STORAGE_TOKEN')) - - # Build a context, including the auth_token... - remote_address = getattr(req, 'remote_address', '127.0.0.1') - remote_address = req.remote_addr - if FLAGS.use_forwarded_for: - remote_address = req.headers.get('X-Forwarded-For', remote_address) - ctx = context.RequestContext(user_id, - project_id, - roles=roles, - auth_token=auth_token, - remote_address=remote_address) - - req.environ['nova.context'] = ctx - return self.application diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 144aa3811..8dcb44bba 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -46,9 +46,6 @@ flags.DEFINE_integer('lockout_minutes', 15, 'Number of minutes to lockout if triggered.') flags.DEFINE_integer('lockout_window', 15, 'Number of minutes for lockout window.') -flags.DEFINE_string('keystone_ec2_url', - 'http://localhost:5000/v2.0/ec2tokens', - 'URL to get token from ec2 request.') flags.DECLARE('use_forwarded_for', 'nova.api.auth') @@ -142,54 +139,6 @@ class Lockout(wsgi.Middleware): return res -class ToToken(wsgi.Middleware): - """Authenticate an EC2 request with keystone and convert to token.""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - # Read request signature and access id. - try: - signature = req.params['Signature'] - access = req.params['AWSAccessKeyId'] - except KeyError: - raise webob.exc.HTTPBadRequest() - - # Make a copy of args for authentication and signature verification. - auth_params = dict(req.params) - # Not part of authentication args - auth_params.pop('Signature') - - # Authenticate the request. - creds = {'ec2Credentials': {'access': access, - 'signature': signature, - 'host': req.host, - 'verb': req.method, - 'path': req.path, - 'params': auth_params, - }} - creds_json = utils.dumps(creds) - headers = {'Content-Type': 'application/json'} - o = urlparse(FLAGS.keystone_ec2_url) - if o.scheme == "http": - conn = httplib.HTTPConnection(o.netloc) - else: - conn = httplib.HTTPSConnection(o.netloc) - conn.request('POST', o.path, body=creds_json, headers=headers) - response = conn.getresponse().read() - conn.close() - - # NOTE(vish): We could save a call to keystone by - # having keystone return token, tenant, - # user, and roles from this call. - result = utils.loads(response) - # TODO(vish): check for errors - - token_id = result['auth']['token']['id'] - # Authenticated! - req.headers['X-Auth-Token'] = token_id - return self.application - - class NoAuth(wsgi.Middleware): """Add user:project as 'nova.context' to WSGI environ.""" diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 049ca6f93..68d39042f 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -89,6 +89,7 @@ _STATE_DESCRIPTION_MAP = { vm_states.BUILDING: 'pending', vm_states.REBUILDING: 'pending', vm_states.DELETED: 'terminated', + vm_states.SOFT_DELETE: 'terminated', vm_states.STOPPED: 'stopped', vm_states.MIGRATING: 'migrate', vm_states.RESIZING: 'resize', @@ -272,11 +273,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 +299,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 +327,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 +354,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], @@ -572,18 +595,31 @@ class CloudController(object): g['ipPermissions'] = [] for rule in group.rules: r = {} - r['ipProtocol'] = rule.protocol - r['fromPort'] = rule.from_port - r['toPort'] = rule.to_port r['groups'] = [] r['ipRanges'] = [] if rule.group_id: source_group = db.security_group_get(context, rule.group_id) r['groups'] += [{'groupName': source_group.name, 'userId': source_group.project_id}] + if rule.protocol: + r['ipProtocol'] = rule.protocol + r['fromPort'] = rule.from_port + r['toPort'] = rule.to_port + g['ipPermissions'] += [dict(r)] + else: + for protocol, min_port, max_port in (('icmp', -1, -1), + ('tcp', 1, 65535), + ('udp', 1, 65536)): + r['ipProtocol'] = protocol + r['fromPort'] = min_port + r['toPort'] = max_port + g['ipPermissions'] += [dict(r)] else: + r['ipProtocol'] = rule.protocol + r['fromPort'] = rule.from_port + r['toPort'] = rule.to_port r['ipRanges'] += [{'cidrIp': rule.cidr}] - g['ipPermissions'] += [r] + g['ipPermissions'] += [r] return g def _rule_args_to_dict(self, context, kwargs): @@ -1200,8 +1236,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: @@ -1465,7 +1503,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/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index bcdf2ba78..aac8f9a1e 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -116,7 +116,7 @@ def dict_from_dotted_str(items): args = {} for key, value in items: parts = key.split(".") - key = camelcase_to_underscore(parts[0]) + key = str(camelcase_to_underscore(parts[0])) if isinstance(value, str) or isinstance(value, unicode): # NOTE(vish): Automatically convert strings back # into their respective values diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index d743a66ef..3ef9bdee5 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 @@ -76,6 +78,9 @@ _STATE_MAP = { vm_states.DELETED: { 'default': 'DELETED', }, + vm_states.SOFT_DELETE: { + 'default': 'DELETED', + }, } @@ -185,30 +190,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 +299,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/deferred_delete.py b/nova/api/openstack/contrib/deferred_delete.py new file mode 100644 index 000000000..13ee5511e --- /dev/null +++ b/nova/api/openstack/contrib/deferred_delete.py @@ -0,0 +1,76 @@ +# 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. + +"""The deferred instance delete extension.""" + +import webob +from webob import exc + +from nova import compute +from nova import exception +from nova import log as logging +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults +from nova.api.openstack import servers + + +LOG = logging.getLogger("nova.api.contrib.deferred-delete") + + +class Deferred_delete(extensions.ExtensionDescriptor): + def __init__(self): + super(Deferred_delete, self).__init__() + self.compute_api = compute.API() + + def _restore(self, input_dict, req, instance_id): + """Restore a previously deleted instance.""" + + context = req.environ["nova.context"] + self.compute_api.restore(context, instance_id) + return webob.Response(status_int=202) + + def _force_delete(self, input_dict, req, instance_id): + """Force delete of instance before deferred cleanup.""" + + context = req.environ["nova.context"] + self.compute_api.force_delete(context, instance_id) + return webob.Response(status_int=202) + + def get_name(self): + return "DeferredDelete" + + def get_alias(self): + return "os-deferred-delete" + + def get_description(self): + return "Instance deferred delete" + + def get_namespace(self): + return "http://docs.openstack.org/ext/deferred-delete/api/v1.1" + + def get_updated(self): + return "2011-09-01T00:00:00+00:00" + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + extensions.ActionExtension("servers", "restore", + self._restore), + extensions.ActionExtension("servers", "forceDelete", + self._force_delete), + ] + + return actions 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/rescue.py b/nova/api/openstack/contrib/rescue.py index 3de128895..2e5dbab73 100644 --- a/nova/api/openstack/contrib/rescue.py +++ b/nova/api/openstack/contrib/rescue.py @@ -18,11 +18,14 @@ import webob from webob import exc from nova import compute +from nova import flags from nova import log as logging +from nova import utils from nova.api.openstack import extensions as exts from nova.api.openstack import faults +FLAGS = flags.FLAGS LOG = logging.getLogger("nova.api.contrib.rescue") @@ -30,7 +33,7 @@ def wrap_errors(fn): """"Ensure errors are not passed along.""" def wrapped(*args): try: - fn(*args) + return fn(*args) except Exception, e: return faults.Fault(exc.HTTPInternalServerError()) return wrapped @@ -46,9 +49,13 @@ class Rescue(exts.ExtensionDescriptor): def _rescue(self, input_dict, req, instance_id): """Rescue an instance.""" context = req.environ["nova.context"] - self.compute_api.rescue(context, instance_id) + if input_dict['rescue'] and 'adminPass' in input_dict['rescue']: + password = input_dict['rescue']['adminPass'] + else: + password = utils.generate_password(FLAGS.password_length) + self.compute_api.rescue(context, instance_id, rescue_password=password) - return webob.Response(status_int=202) + return {'adminPass': password} @wrap_errors def _unrescue(self, input_dict, req, instance_id): diff --git a/nova/api/openstack/contrib/virtual_interfaces.py b/nova/api/openstack/contrib/virtual_interfaces.py index dab61efc8..1981cd372 100644 --- a/nova/api/openstack/contrib/virtual_interfaces.py +++ b/nova/api/openstack/contrib/virtual_interfaces.py @@ -15,15 +15,10 @@ """The virtual interfaces extension.""" -from webob import exc -import webob - -from nova import compute -from nova import exception from nova import log as logging +from nova import network from nova.api.openstack import common from nova.api.openstack import extensions -from nova.api.openstack import faults from nova.api.openstack import wsgi @@ -50,19 +45,14 @@ class ServerVirtualInterfaceController(object): """ def __init__(self): - self.compute_api = compute.API() + self.network_api = network.API() super(ServerVirtualInterfaceController, self).__init__() def _items(self, req, server_id, entity_maker): """Returns a list of VIFs, transformed through entity_maker.""" context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, server_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - vifs = instance['virtual_interfaces'] + vifs = self.network_api.get_vifs_by_instance(context, server_id) limited_list = common.limited(vifs, req) res = [entity_maker(context, vif) for vif in limited_list] return {'virtual_interfaces': res} 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 67e669c17..79f17e27f 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)]) @@ -316,14 +317,14 @@ class CreateInstanceHelper(object): def _get_server_admin_password_old_style(self, server): """ Determine the admin password for a server on creation """ - return utils.generate_password(16) + return utils.generate_password(FLAGS.password_length) def _get_server_admin_password_new_style(self, server): """ Determine the admin password for a server on creation """ password = server.get('adminPass') if password is None: - return utils.generate_password(16) + return utils.generate_password(FLAGS.password_length) if not isinstance(password, basestring) or password == '': msg = _("Invalid adminPass") raise exc.HTTPBadRequest(explanation=msg) 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..d579ae716 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,73 @@ 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']) + if 'type' in link: + elem.set('type', link['type']) + 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 ef835e408..4eb1a0b85 100644 --- a/nova/api/openstack/schemas/v1.1/server.rng +++ b/nova/api/openstack/schemas/v1.1/server.rng @@ -17,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"/> @@ -49,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 d084ac360..0e7c37486 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') @@ -168,6 +169,12 @@ class Controller(object): server['server']['adminPass'] = extra_values['password'] return server + def _delete(self, context, id): + if FLAGS.reclaim_instance_interval: + self.compute_api.soft_delete(context, id) + else: + self.compute_api.delete(context, id) + @scheduler_api.redirect_handler def update(self, req, id, body): """Update server then pass on to version-specific controller""" @@ -334,9 +341,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() @@ -477,16 +483,22 @@ class Controller(object): return webob.Response(status_int=202) @scheduler_api.redirect_handler - def rescue(self, req, id): + def rescue(self, req, id, body={}): """Permit users to rescue the server.""" context = req.environ["nova.context"] try: - self.compute_api.rescue(context, id) + if 'rescue' in body and body['rescue'] and \ + 'adminPass' in body['rescue']: + password = body['rescue']['adminPass'] + else: + password = utils.generate_password(FLAGS.password_length) + self.compute_api.rescue(context, id, rescue_password=password) except Exception: readable = traceback.format_exc() LOG.exception(_("compute.api::rescue %s"), readable) raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) + + return {'adminPass': password} @scheduler_api.redirect_handler def unrescue(self, req, id): @@ -566,7 +578,7 @@ class ControllerV10(Controller): def delete(self, req, id): """ Destroys a server """ try: - self.compute_api.delete(req.environ['nova.context'], id) + self._delete(req.environ['nova.context'], id) except exception.NotFound: raise exc.HTTPNotFound() return webob.Response(status_int=202) @@ -618,7 +630,7 @@ class ControllerV10(Controller): LOG.debug(msg) raise exc.HTTPBadRequest(explanation=msg) - password = utils.generate_password(16) + password = utils.generate_password(FLAGS.password_length) try: self.compute_api.rebuild(context, instance_id, image_id, password) @@ -644,13 +656,17 @@ class ControllerV11(Controller): def delete(self, req, id): """ Destroys a server """ try: - self.compute_api.delete(req.environ['nova.context'], id) + self._delete(req.environ['nova.context'], id) except exception.NotFound: raise exc.HTTPNotFound() def _get_key_name(self, req, body): if 'server' in body: - return body['server'].get('key_name') + try: + return body['server'].get('key_name') + except AttributeError: + msg = _("Malformed server entity") + raise exc.HTTPBadRequest(explanation=msg) def _image_ref_from_req_data(self, data): try: @@ -760,8 +776,10 @@ class ControllerV11(Controller): self._validate_metadata(metadata) self._decode_personalities(personalities) - password = info["rebuild"].get("adminPass", - utils.generate_password(16)) + if 'rebuild' in info and 'adminPass' in info['rebuild']: + password = info['rebuild']['adminPass'] + else: + password = utils.generate_password(FLAGS.password_length) try: self.compute_api.rebuild(context, instance_id, image_href, @@ -851,130 +869,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('userId', str(server['user_id'])) - node.setAttribute('tenantId', str(server['tenant_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) - - if 'security_groups' in server: - security_groups_node = self._create_security_groups_node(xml_doc, - server['security_groups']) - server_node.appendChild(security_groups_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 @@ -982,23 +983,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) - - def _security_group_to_xml(self, doc, security_group): - node = doc.createElement('security_group') - node.setAttribute('name', str(security_group.get('name'))) - return node - - def _create_security_groups_node(self, xml_doc, security_groups): - security_groups_node = xml_doc.createElement('security_groups') - if security_groups: - for security_group in security_groups: - node = self._security_group_to_xml(xml_doc, security_group) - security_groups_node.appendChild(node) - return security_groups_node + 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'): 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/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..659bfd463 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -18,6 +18,7 @@ import os.path from nova.api.openstack import common +from nova import utils class ViewBuilder(object): @@ -37,17 +38,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 +72,7 @@ class ViewBuilder(object): } self._build_server(image, image_obj) + self._build_image_id(image, image_obj) if detail: image.update({ @@ -95,6 +98,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 +127,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, @@ -128,6 +140,7 @@ class ViewBuilderV11(ViewBuilder): image = ViewBuilder.build(self, image_obj, detail) href = self.generate_href(image_obj["id"]) bookmark = self.generate_bookmark(image_obj["id"]) + alternate = self.generate_alternate(image_obj["id"]) image["links"] = [ { @@ -138,6 +151,11 @@ class ViewBuilderV11(ViewBuilder): "rel": "bookmark", "href": bookmark, }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate, + }, ] @@ -147,6 +165,13 @@ class ViewBuilderV11(ViewBuilder): return image def generate_bookmark(self, image_id): - """Create an url that refers to a specific flavor id.""" + """Create a URL that refers to a specific flavor id.""" return os.path.join(common.remove_version_from_href(self.base_url), self.project_id, "images", str(image_id)) + + def generate_alternate(self, image_id): + """Create an alternate link for a specific flavor id.""" + glance_url = utils.generate_glance_url() + + return "%s/%s/images/%s" % (glance_url, self.project_id, + str(image_id)) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index ac09b5864..473dc9e7e 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -145,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 @@ -185,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"]) @@ -205,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 4e2944bb7..1b35f061d 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -92,6 +92,19 @@ def _is_able_to_shutdown(instance, instance_id): return True +def _is_queued_delete(instance, instance_id): + vm_state = instance["vm_state"] + task_state = instance["task_state"] + + if vm_state != vm_states.SOFT_DELETE: + LOG.warn(_("Instance %(instance_id)s is not in a 'soft delete' " + "state. It is currently %(vm_state)s. Action aborted.") % + locals()) + return False + + return True + + class API(base.Base): """API for interacting with the compute manager.""" @@ -202,7 +215,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 @@ -286,18 +300,24 @@ class API(base.Base): return (num_instances, base_options, image) @staticmethod - def _ephemeral_size(instance_type, ephemeral_name): - num = block_device.ephemeral_num(ephemeral_name) + def _volume_size(instance_type, virtual_name): + size = 0 + if virtual_name == 'swap': + size = instance_type.get('swap', 0) + elif block_device.is_ephemeral(virtual_name): + num = block_device.ephemeral_num(virtual_name) + + # TODO(yamahata): ephemeralN where N > 0 + # Only ephemeral0 is allowed for now because InstanceTypes + # table only allows single local disk, local_gb. + # In order to enhance it, we need to add a new columns to + # instance_types table. + if num > 0: + return 0 - # TODO(yamahata): ephemeralN where N > 0 - # Only ephemeral0 is allowed for now because InstanceTypes - # table only allows single local disk, local_gb. - # In order to enhance it, we need to add a new columns to - # instance_types table. - if num > 0: - return 0 + size = instance_type.get('local_gb') - return instance_type.get('local_gb') + return size def _update_image_block_device_mapping(self, elevated_context, instance_type, instance_id, @@ -318,12 +338,7 @@ class API(base.Base): if not block_device.is_swap_or_ephemeral(virtual_name): continue - size = 0 - if virtual_name == 'swap': - size = instance_type.get('swap', 0) - elif block_device.is_ephemeral(virtual_name): - size = self._ephemeral_size(instance_type, virtual_name) - + size = self._volume_size(instance_type, virtual_name) if size == 0: continue @@ -353,8 +368,8 @@ class API(base.Base): virtual_name = bdm.get('virtual_name') if (virtual_name is not None and - block_device.is_ephemeral(virtual_name)): - size = self._ephemeral_size(instance_type, virtual_name) + block_device.is_swap_or_ephemeral(virtual_name)): + size = self._volume_size(instance_type, virtual_name) if size == 0: continue values['volume_size'] = size @@ -750,15 +765,85 @@ class API(base.Base): {'instance_id': instance_id, 'action_str': action_str}) raise + @scheduler_api.reroute_compute("soft_delete") + def soft_delete(self, context, instance_id): + """Terminate an instance.""" + LOG.debug(_("Going to try to soft delete %s"), instance_id) + instance = self._get_instance(context, instance_id, 'soft delete') + + if not _is_able_to_shutdown(instance, instance_id): + return + + # NOTE(jerdfelt): The compute daemon handles reclaiming instances + # that are in soft delete. If there is no host assigned, there is + # no daemon to reclaim, so delete it immediately. + host = instance['host'] + if host: + self.update(context, + instance_id, + vm_state=vm_states.SOFT_DELETE, + task_state=task_states.POWERING_OFF, + deleted_at=utils.utcnow()) + + self._cast_compute_message('power_off_instance', context, + instance_id, host) + else: + LOG.warning(_("No host for instance %s, deleting immediately"), + instance_id) + terminate_volumes(self.db, context, instance_id) + self.db.instance_destroy(context, instance_id) + @scheduler_api.reroute_compute("delete") def delete(self, context, instance_id): """Terminate an instance.""" LOG.debug(_("Going to try to terminate %s"), instance_id) - instance = self._get_instance(context, instance_id, 'terminating') + instance = self._get_instance(context, instance_id, 'delete') if not _is_able_to_shutdown(instance, instance_id): return + host = instance['host'] + if host: + self.update(context, + instance_id, + task_state=task_states.DELETING) + + self._cast_compute_message('terminate_instance', context, + instance_id, host) + else: + terminate_volumes(self.db, context, instance_id) + self.db.instance_destroy(context, instance_id) + + @scheduler_api.reroute_compute("restore") + def restore(self, context, instance_id): + """Restore a previously deleted (but not reclaimed) instance.""" + instance = self._get_instance(context, instance_id, 'restore') + + if not _is_queued_delete(instance, instance_id): + return + + self.update(context, + instance_id, + vm_state=vm_states.ACTIVE, + task_state=None, + deleted_at=None) + + host = instance['host'] + if host: + self.update(context, + instance_id, + task_state=task_states.POWERING_ON) + self._cast_compute_message('power_on_instance', context, + instance_id, host) + + @scheduler_api.reroute_compute("force_delete") + def force_delete(self, context, instance_id): + """Force delete a previously deleted (but not reclaimed) instance.""" + instance = self._get_instance(context, instance_id, 'force delete') + + if not _is_queued_delete(instance, instance_id): + return + self.update(context, instance_id, task_state=task_states.DELETING) @@ -903,7 +988,7 @@ class API(base.Base): if 'reservation_id' in filters: recurse_zones = True - instances = self.db.instance_get_all_by_filters(context, filters) + instances = self._get_instances_by_filters(context, filters) if not recurse_zones: return instances @@ -928,6 +1013,18 @@ class API(base.Base): return instances + def _get_instances_by_filters(self, context, filters): + ids = None + if 'ip6' in filters or 'ip' in filters: + res = self.network_api.get_instance_uuids_by_ip_filter(context, + filters) + # NOTE(jkoelker) It is possible that we will get the same + # instance uuid twice (one for ipv4 and ipv6) + uuids = set([r['instance_uuid'] for r in res]) + filters['uuid'] = uuids + + return self.db.instance_get_all_by_filters(context, filters) + def _cast_compute_message(self, method, context, instance_id, host=None, params=None): """Generic handler for RPC casts to compute. @@ -1042,13 +1139,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, @@ -1272,13 +1370,18 @@ class API(base.Base): self._cast_compute_message('resume_instance', context, instance_id) @scheduler_api.reroute_compute("rescue") - def rescue(self, context, instance_id): + def rescue(self, context, instance_id, rescue_password=None): """Rescue the given instance.""" self.update(context, instance_id, vm_state=vm_states.ACTIVE, task_state=task_states.RESCUING) - self._cast_compute_message('rescue_instance', context, instance_id) + + rescue_params = { + "rescue_password": rescue_password + } + self._cast_compute_message('rescue_instance', context, instance_id, + params=rescue_params) @scheduler_api.reroute_compute("unrescue") def unrescue(self, context, instance_id): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 0477db745..d7c23c65d 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -35,12 +35,13 @@ terminating it. """ +import datetime +import functools import os import socket import sys import tempfile import time -import functools from eventlet import greenthread @@ -70,8 +71,6 @@ flags.DEFINE_string('compute_driver', 'nova.virt.connection.get_connection', 'Driver to use for controlling virtualization') flags.DEFINE_string('stub_network', False, 'Stub network related code') -flags.DEFINE_integer('password_length', 12, - 'Length of generated admin passwords') flags.DEFINE_string('console_host', socket.gethostname(), 'Console proxy host to use to connect to instances on' 'this host.') @@ -81,8 +80,13 @@ flags.DEFINE_integer('live_migration_retry_count', 30, flags.DEFINE_integer("rescue_timeout", 0, "Automatically unrescue an instance after N seconds." " Set to 0 to disable.") +flags.DEFINE_integer("resize_confirm_window", 0, + "Automatically confirm resizes after N seconds." + " Set to 0 to disable.") flags.DEFINE_integer('host_state_interval', 120, 'Interval in seconds for querying the host status') +flags.DEFINE_integer('reclaim_instance_interval', 0, + 'Interval in seconds for reclaiming deleted instances') LOG = logging.getLogger('nova.compute.manager') @@ -174,7 +178,7 @@ class ComputeManager(manager.SchedulerDependentManager): 'nova-compute restart.'), locals()) self.reboot_instance(context, instance['id']) elif drv_state == power_state.RUNNING: - # Hyper-V and VMWareAPI drivers will raise and exception + # Hyper-V and VMWareAPI drivers will raise an exception try: net_info = self._get_instance_nw_info(context, instance) self.driver.ensure_filtering_rules_for_instance(instance, @@ -322,7 +326,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: @@ -485,10 +490,8 @@ class ComputeManager(manager.SchedulerDependentManager): if action_str == 'Terminating': terminate_volumes(self.db, context, instance_id) - @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - @checks_instance_lock - def terminate_instance(self, context, instance_id): - """Terminate an instance on this host.""" + def _delete_instance(self, context, instance_id): + """Delete an instance on this host.""" self._shutdown_instance(context, instance_id, 'Terminating') instance = self.db.instance_get(context.elevated(), instance_id) self._instance_update(context, @@ -506,6 +509,12 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock + def terminate_instance(self, context, instance_id): + """Terminate an instance on this host.""" + self._delete_instance(context, instance_id) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @checks_instance_lock def stop_instance(self, context, instance_id): """Stopping an instance on this host.""" self._shutdown_instance(context, instance_id, 'Stopping') @@ -516,6 +525,30 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock + def power_off_instance(self, context, instance_id): + """Power off an instance on this host.""" + instance = self.db.instance_get(context, instance_id) + self.driver.power_off(instance) + current_power_state = self._get_power_state(context, instance) + self._instance_update(context, + instance_id, + power_state=current_power_state, + task_state=None) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @checks_instance_lock + def power_on_instance(self, context, instance_id): + """Power on an instance on this host.""" + instance = self.db.instance_get(context, instance_id) + self.driver.power_on(instance) + current_power_state = self._get_power_state(context, instance) + self._instance_update(context, + instance_id, + power_state=current_power_state, + task_state=None) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @checks_instance_lock def rebuild_instance(self, context, instance_id, **kwargs): """Destroy and re-make this instance. @@ -579,7 +612,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 +634,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, @@ -796,12 +829,18 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock - def rescue_instance(self, context, instance_id): - """Rescue an instance on this host.""" + def rescue_instance(self, context, instance_id, **kwargs): + """ + Rescue an instance on this host. + :param rescue_password: password to set on rescue instance + """ + LOG.audit(_('instance %s: rescuing'), instance_id, context=context) context = context.elevated() instance_ref = self.db.instance_get(context, instance_id) + instance_ref.admin_pass = kwargs.get('rescue_password', + utils.generate_password(FLAGS.password_length)) network_info = self._get_instance_nw_info(context, instance_ref) # NOTE(blamar): None of the virt drivers use the 'callback' param @@ -1387,11 +1426,6 @@ class ComputeManager(manager.SchedulerDependentManager): instance_ref = self.db.instance_get(context, instance_id) hostname = instance_ref['hostname'] - # Getting fixed ips - fixed_ips = self.db.instance_get_fixed_addresses(context, instance_id) - if not fixed_ips: - raise exception.FixedIpNotFoundForInstance(instance_id=instance_id) - # If any volume is mounted, prepare here. if not instance_ref['volumes']: LOG.info(_("%s has no volume."), hostname) @@ -1407,6 +1441,11 @@ class ComputeManager(manager.SchedulerDependentManager): # Retry operation is necessary because continuously request comes, # concorrent request occurs to iptables, then it complains. network_info = self._get_instance_nw_info(context, instance_ref) + + fixed_ips = [nw_info[1]['ips'] for nw_info in network_info] + if not fixed_ips: + raise exception.FixedIpNotFoundForInstance(instance_id=instance_id) + max_retry = FLAGS.live_migration_retry_count for cnt in range(max_retry): try: @@ -1643,14 +1682,23 @@ class ComputeManager(manager.SchedulerDependentManager): self.driver.poll_rescued_instances(FLAGS.rescue_timeout) except Exception as ex: LOG.warning(_("Error during poll_rescued_instances: %s"), - unicode(ex)) + unicode(ex)) + error_list.append(ex) + + try: + if FLAGS.resize_confirm_window > 0: + self.driver.poll_unconfirmed_resizes( + FLAGS.resize_confirm_window) + except Exception as ex: + LOG.warning(_("Error during poll_unconfirmed_resizes: %s"), + unicode(ex)) error_list.append(ex) try: self._report_driver_status() except Exception as ex: LOG.warning(_("Error during report_driver_status(): %s"), - unicode(ex)) + unicode(ex)) error_list.append(ex) try: @@ -1659,6 +1707,13 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.warning(_("Error during power_state sync: %s"), unicode(ex)) error_list.append(ex) + try: + self._reclaim_queued_deletes(context) + except Exception as ex: + LOG.warning(_("Error during reclamation of queued deletes: %s"), + unicode(ex)) + error_list.append(ex) + return error_list def _report_driver_status(self): @@ -1708,3 +1763,17 @@ class ComputeManager(manager.SchedulerDependentManager): self._instance_update(context, db_instance["id"], power_state=vm_power_state) + + def _reclaim_queued_deletes(self, context): + """Reclaim instances that are queued for deletion.""" + + instances = self.db.instance_get_all_by_host(context, self.host) + + queue_time = datetime.timedelta( + seconds=FLAGS.reclaim_instance_interval) + curtime = utils.utcnow() + for instance in instances: + if instance['vm_state'] == vm_states.SOFT_DELETE and \ + (curtime - instance['deleted_at']) >= queue_time: + LOG.info('Deleting %s' % instance['name']) + self._delete_instance(context, instance['id']) diff --git a/nova/compute/task_states.py b/nova/compute/task_states.py index e3315a542..b52140bf8 100644 --- a/nova/compute/task_states.py +++ b/nova/compute/task_states.py @@ -50,6 +50,8 @@ PAUSING = 'pausing' UNPAUSING = 'unpausing' SUSPENDING = 'suspending' RESUMING = 'resuming' +POWERING_OFF = 'powering-off' +POWERING_ON = 'powering-on' RESCUING = 'rescuing' UNRESCUING = 'unrescuing' diff --git a/nova/compute/vm_states.py b/nova/compute/vm_states.py index 6f16c1f09..f219bf7f4 100644 --- a/nova/compute/vm_states.py +++ b/nova/compute/vm_states.py @@ -32,6 +32,7 @@ SUSPENDED = 'suspended' RESCUED = 'rescued' DELETED = 'deleted' STOPPED = 'stopped' +SOFT_DELETE = 'soft-delete' MIGRATING = 'migrating' RESIZING = 'resizing' 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 c03a86671..8c4c78374 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): @@ -321,16 +323,23 @@ def migration_get_by_instance_and_status(context, instance_uuid, status): status) +def migration_get_all_unconfirmed(context, confirm_window): + """Finds all unconfirmed migrations within the confirmation window.""" + return IMPL.migration_get_all_unconfirmed(context, confirm_window) + + #################### -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 +374,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): @@ -458,6 +467,11 @@ def virtual_interface_delete_by_instance(context, instance_id): return IMPL.virtual_interface_delete_by_instance(context, instance_id) +def virtual_interface_get_all(context): + """Gets all virtual interfaces from the table""" + return IMPL.virtual_interface_get_all(context) + + #################### @@ -602,6 +616,11 @@ def instance_get_actions(context, instance_id): return IMPL.instance_get_actions(context, instance_id) +def instance_get_id_to_uuid_mapping(context, ids): + """Return a dictionary containing 'ID: UUID' given the ids""" + return IMPL.instance_get_id_to_uuid_mapping(context, ids) + + ################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 6aebe22f6..aa9fbda75 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -15,9 +15,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -""" -Implementation of SQLAlchemy backend. -""" + +"""Implementation of SQLAlchemy backend.""" + +import datetime import re import warnings @@ -529,7 +530,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(): floating_ip_ref = floating_ip_get_by_address(context, @@ -539,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) @@ -578,6 +581,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 @@ -670,14 +674,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').\ @@ -923,7 +932,6 @@ def virtual_interface_get(context, vif_id, session=None): vif_ref = session.query(models.VirtualInterface).\ filter_by(id=vif_id).\ options(joinedload('network')).\ - options(joinedload('instance')).\ options(joinedload('fixed_ips')).\ first() return vif_ref @@ -939,7 +947,6 @@ def virtual_interface_get_by_address(context, address): vif_ref = session.query(models.VirtualInterface).\ filter_by(address=address).\ options(joinedload('network')).\ - options(joinedload('instance')).\ options(joinedload('fixed_ips')).\ first() return vif_ref @@ -955,7 +962,6 @@ def virtual_interface_get_by_uuid(context, vif_uuid): 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 @@ -971,7 +977,6 @@ def virtual_interface_get_by_fixed_ip(context, fixed_ip_id): vif_ref = session.query(models.VirtualInterface).\ filter_by(fixed_ip_id=fixed_ip_id).\ options(joinedload('network')).\ - options(joinedload('instance')).\ options(joinedload('fixed_ips')).\ first() return vif_ref @@ -988,7 +993,6 @@ def virtual_interface_get_by_instance(context, instance_id): vif_refs = session.query(models.VirtualInterface).\ filter_by(instance_id=instance_id).\ options(joinedload('network')).\ - options(joinedload('instance')).\ options(joinedload('fixed_ips')).\ all() return vif_refs @@ -1003,7 +1007,6 @@ def virtual_interface_get_by_instance_and_network(context, instance_id, filter_by(instance_id=instance_id).\ filter_by(network_id=network_id).\ options(joinedload('network')).\ - options(joinedload('instance')).\ options(joinedload('fixed_ips')).\ first() return vif_ref @@ -1019,7 +1022,6 @@ def virtual_interface_get_by_network(context, network_id): vif_refs = session.query(models.VirtualInterface).\ filter_by(network_id=network_id).\ options(joinedload('network')).\ - options(joinedload('instance')).\ options(joinedload('fixed_ips')).\ all() return vif_refs @@ -1049,6 +1051,17 @@ def virtual_interface_delete_by_instance(context, instance_id): virtual_interface_delete(context, vif_ref['id']) +@require_context +def virtual_interface_get_all(context): + """Get all vifs""" + session = get_session() + vif_refs = session.query(models.VirtualInterface).\ + options(joinedload('network')).\ + options(joinedload('fixed_ips')).\ + all() + return vif_refs + + ################### @@ -1165,7 +1178,6 @@ def _build_instance_get(context, session=None): partial = session.query(models.Instance).\ options(joinedload_all('fixed_ips.floating_ips')).\ options(joinedload_all('fixed_ips.network')).\ - options(joinedload('virtual_interfaces')).\ options(joinedload_all('security_groups.rules')).\ options(joinedload('volumes')).\ options(joinedload('metadata')).\ @@ -1184,10 +1196,6 @@ def instance_get_all(context): session = get_session() return session.query(models.Instance).\ options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload_all('virtual_interfaces.network')).\ - options(joinedload_all( - 'virtual_interfaces.fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces.instance')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ @@ -1202,27 +1210,6 @@ def instance_get_all_by_filters(context, filters): will be returned by default, unless there's a filter that says otherwise""" - def _regexp_filter_by_ipv6(instance, filter_re): - for interface in instance['virtual_interfaces']: - fixed_ipv6 = interface.get('fixed_ipv6') - if fixed_ipv6 and filter_re.match(fixed_ipv6): - return True - return False - - def _regexp_filter_by_ip(instance, filter_re): - for interface in instance['virtual_interfaces']: - for fixed_ip in interface['fixed_ips']: - if not fixed_ip or not fixed_ip['address']: - continue - if filter_re.match(fixed_ip['address']): - return True - for floating_ip in fixed_ip.get('floating_ips', []): - if not floating_ip or not floating_ip['address']: - continue - if filter_re.match(floating_ip['address']): - return True - return False - def _regexp_filter_by_metadata(instance, meta): inst_metadata = [{node['key']: node['value']} \ for node in instance['metadata']] @@ -1249,7 +1236,7 @@ def instance_get_all_by_filters(context, filters): """Do exact match against a column. value to match can be a list so you can match any value in the list. """ - if isinstance(value, list): + if isinstance(value, list) or isinstance(value, set): column_attr = getattr(models.Instance, column) return query.filter(column_attr.in_(value)) else: @@ -1259,13 +1246,7 @@ def instance_get_all_by_filters(context, filters): session = get_session() query_prefix = session.query(models.Instance).\ - options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload_all('virtual_interfaces.network')).\ - options(joinedload_all( - 'virtual_interfaces.fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces.instance')).\ options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')).\ order_by(desc(models.Instance.created_at)) @@ -1289,7 +1270,7 @@ def instance_get_all_by_filters(context, filters): # Filters for exact matches that we can do along with the SQL query... # For other filters that don't match this, we will do regexp matching exact_match_filter_names = ['project_id', 'user_id', 'image_ref', - 'vm_state', 'instance_type_id', 'deleted'] + 'vm_state', 'instance_type_id', 'deleted', 'uuid'] query_filters = [key for key in filters.iterkeys() if key in exact_match_filter_names] @@ -1301,15 +1282,13 @@ def instance_get_all_by_filters(context, filters): filters.pop(filter_name)) instances = query_prefix.all() - if not instances: return [] # Now filter on everything else for regexp matching.. # For filters not in the list, we'll attempt to use the filter_name # as a column name in Instance.. - regexp_filter_funcs = {'ip6': _regexp_filter_by_ipv6, - 'ip': _regexp_filter_by_ip} + regexp_filter_funcs = {} for filter_name in filters.iterkeys(): filter_func = regexp_filter_funcs.get(filter_name, None) @@ -1323,6 +1302,8 @@ def instance_get_all_by_filters(context, filters): filter_l = lambda instance: _regexp_filter_by_column(instance, filter_name, filter_re) instances = filter(filter_l, instances) + if not instances: + break return instances @@ -1369,7 +1350,6 @@ def instance_get_all_by_user(context, user_id): session = get_session() return session.query(models.Instance).\ options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ @@ -1384,7 +1364,6 @@ def instance_get_all_by_host(context, host): session = get_session() return session.query(models.Instance).\ options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ @@ -1401,7 +1380,6 @@ def instance_get_all_by_project(context, project_id): session = get_session() return session.query(models.Instance).\ options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ @@ -1417,7 +1395,6 @@ def instance_get_all_by_reservation(context, reservation_id): query = session.query(models.Instance).\ filter_by(reservation_id=reservation_id).\ options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ @@ -1434,36 +1411,11 @@ def instance_get_all_by_reservation(context, reservation_id): all() -@require_context -def instance_get_by_fixed_ip(context, address): - """Return instance ref by exact match of FixedIP""" - fixed_ip_ref = fixed_ip_get_by_address(context, address) - return fixed_ip_ref.instance - - -@require_context -def instance_get_by_fixed_ipv6(context, address): - """Return instance ref by exact match of IPv6""" - session = get_session() - - # convert IPv6 address to mac - mac = ipv6.to_mac(address) - - # get virtual interface - vif_ref = virtual_interface_get_by_address(context, mac) - - # look up instance based on instance_id from vif row - result = session.query(models.Instance).\ - filter_by(id=vif_ref['instance_id']) - return result - - @require_admin_context def instance_get_project_vpn(context, project_id): session = get_session() return session.query(models.Instance).\ options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ @@ -1595,6 +1547,18 @@ def instance_get_actions(context, instance_id): all() +@require_context +def instance_get_id_to_uuid_mapping(context, ids): + session = get_session() + instances = session.query(models.Instance).\ + filter(models.Instance.id.in_(ids)).\ + all() + mapping = {} + for instance in instances: + mapping[instance['id']] = instance['uuid'] + return mapping + + ################### @@ -2820,12 +2784,14 @@ def security_group_rule_get_by_security_group(context, security_group_id, result = session.query(models.SecurityGroupIngressRule).\ filter_by(deleted=can_read_deleted(context)).\ filter_by(parent_group_id=security_group_id).\ + options(joinedload_all('grantee_group')).\ all() else: # TODO(vish): Join to group and check for project_id result = session.query(models.SecurityGroupIngressRule).\ filter_by(deleted=False).\ filter_by(parent_group_id=security_group_id).\ + options(joinedload_all('grantee_group')).\ all() return result @@ -3190,6 +3156,21 @@ def migration_get_by_instance_and_status(context, instance_uuid, status): return result +@require_admin_context +def migration_get_all_unconfirmed(context, confirm_window, session=None): + confirm_window = datetime.datetime.utcnow() - datetime.timedelta( + seconds=confirm_window) + + if not session: + session = get_session() + + results = session.query(models.Migration).\ + filter(models.Migration.updated_at <= confirm_window).\ + filter_by(status="FINISHED").all() + + return results + + ################## 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/migrate_repo/versions/047_remove_instances_fk_from_vif.py b/nova/db/sqlalchemy/migrate_repo/versions/047_remove_instances_fk_from_vif.py new file mode 100644 index 000000000..dadcefc39 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/047_remove_instances_fk_from_vif.py @@ -0,0 +1,60 @@ +# Copyright 2011 OpenStack LLC. +# +# 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 +from migrate import ForeignKeyConstraint + +from nova import log as logging + +meta = MetaData() + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + dialect = migrate_engine.url.get_dialect().name + if dialect.startswith('sqlite'): + return + + instances = Table('instances', meta, autoload=True) + vifs = Table('virtual_interfaces', meta, autoload=True) + + try: + fkey_name = vifs.c.instance_id.foreign_keys[0].constraint.name + ForeignKeyConstraint(columns=[vifs.c.instance_id], + refcolumns=[instances.c.id], + name=fkey_name).drop() + + except Exception: + logging.error(_("foreign key constraint couldn't be removed")) + raise + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + dialect = migrate_engine.url.get_dialect().name + if dialect.startswith('sqlite'): + return + + instances = Table('instances', meta, autoload=True) + vifs = Table('virtual_interfaces', meta, autoload=True) + + try: + ForeignKeyConstraint(columns=[vifs.c.instance_id], + refcolumns=[instances.c.id]).create() + except Exception: + logging.error(_("foreign key constraint couldn't be added")) + raise diff --git a/nova/db/sqlalchemy/migrate_repo/versions/047_sqlite_downgrade.sql b/nova/db/sqlalchemy/migrate_repo/versions/047_sqlite_downgrade.sql new file mode 100644 index 000000000..cf9afbb09 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/047_sqlite_downgrade.sql @@ -0,0 +1,47 @@ +COMMIT; +BEGIN TRANSACTION; + CREATE TEMPORARY TABLE virtual_interfaces_backup ( + created_at DATETIME, + updated_at DATETIME, + deleted_at DATETIME, + deleted BOOLEAN, + id INTEGER NOT NULL, + address VARCHAR(255), + network_id INTEGER, + instance_id INTEGER NOT NULL, + uuid VARCHAR(36), + PRIMARY KEY (id) + ); + + INSERT INTO virtual_interfaces_backup + SELECT created_at, updated_at, deleted_at, deleted, id, address, + network_id, instance_id, uuid + FROM virtual_interfaces; + + DROP TABLE virtual_interfaces; + + CREATE TABLE virtual_interfaces ( + created_at DATETIME, + updated_at DATETIME, + deleted_at DATETIME, + deleted BOOLEAN, + id INTEGER NOT NULL, + address VARCHAR(255), + network_id INTEGER, + instance_id INTEGER NOT NULL, + uuid VARCHAR(36), + PRIMARY KEY (id), + FOREIGN KEY(network_id) REFERENCES networks (id), + FOREIGN KEY(instance_id) REFERENCES instances (id), + UNIQUE (address), + CHECK (deleted IN (0, 1)) + ); + + INSERT INTO virtual_interfaces + SELECT created_at, updated_at, deleted_at, deleted, id, address, + network_id, instance_id, uuid + FROM virtual_interfaces_backup; + + DROP TABLE virtual_interfaces_backup; + +COMMIT; diff --git a/nova/db/sqlalchemy/migrate_repo/versions/047_sqlite_upgrade.sql b/nova/db/sqlalchemy/migrate_repo/versions/047_sqlite_upgrade.sql new file mode 100644 index 000000000..2c0919f1d --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/047_sqlite_upgrade.sql @@ -0,0 +1,45 @@ +BEGIN TRANSACTION; + CREATE TEMPORARY TABLE virtual_interfaces_backup ( + created_at DATETIME, + updated_at DATETIME, + deleted_at DATETIME, + deleted BOOLEAN, + id INTEGER NOT NULL, + address VARCHAR(255), + network_id INTEGER, + instance_id INTEGER NOT NULL, + uuid VARCHAR(36), + PRIMARY KEY (id) + ); + + INSERT INTO virtual_interfaces_backup + SELECT created_at, updated_at, deleted_at, deleted, id, address, + network_id, instance_id, uuid + FROM virtual_interfaces; + + DROP TABLE virtual_interfaces; + + CREATE TABLE virtual_interfaces ( + created_at DATETIME, + updated_at DATETIME, + deleted_at DATETIME, + deleted BOOLEAN, + id INTEGER NOT NULL, + address VARCHAR(255), + network_id INTEGER, + instance_id INTEGER NOT NULL, + uuid VARCHAR(36), + PRIMARY KEY (id), + FOREIGN KEY(network_id) REFERENCES networks (id), + UNIQUE (address), + CHECK (deleted IN (0, 1)) + ); + + INSERT INTO virtual_interfaces + SELECT created_at, updated_at, deleted_at, deleted, id, address, + network_id, instance_id, uuid + FROM virtual_interfaces_backup; + + DROP TABLE virtual_interfaces_backup; + +COMMIT; diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 211049112..089cd5450 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 @@ -640,10 +642,7 @@ class VirtualInterface(BASE, NovaBase): address = Column(String(255), unique=True) network_id = Column(Integer, ForeignKey('networks.id')) network = relationship(Network, backref=backref('virtual_interfaces')) - - # TODO(tr3buchet): cut the cord, removed foreign key and backrefs - instance_id = Column(Integer, ForeignKey('instances.id'), nullable=False) - instance = relationship(Instance, backref=backref('virtual_interfaces')) + instance_id = Column(Integer, nullable=False) uuid = Column(String(36)) diff --git a/nova/exception.py b/nova/exception.py index a3cbb98cf..4f25d3721 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -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.") diff --git a/nova/flags.py b/nova/flags.py index aa76defe5..ffb313cec 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -271,8 +271,10 @@ DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_string('aws_access_key_id', 'admin', 'AWS Access ID') DEFINE_string('aws_secret_access_key', 'admin', 'AWS Access Key') # NOTE(sirp): my_ip interpolation doesn't work within nested structures +DEFINE_string('glance_host', _get_my_ip(), 'default glance host') +DEFINE_integer('glance_port', 9292, 'default glance port') DEFINE_list('glance_api_servers', - ['%s:9292' % _get_my_ip()], + ['%s:%d' % (FLAGS.glance_host, FLAGS.glance_port)], 'list of glance api servers available to nova (host:port)') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '$my_ip', 's3 host (for infrastructure)') @@ -421,6 +423,9 @@ DEFINE_string('root_helper', 'sudo', DEFINE_bool('use_ipv6', False, 'use ipv6') +DEFINE_integer('password_length', 12, + 'Length of generated instance admin passwords') + DEFINE_bool('monkey_patch', False, 'Whether to log monkey patching') 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/api.py b/nova/network/api.py index 78580d360..a1ed28496 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -46,8 +46,9 @@ class API(base.Base): return ips def get_vifs_by_instance(self, context, instance_id): - vifs = self.db.virtual_interface_get_by_instance(context, instance_id) - return vifs + return rpc.call(context, FLAGS.network_topic, + {'method': 'get_vifs_by_instance', + 'args': {'instance_id': instance_id}}) def allocate_floating_ip(self, context): """Adds a floating ip to a project.""" @@ -210,3 +211,12 @@ class API(base.Base): return rpc.call(context, FLAGS.network_topic, {'method': 'validate_networks', 'args': args}) + + def get_instance_uuids_by_ip_filter(self, context, filters): + """Returns a list of dicts in the form of + {'instance_uuid': uuid, 'ip': ip} that matched the ip_filter + """ + args = {'filters': filters} + return rpc.call(context, FLAGS.network_topic, + {'method': 'get_instance_uuids_by_ip_filter', + 'args': args}) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 7d89b2bcc..0459b4aeb 100755 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -68,6 +68,8 @@ 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('send_arp_for_ha', False, + 'send gratuitous ARPs for HA setup') flags.DEFINE_bool('use_single_default_gateway', False, 'Use single default gateway. Only first nic of vm' ' will get default gateway from dhcp server') @@ -407,6 +409,10 @@ def bind_floating_ip(floating_ip, check_exit_code=True): _execute('ip', 'addr', 'add', floating_ip, 'dev', FLAGS.public_interface, run_as_root=True, check_exit_code=check_exit_code) + if FLAGS.send_arp_for_ha: + _execute('arping', '-U', floating_ip, + '-A', '-I', FLAGS.public_interface, + '-c', 1, run_as_root=True, check_exit_code=False) def unbind_floating_ip(floating_ip): @@ -466,18 +472,30 @@ def initialize_gateway_device(dev, network_ref): # NOTE(vish): The ip for dnsmasq has to be the first address on the # bridge for it to respond to reqests properly - suffix = network_ref['cidr'].rpartition('/')[2] - out, err = _execute('ip', 'addr', 'add', - '%s/%s' % - (network_ref['dhcp_server'], suffix), - 'brd', - network_ref['broadcast'], - 'dev', - dev, - run_as_root=True, - check_exit_code=False) - if err and err != 'RTNETLINK answers: File exists\n': - raise exception.Error('Failed to add ip: %s' % err) + full_ip = '%s/%s' % (network_ref['dhcp_server'], + network_ref['cidr'].rpartition('/')[2]) + new_ip_params = [[full_ip, 'brd', network_ref['broadcast']]] + old_ip_params = [] + out, err = _execute('ip', 'addr', 'show', 'dev', dev, + 'scope', 'global', run_as_root=True) + for line in out.split('\n'): + fields = line.split() + if fields and fields[0] == 'inet': + ip_params = fields[1:-1] + old_ip_params.append(ip_params) + if ip_params[0] != full_ip: + new_ip_params.append(ip_params) + if not old_ip_params or old_ip_params[0][0] != full_ip: + for ip_params in old_ip_params: + _execute(*_ip_bridge_cmd('del', ip_params, dev), + run_as_root=True) + for ip_params in new_ip_params: + _execute(*_ip_bridge_cmd('add', ip_params, dev), + run_as_root=True) + if FLAGS.send_arp_for_ha: + _execute('arping', '-U', network_ref['dhcp_server'], + '-A', '-I', dev, + '-c', 1, run_as_root=True, check_exit_code=False) if(FLAGS.use_ipv6): _execute('ip', '-f', 'inet6', 'addr', 'change', network_ref['cidr_v6'], @@ -514,6 +532,18 @@ def get_dhcp_hosts(context, network_ref): return '\n'.join(hosts) +def _add_dnsmasq_accept_rules(dev): + """Allow DHCP and DNS traffic through to dnsmasq.""" + table = iptables_manager.ipv4['filter'] + for port in [67, 53]: + for proto in ['udp', 'tcp']: + args = {'dev': dev, 'port': port, 'proto': proto} + table.add_rule('INPUT', + '-i %(dev)s -p %(proto)s -m %(proto)s ' + '--dport %(port)s -j ACCEPT' % args) + iptables_manager.apply() + + def get_dhcp_opts(context, network_ref): """Get network's hosts config in dhcp-opts format.""" hosts = [] @@ -540,6 +570,10 @@ def get_dhcp_opts(context, network_ref): return '\n'.join(hosts) +def release_dhcp(dev, address, mac_address): + utils.execute('dhcp_release', dev, address, mac_address, run_as_root=True) + + # NOTE(ja): Sending a HUP only reloads the hostfile, so any # configuration options (like dchp-range, vlan, ...) # aren't reloaded. @@ -584,7 +618,6 @@ def update_dhcp(context, dev, network_ref): 'dnsmasq', '--strict-order', '--bind-interfaces', - '--interface=%s' % dev, '--conf-file=%s' % FLAGS.dnsmasq_config_file, '--domain=%s' % FLAGS.dhcp_domain, '--pid-file=%s' % _dhcp_file(dev, 'pid'), @@ -603,6 +636,8 @@ def update_dhcp(context, dev, network_ref): _execute(*cmd, run_as_root=True) + _add_dnsmasq_accept_rules(dev) + @utils.synchronized('radvd_start') def update_ra(context, dev, network_ref): @@ -790,6 +825,10 @@ def unplug(network): return interface_driver.unplug(network) +def get_dev(network): + return interface_driver.get_dev(network) + + class LinuxNetInterfaceDriver(object): """Abstract class that defines generic network host API""" """ for for all Linux interface drivers.""" @@ -802,6 +841,10 @@ class LinuxNetInterfaceDriver(object): """Destory Linux device, return device name""" raise NotImplementedError() + def get_dev(self, network): + """Get device name""" + raise NotImplementedError() + # plugs interfaces using Linux Bridge class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): @@ -823,6 +866,9 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): return network['bridge'] def unplug(self, network): + return self.get_dev(network) + + def get_dev(self, network): return network['bridge'] @classmethod @@ -947,6 +993,9 @@ class LinuxOVSInterfaceDriver(LinuxNetInterfaceDriver): return dev def unplug(self, network): + return self.get_dev(network) + + def get_dev(self, network): dev = "gw-" + str(network['id']) return dev diff --git a/nova/network/manager.py b/nova/network/manager.py index 05d928fab..ffb9f976c 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -48,6 +48,7 @@ import datetime import itertools import math import netaddr +import re import socket from eventlet import greenpool @@ -110,6 +111,8 @@ flags.DEFINE_string('network_host', socket.gethostname(), 'Network host to use for ip allocation in flat modes') flags.DEFINE_bool('fake_call', False, 'If True, skip using the queue and make local calls') +flags.DEFINE_bool('force_dhcp_release', False, + 'If True, send a dhcp release on instance termination') class AddressAlreadyAllocated(exception.Error): @@ -289,7 +292,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) @@ -396,6 +400,59 @@ class NetworkManager(manager.SchedulerDependentManager): self.compute_api.trigger_security_group_members_refresh(admin_context, group_ids) + def get_vifs_by_instance(self, context, instance_id): + vifs = self.db.virtual_interface_get_by_instance(context, + instance_id) + return vifs + + def get_instance_uuids_by_ip_filter(self, context, filters): + fixed_ip_filter = filters.get('fixed_ip') + ip_filter = re.compile(str(filters.get('ip'))) + ipv6_filter = re.compile(str(filters.get('ip6'))) + + # NOTE(jkoelker) Should probably figure out a better way to do + # this. But for now it "works", this could suck on + # large installs. + + vifs = self.db.virtual_interface_get_all(context) + results = [] + + for vif in vifs: + if vif['instance_id'] is None: + continue + + fixed_ipv6 = vif.get('fixed_ipv6') + if fixed_ipv6 and ipv6_filter.match(fixed_ipv6): + # NOTE(jkoelker) Will need to update for the UUID flip + results.append({'instance_id': vif['instance_id'], + 'ip': fixed_ipv6}) + + for fixed_ip in vif['fixed_ips']: + if not fixed_ip or not fixed_ip['address']: + continue + if fixed_ip['address'] == fixed_ip_filter: + results.append({'instance_id': vif['instance_id'], + 'ip': fixed_ip['address']}) + continue + if ip_filter.match(fixed_ip['address']): + results.append({'instance_id': vif['instance_id'], + 'ip': fixed_ip['address']}) + continue + for floating_ip in fixed_ip.get('floating_ips', []): + if not floating_ip or not floating_ip['address']: + continue + if ip_filter.match(floating_ip['address']): + results.append({'instance_id': vif['instance_id'], + 'ip': floating_ip['address']}) + continue + + # NOTE(jkoelker) Until we switch over to instance_uuid ;) + ids = [res['instance_id'] for res in results] + uuid_map = self.db.instance_get_id_to_uuid_mapping(context, ids) + for res in results: + res['instance_uuid'] = uuid_map.get(res['instance_id']) + return results + def _get_networks_for_instance(self, context, instance_id, project_id, requested_networks=None): """Determine & return which networks an instance should connect to.""" @@ -628,6 +685,11 @@ class NetworkManager(manager.SchedulerDependentManager): instance_id = instance_ref['id'] self._do_trigger_security_group_members_refresh_for_instance( instance_id) + if FLAGS.force_dhcp_release: + dev = self.driver.get_dev(fixed_ip_ref['network']) + vif = self.db.virtual_interface_get_by_instance_and_network( + context, instance_ref['id'], fixed_ip_ref['network']['id']) + self.driver.release_dhcp(dev, address, vif['address']) def lease_fixed_ip(self, context, address): """Called by dhcp-bridge when ip is leased.""" @@ -1005,7 +1067,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/scheduler/abstract_scheduler.py b/nova/scheduler/abstract_scheduler.py index e5ea0f4e4..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 diff --git a/nova/scheduler/base_scheduler.py b/nova/scheduler/base_scheduler.py index e9c078b81..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') @@ -68,4 +70,9 @@ class BaseScheduler(abstract_scheduler.AbstractScheduler): 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/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 3fe6a9b42..cc85cbd95 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py @@ -305,6 +305,61 @@ class CloudTestCase(test.TestCase): 'ip_protocol': u'tcp'}]} self.assertTrue(authz(self.context, group_name=sec['name'], **kwargs)) + def test_describe_security_group_ingress_groups(self): + kwargs = {'project_id': self.context.project_id, 'name': 'test'} + sec1 = db.security_group_create(self.context, kwargs) + sec2 = db.security_group_create(self.context, + {'project_id': 'someuser', + 'name': 'somegroup1'}) + sec3 = db.security_group_create(self.context, + {'project_id': 'someuser', + 'name': 'othergroup2'}) + authz = self.cloud.authorize_security_group_ingress + kwargs = {'ip_permissions': [ + {'groups': {'1': {'user_id': u'someuser', + 'group_name': u'somegroup1'}}}, + {'ip_protocol': 'tcp', + 'from_port': 80, + 'to_port': 80, + 'groups': {'1': {'user_id': u'someuser', + 'group_name': u'othergroup2'}}}]} + self.assertTrue(authz(self.context, group_name=sec1['name'], **kwargs)) + describe = self.cloud.describe_security_groups + groups = describe(self.context, group_name=['test']) + self.assertEquals(len(groups['securityGroupInfo']), 1) + actual_rules = groups['securityGroupInfo'][0]['ipPermissions'] + self.assertEquals(len(actual_rules), 4) + expected_rules = [{'fromPort': -1, + 'groups': [{'groupName': 'somegroup1', + 'userId': 'someuser'}], + 'ipProtocol': 'icmp', + 'ipRanges': [], + 'toPort': -1}, + {'fromPort': 1, + 'groups': [{'groupName': u'somegroup1', + 'userId': u'someuser'}], + 'ipProtocol': 'tcp', + 'ipRanges': [], + 'toPort': 65535}, + {'fromPort': 1, + 'groups': [{'groupName': u'somegroup1', + 'userId': u'someuser'}], + 'ipProtocol': 'udp', + 'ipRanges': [], + 'toPort': 65536}, + {'fromPort': 80, + 'groups': [{'groupName': u'othergroup2', + 'userId': u'someuser'}], + 'ipProtocol': u'tcp', + 'ipRanges': [], + 'toPort': 80}] + for rule in expected_rules: + self.assertTrue(rule in actual_rules) + + db.security_group_destroy(self.context, sec3['id']) + db.security_group_destroy(self.context, sec2['id']) + db.security_group_destroy(self.context, sec1['id']) + def test_revoke_security_group_ingress(self): kwargs = {'project_id': self.context.project_id, 'name': 'test'} sec = db.security_group_create(self.context, kwargs) @@ -486,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): @@ -1542,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 078b72d67..03c7d1ec5 100644 --- a/nova/tests/api/openstack/contrib/test_createserverext.py +++ b/nova/tests/api/openstack/contrib/test_createserverext.py @@ -49,9 +49,13 @@ 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"}] + "security_groups": [{"id": 1, "name": "test"}], + "image_ref": 'http://foo.com/123', + "instance_type": {"flavorid": '124'}, } diff --git a/nova/tests/api/openstack/contrib/test_rescue.py b/nova/tests/api/openstack/contrib/test_rescue.py index f8126d461..403bcfd4c 100644 --- a/nova/tests/api/openstack/contrib/test_rescue.py +++ b/nova/tests/api/openstack/contrib/test_rescue.py @@ -16,11 +16,14 @@ import json import webob from nova import compute +from nova import flags from nova import test from nova.tests.api.openstack import fakes +FLAGS = flags.FLAGS -def rescue(self, context, instance_id): + +def rescue(self, context, instance_id, rescue_password=None): pass @@ -34,7 +37,19 @@ class RescueTest(test.TestCase): self.stubs.Set(compute.api.API, "rescue", rescue) self.stubs.Set(compute.api.API, "unrescue", unrescue) - def test_rescue(self): + def test_rescue_with_preset_password(self): + body = {"rescue": {"adminPass": "AABBCC112233"}} + 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, 200) + resp_json = json.loads(resp.body) + self.assertEqual("AABBCC112233", resp_json['adminPass']) + + def test_rescue_generates_password(self): body = dict(rescue=None) req = webob.Request.blank('/v1.1/123/servers/test_inst/action') req.method = "POST" @@ -43,6 +58,8 @@ class RescueTest(test.TestCase): resp = req.get_response(fakes.wsgi_app()) self.assertEqual(resp.status_int, 200) + resp_json = json.loads(resp.body) + self.assertEqual(FLAGS.password_length, len(resp_json['adminPass'])) def test_unrescue(self): body = dict(unrescue=None) @@ -52,4 +69,4 @@ class RescueTest(test.TestCase): req.headers["content-type"] = "application/json" resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) + self.assertEqual(resp.status_int, 202) diff --git a/nova/tests/api/openstack/contrib/test_virtual_interfaces.py b/nova/tests/api/openstack/contrib/test_virtual_interfaces.py index 1db253b35..0260e89d4 100644 --- a/nova/tests/api/openstack/contrib/test_virtual_interfaces.py +++ b/nova/tests/api/openstack/contrib/test_virtual_interfaces.py @@ -14,22 +14,20 @@ # under the License. import json -import stubout import webob from nova import test -from nova import compute +from nova import network from nova.tests.api.openstack import fakes from nova.api.openstack.contrib.virtual_interfaces import \ ServerVirtualInterfaceController -def compute_api_get(self, context, server_id): - return {'virtual_interfaces': [ - {'uuid': '00000000-0000-0000-0000-00000000000000000', - 'address': '00-00-00-00-00-00'}, - {'uuid': '11111111-1111-1111-1111-11111111111111111', - 'address': '11-11-11-11-11-11'}]} +def get_vifs_by_instance(self, context, server_id): + return [{'uuid': '00000000-0000-0000-0000-00000000000000000', + 'address': '00-00-00-00-00-00'}, + {'uuid': '11111111-1111-1111-1111-11111111111111111', + 'address': '11-11-11-11-11-11'}] class ServerVirtualInterfaceTest(test.TestCase): @@ -37,7 +35,8 @@ class ServerVirtualInterfaceTest(test.TestCase): def setUp(self): super(ServerVirtualInterfaceTest, self).setUp() self.controller = ServerVirtualInterfaceController() - self.stubs.Set(compute.api.API, "get", compute_api_get) + self.stubs.Set(network.api.API, "get_vifs_by_instance", + get_vifs_by_instance) def tearDown(self): super(ServerVirtualInterfaceTest, self).tearDown() 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..52b65f5e1 --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_volumes.py @@ -0,0 +1,77 @@ +# 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 flags +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 + + +FLAGS = flags.FLAGS + + +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(FLAGS.password_length, 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..ca36523e4 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -86,7 +86,9 @@ class ExtensionControllerTest(test.TestCase): self.flags(osapi_extensions_path=ext_path) self.ext_list = [ "Createserverext", + "DeferredDelete", "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..886efb6ac 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -22,340 +22,59 @@ 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.api.openstack.views import images as images_view +from nova import test +from nova import utils 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 +84,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,36 +103,38 @@ 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) href = "http://localhost/v1.1/fake/images/124" bookmark = "http://localhost/fake/images/124" + alternate = "%s/fake/images/124" % utils.generate_glance_url() server_href = "http://localhost/v1.1/servers/42" server_bookmark = "http://localhost/servers/42" 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, @@ -431,6 +155,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { "rel": "bookmark", "href": bookmark, + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate }], }, } @@ -440,11 +169,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 +188,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 +232,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 +267,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 +278,181 @@ 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": "http://localhost/v1.1/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/123" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "124", + "name": "queued snapshot", "links": [ { "rel": "self", - "href": href, + "href": "http://localhost/v1.1/fake/images/124", }, { "rel": "bookmark", - "href": bookmark, + "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/124" % + utils.generate_glance_url() }, ], - } - self.assertTrue(test_image in response_list) + }, + { + "id": "125", + "name": "saving snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/125", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/125", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/125" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "126", + "name": "active snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/126", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/126", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/126" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "127", + "name": "killed snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/127", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/127", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/127" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "128", + "name": "deleted snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/128", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/128", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/128" % + utils.generate_glance_url() + }, + ], + }, + { + "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", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/129" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "130", + "name": None, + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/130", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/130", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/130" % + utils.generate_glance_url() + }, + ], + }, + ] - 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 +460,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 +526,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 +535,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": [{ @@ -665,21 +549,26 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/123" % utils.generate_glance_url() }], }, { - '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, @@ -696,21 +585,26 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/124" % utils.generate_glance_url() }], }, { - '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, @@ -727,21 +621,26 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/125", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/125" % utils.generate_glance_url() }], }, { - '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, @@ -758,21 +657,26 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/126", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/126" % utils.generate_glance_url() }], }, { - '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, @@ -789,23 +693,105 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/127", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/127" % utils.generate_glance_url() }], }, { - '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", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/128" % utils.generate_glance_url() + }], + }, + { + '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", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/129" % utils.generate_glance_url() + }], + }, + { + '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", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/130" % utils.generate_glance_url() }], }, ] @@ -1017,11 +1003,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 +1017,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,48 +1059,11 @@ 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 + def test_generate_alternate(self): + view = images_view.ViewBuilderV11(1) + generated_url = view.generate_alternate(1) + actual_url = "%s//images/1" % utils.generate_glance_url() + self.assertEqual(generated_url, actual_url) class ImageXMLSerializationTest(test.TestCase): @@ -1132,7 +1074,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 +1086,7 @@ class ImageXMLSerializationTest(test.TestCase): 'status': 'ACTIVE', 'progress': 80, 'server': { - 'id': 1, + 'id': '1', 'links': [ { 'href': self.SERVER_HREF, @@ -1173,37 +1115,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 +1201,7 @@ class ImageXMLSerializationTest(test.TestCase): 'updated': self.TIMESTAMP, 'status': 'ACTIVE', 'server': { - 'id': 1, + 'id': '1', 'links': [ { 'href': self.SERVER_HREF, @@ -1243,31 +1228,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 +1265,7 @@ class ImageXMLSerializationTest(test.TestCase): 'updated': self.TIMESTAMP, 'status': 'ACTIVE', 'server': { - 'id': 1, + 'id': '1', 'links': [ { 'href': self.SERVER_HREF, @@ -1306,31 +1291,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 +1344,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 +1382,10 @@ class ImageXMLSerializationTest(test.TestCase): 'href': self.IMAGE_HREF % 1, 'rel': 'self', }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, ], }, { @@ -1407,35 +1396,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 +1431,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 +1449,7 @@ class ImageXMLSerializationTest(test.TestCase): 'updated': self.TIMESTAMP, 'status': 'ACTIVE', 'server': { - 'id': 1, + 'id': '1', 'links': [ { 'href': self.SERVER_HREF, @@ -1491,7 +1473,7 @@ class ImageXMLSerializationTest(test.TestCase): ], }, { - 'id': 2, + 'id': '2', 'name': 'Image2', 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, @@ -1515,46 +1497,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_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index b9ef41465..4a215dd74 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -622,7 +622,8 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(res.status_int, 202) body = json.loads(res.body) self.assertEqual(body['server']['image']['id'], '2') - self.assertEqual(len(body['server']['adminPass']), 16) + self.assertEqual(len(body['server']['adminPass']), + FLAGS.password_length) def test_server_rebuild_rejected_when_building(self): body = { diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index d063a60c2..c21fb4a62 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -28,6 +28,7 @@ import webob from nova import context from nova import db from nova import exception +from nova import flags from nova import test from nova import utils import nova.api.openstack @@ -49,9 +50,14 @@ from nova.tests.api.openstack import common from nova.tests.api.openstack import fakes +FLAGS = flags.FLAGS 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(): @@ -412,12 +418,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 = [ @@ -441,50 +442,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" - userId="fake" - tenantId="fake" - 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" @@ -1208,7 +1247,7 @@ class ServersTest(test.TestCase): 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): + def fake_get_all(context, filters=None, instances=None): self.assertNotEqual(filters, None) self.assertEqual(filters['project_id'], 'faketenant') self.assertFalse(filters.get('tenant_id')) @@ -1538,7 +1577,7 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 202) server = json.loads(res.body)['server'] - self.assertEqual(16, len(server['adminPass'])) + self.assertEqual(FLAGS.password_length, len(server['adminPass'])) self.assertEqual('server_test', server['name']) self.assertEqual(1, server['id']) self.assertEqual(2, server['flavorId']) @@ -1739,7 +1778,7 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 202) server = json.loads(res.body)['server'] - self.assertEqual(16, len(server['adminPass'])) + self.assertEqual(FLAGS.password_length, len(server['adminPass'])) self.assertEqual(1, server['id']) self.assertEqual(0, server['progress']) self.assertEqual('server_test', server['name']) @@ -1799,7 +1838,7 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 202) server = json.loads(res.body)['server'] - self.assertEqual(16, len(server['adminPass'])) + self.assertEqual(FLAGS.password_length, len(server['adminPass'])) self.assertEqual(1, server['id']) self.assertEqual("BUILD", server["status"]) self.assertEqual(0, server['progress']) @@ -2155,6 +2194,58 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_create_instance_v1_1_malformed_entity(self): + self._setup_for_create_instance() + req = webob.Request.blank('/v1.1/fake/servers') + req.method = 'POST' + req.body = json.dumps({'server': 'string'}) + req.headers['content-type'] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_create_instance_v1_1_malformed_body_string(self): + self._setup_for_create_instance() + req = webob.Request.blank('/v1.1/fake/servers') + req.method = 'POST' + req.body = 'string' + req.headers['content-type'] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_create_instance_v1_1_malformed_body_list(self): + self._setup_for_create_instance() + body = ['string'] + req = webob.Request.blank('/v1.1/fake/servers') + req.method = 'POST' + req.body = json.dumps(['string']) + req.headers['content-type'] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 422) + + def test_create_instance_v1_0_malformed_entity(self): + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps({'server': 'string'}) + req.headers['content-type'] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_create_instance_v1_0_malformed_body_string(self): + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = 'string' + req.headers['content-type'] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_create_instance_v1_0_malformed_body_list(self): + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps(['string']) + req.headers['content-type'] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 422) + def test_update_server_no_body(self): req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' @@ -2510,9 +2601,8 @@ class ServersTest(test.TestCase): self.assertEqual(res.status, '202 Accepted') self.assertEqual(self.server_delete_called, True) - def test_rescue_accepted(self): + def test_rescue_generates_password(self): self.flags(allow_admin_api=True) - body = {} self.called = False @@ -2527,7 +2617,33 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(self.called, True) - self.assertEqual(res.status_int, 202) + self.assertEqual(res.status_int, 200) + res_body = json.loads(res.body) + self.assertTrue('adminPass' in res_body) + self.assertEqual(FLAGS.password_length, len(res_body['adminPass'])) + + def test_rescue_with_preset_password(self): + self.flags(allow_admin_api=True) + + self.called = False + + def rescue_mock(*args, **kwargs): + self.called = True + + self.stubs.Set(nova.compute.api.API, 'rescue', rescue_mock) + req = webob.Request.blank('/v1.0/servers/1/rescue') + req.method = 'POST' + body = {"rescue": {"adminPass": "AABBCC112233"}} + req.body = json.dumps(body) + req.content_type = 'application/json' + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(self.called, True) + self.assertEqual(res.status_int, 200) + res_body = json.loads(res.body) + self.assertTrue('adminPass' in res_body) + self.assertEqual('AABBCC112233', res_body['adminPass']) def test_rescue_raises_handled(self): self.flags(allow_admin_api=True) @@ -3285,7 +3401,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}, @@ -3293,17 +3409,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 = { @@ -3319,22 +3447,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): @@ -3572,7 +3700,8 @@ class TestServerInstanceCreation(test.TestCase): self.assertEquals(response.status_int, 202) response = json.loads(response.body) self.assertTrue('adminPass' in response['server']) - self.assertEqual(16, len(response['server']['adminPass'])) + self.assertEqual(FLAGS.password_length, + len(response['server']['adminPass'])) def test_create_instance_admin_pass_xml(self): request, response, dummy = \ @@ -3581,7 +3710,8 @@ class TestServerInstanceCreation(test.TestCase): dom = minidom.parseString(response.body) server = dom.childNodes[0] self.assertEquals(server.nodeName, 'server') - self.assertEqual(16, len(server.getAttribute('adminPass'))) + self.assertEqual(FLAGS.password_length, + len(server.getAttribute('adminPass'))) class TestGetKernelRamdiskFromImage(test.TestCase): @@ -3715,7 +3845,6 @@ class ServersViewBuilderV11Test(test.TestCase): "id": 1, "uuid": self.instance['uuid'], "name": "test_server", - "key_name": '', "links": [ { "rel": "self", @@ -3726,7 +3855,6 @@ class ServersViewBuilderV11Test(test.TestCase): "href": "http://localhost/servers/1", }, ], - "config_drive": None, } } @@ -3739,8 +3867,6 @@ class ServersViewBuilderV11Test(test.TestCase): "id": 1, "uuid": self.instance['uuid'], "name": "test_server", - "key_name": '', - "config_drive": None, "links": [ { "rel": "self", @@ -4063,6 +4189,85 @@ 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() 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..febac5e09 --- /dev/null +++ b/nova/tests/fake_network.py @@ -0,0 +1,253 @@ +# 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 exception +from nova import flags +from nova import utils +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] + + +class FakeNetworkManager(network_manager.NetworkManager): + """This NetworkManager doesn't call the base class so we can bypass all + inherited service cruft and just perform unit tests. + """ + + class FakeDB: + def fixed_ip_get_by_instance(self, context, instance_id): + return [dict(address='10.0.0.0'), dict(address='10.0.0.1'), + dict(address='10.0.0.2')] + + def network_get_by_cidr(self, context, cidr): + raise exception.NetworkNotFoundForCidr() + + def network_create_safe(self, context, net): + fakenet = dict(net) + fakenet['id'] = 999 + return fakenet + + def network_get_all(self, context): + raise exception.NoNetworksFound() + + def virtual_interface_get_all(self, context): + floats = [{'address': '172.16.1.1'}, + {'address': '172.16.1.2'}, + {'address': '173.16.1.2'}] + + vifs = [{'instance_id': 0, + 'fixed_ipv6': '2001:db8::dcad:beff:feef:1', + 'fixed_ips': [{'address': '172.16.0.1', + 'floating_ips': [floats[0]]}]}, + {'instance_id': 20, + 'fixed_ipv6': '2001:db8::dcad:beff:feef:2', + 'fixed_ips': [{'address': '172.16.0.2', + 'floating_ips': [floats[1]]}]}, + {'instance_id': 30, + 'fixed_ipv6': '2002:db8::dcad:beff:feef:2', + 'fixed_ips': [{'address': '173.16.0.2', + 'floating_ips': [floats[2]]}]}] + return vifs + + def instance_get_id_to_uuid_mapping(self, context, ids): + # NOTE(jkoelker): This is just here until we can rely on UUIDs + mapping = {} + for id in ids: + mapping[id] = str(utils.gen_uuid()) + return mapping + + def __init__(self): + self.db = self.FakeDB() + self.deallocate_called = None + + def deallocate_fixed_ip(self, context, address): + self.deallocate_called = address + + def _create_fixed_ips(self, context, network_id): + pass + + +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..b53e4cec6 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -64,16 +64,16 @@ 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) # set up services - self.start_service('compute') - self.start_service('volume') - self.start_service('network') - self.start_service('scheduler') + self.compute = self.start_service('compute') + self.volume = self.start_service('volume') + self.network = self.start_service('network') + self.scheduler = self.start_service('scheduler') self._start_api_service() diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 2cf604d06..e9c79aa13 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -28,17 +28,25 @@ LOG = logging.getLogger('nova.tests.integrated') class ServersTest(integrated_helpers._IntegratedTestBase): - def _wait_for_creation(self, server): - retries = 0 - while server['status'] == 'BUILD': - time.sleep(1) + def _wait_for_state_change(self, server, status): + for i in xrange(0, 50): server = self.api.get_server(server['id']) print server - retries = retries + 1 - if retries > 5: + if server['status'] != status: break + time.sleep(.1) + return server + def _restart_compute_service(self, periodic_interval=None): + """restart compute service. NOTE: fake driver forgets all instances.""" + self.compute.kill() + if periodic_interval: + self.compute = self.start_service( + 'compute', periodic_interval=periodic_interval) + else: + self.compute = self.start_service('compute') + def test_get_servers(self): """Simple check that listing servers works.""" servers = self.api.get_servers() @@ -102,7 +110,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): server_ids = [server['id'] for server in servers] self.assertTrue(created_server_id in server_ids) - found_server = self._wait_for_creation(found_server) + found_server = self._wait_for_state_change(found_server, 'BUILD') # It should be available... # TODO(justinsb): Mock doesn't yet do this... @@ -114,12 +122,117 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self._delete_server(created_server_id) - def _delete_server(self, server_id): + def test_deferred_delete(self): + """Creates, deletes and waits for server to be reclaimed.""" + self.flags(stub_network=True, reclaim_instance_interval=1) + + # enforce periodic tasks run in short time to avoid wait for 60s. + self._restart_compute_service(periodic_interval=0.3) + + # Create server + server = self._build_minimal_create_server_request() + + created_server = self.api.post_server({'server': server}) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # Wait for it to finish being created + found_server = self._wait_for_state_change(created_server, 'BUILD') + + # It should be available... + self.assertEqual('ACTIVE', found_server['status']) + + # Cannot restore unless instance is deleted + self.api.post_server_action(created_server_id, {'restore': {}}) + + # Check it's still active + found_server = self.api.get_server(created_server_id) + self.assertEqual('ACTIVE', found_server['status']) + + # Cannot forceDelete unless instance is deleted + self.api.post_server_action(created_server_id, {'forceDelete': {}}) + + # Check it's still active + found_server = self.api.get_server(created_server_id) + self.assertEqual('ACTIVE', found_server['status']) + # Delete the server - self.api.delete_server(server_id) + self.api.delete_server(created_server_id) + + # Wait for queued deletion + found_server = self._wait_for_state_change(found_server, 'ACTIVE') + self.assertEqual('DELETED', found_server['status']) + # Wait for real deletion + self._wait_for_deletion(created_server_id) + + def test_deferred_delete_restore(self): + """Creates, deletes and restores a server.""" + self.flags(stub_network=True, reclaim_instance_interval=1) + + # Create server + server = self._build_minimal_create_server_request() + + created_server = self.api.post_server({'server': server}) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # Wait for it to finish being created + found_server = self._wait_for_state_change(created_server, 'BUILD') + + # It should be available... + self.assertEqual('ACTIVE', found_server['status']) + + # Delete the server + self.api.delete_server(created_server_id) + + # Wait for queued deletion + found_server = self._wait_for_state_change(found_server, 'ACTIVE') + self.assertEqual('DELETED', found_server['status']) + + # Restore server + self.api.post_server_action(created_server_id, {'restore': {}}) + + # Wait for server to become active again + found_server = self._wait_for_state_change(found_server, 'DELETED') + self.assertEqual('ACTIVE', found_server['status']) + + def test_deferred_delete_force(self): + """Creates, deletes and force deletes a server.""" + self.flags(stub_network=True, reclaim_instance_interval=1) + + # Create server + server = self._build_minimal_create_server_request() + + created_server = self.api.post_server({'server': server}) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # Wait for it to finish being created + found_server = self._wait_for_state_change(created_server, 'BUILD') + + # It should be available... + self.assertEqual('ACTIVE', found_server['status']) + + # Delete the server + self.api.delete_server(created_server_id) + + # Wait for queued deletion + found_server = self._wait_for_state_change(found_server, 'ACTIVE') + self.assertEqual('DELETED', found_server['status']) + + # Force delete server + self.api.post_server_action(created_server_id, {'forceDelete': {}}) + + # Wait for real deletion + self._wait_for_deletion(created_server_id) + + def _wait_for_deletion(self, server_id): # Wait (briefly) for deletion - for _retries in range(5): + for _retries in range(50): try: found_server = self.api.get_server(server_id) except client.OpenStackApiNotFoundException: @@ -132,11 +245,16 @@ class ServersTest(integrated_helpers._IntegratedTestBase): # TODO(justinsb): Mock doesn't yet do accurate state changes #if found_server['status'] != 'deleting': # break - time.sleep(1) + time.sleep(.1) # Should be gone self.assertFalse(found_server) + def _delete_server(self, server_id): + # Delete the server + self.api.delete_server(server_id) + self._wait_for_deletion(server_id) + def test_create_server_with_metadata(self): """Creates a server with metadata.""" @@ -194,7 +312,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self.assertTrue(created_server['id']) created_server_id = created_server['id'] - created_server = self._wait_for_creation(created_server) + created_server = self._wait_for_state_change(created_server, 'BUILD') # rebuild the server with metadata post = {} @@ -228,7 +346,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self.assertTrue(created_server['id']) created_server_id = created_server['id'] - created_server = self._wait_for_creation(created_server) + created_server = self._wait_for_state_change(created_server, 'BUILD') # rebuild the server with metadata post = {} @@ -274,7 +392,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self.assertTrue(created_server['id']) created_server_id = created_server['id'] - created_server = self._wait_for_creation(created_server) + created_server = self._wait_for_state_change(created_server, 'BUILD') # rebuild the server with metadata post = {} 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/test_api.py b/nova/tests/test_api.py index 526d1c490..e9f1145dd 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -515,7 +515,7 @@ class ApiEc2TestCase(test.TestCase): # be good enough for that. for group in rv: if group.name == security_group_name: - self.assertEquals(len(group.rules), 1) + self.assertEquals(len(group.rules), 3) self.assertEquals(len(group.rules[0].grants), 1) self.assertEquals(str(group.rules[0].grants[0]), '%s-%s' % (other_security_group_name, 'fake')) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 65fdffbd6..948c7ad40 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -21,22 +21,24 @@ Tests For Compute """ from nova import compute -from nova.compute import instance_types -from nova.compute import manager as compute_manager -from nova.compute import power_state -from nova.compute import vm_states from nova import context from nova import db -from nova.db.sqlalchemy import models -from nova.db.sqlalchemy import api as sqlalchemy_api from nova import exception from nova import flags -import nova.image.fake from nova import log as logging from nova import rpc from nova import test from nova import utils + +from nova.compute import instance_types +from nova.compute import manager as compute_manager +from nova.compute import power_state +from nova.compute import vm_states +from nova.db.sqlalchemy import models +from nova.image import fake as fake_image from nova.notifier import test_notifier +from nova.tests import fake_network + LOG = logging.getLogger('nova.tests.compute') FLAGS = flags.FLAGS @@ -74,7 +76,7 @@ class ComputeTestCase(test.TestCase): def fake_show(meh, context, id): return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}} - self.stubs.Set(nova.image.fake._FakeImageService, 'show', fake_show) + self.stubs.Set(fake_image._FakeImageService, 'show', fake_show) def _create_instance(self, params=None): """Create a test instance""" @@ -174,6 +176,20 @@ class ComputeTestCase(test.TestCase): self.assertEqual(pre_build_len, len(db.instance_get_all(context.get_admin_context()))) + def test_create_instance_with_img_ref_associates_config_drive(self): + """Make sure create associates a config drive.""" + + instance_id = self._create_instance(params={'config_drive': '1234', }) + + try: + self.compute.run_instance(self.context, instance_id) + instances = db.instance_get_all(context.get_admin_context()) + instance = instances[0] + + self.assertTrue(instance.config_drive) + finally: + db.instance_destroy(self.context, instance_id) + def test_create_instance_associates_config_drive(self): """Make sure create associates a config drive.""" @@ -300,11 +316,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) + 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, reboot_type) self.compute.terminate_instance(self.context, instance_id) def test_set_admin_password(self): @@ -638,7 +663,6 @@ class ComputeTestCase(test.TestCase): dbmock = self.mox.CreateMock(db) dbmock.instance_get(c, i_id).AndReturn(instance_ref) - dbmock.instance_get_fixed_addresses(c, i_id).AndReturn(None) self.compute.db = dbmock self.mox.ReplayAll() @@ -648,6 +672,9 @@ class ComputeTestCase(test.TestCase): def test_pre_live_migration_instance_has_volume(self): """Confirm setup_compute_volume is called when volume is mounted.""" + def fake_nw_info(*args, **kwargs): + return [(0, {'ips':['dummy']})] + i_ref = self._get_dummy_instance() c = context.get_admin_context() @@ -657,13 +684,13 @@ class ComputeTestCase(test.TestCase): drivermock = self.mox.CreateMock(self.compute_driver) dbmock.instance_get(c, i_ref['id']).AndReturn(i_ref) - dbmock.instance_get_fixed_addresses(c, i_ref['id']).AndReturn('dummy') for i in range(len(i_ref['volumes'])): vid = i_ref['volumes'][i]['id'] volmock.setup_compute_volume(c, vid).InAnyOrder('g1') - drivermock.plug_vifs(i_ref, []) - drivermock.ensure_filtering_rules_for_instance(i_ref, []) + drivermock.plug_vifs(i_ref, fake_nw_info()) + drivermock.ensure_filtering_rules_for_instance(i_ref, fake_nw_info()) + self.stubs.Set(self.compute, '_get_instance_nw_info', fake_nw_info) self.compute.db = dbmock self.compute.volume_manager = volmock self.compute.driver = drivermock @@ -674,6 +701,9 @@ class ComputeTestCase(test.TestCase): def test_pre_live_migration_instance_has_no_volume(self): """Confirm log meg when instance doesn't mount any volumes.""" + def fake_nw_info(*args, **kwargs): + return [(0, {'ips':['dummy']})] + i_ref = self._get_dummy_instance() i_ref['volumes'] = [] c = context.get_admin_context() @@ -683,12 +713,12 @@ class ComputeTestCase(test.TestCase): drivermock = self.mox.CreateMock(self.compute_driver) dbmock.instance_get(c, i_ref['id']).AndReturn(i_ref) - dbmock.instance_get_fixed_addresses(c, i_ref['id']).AndReturn('dummy') self.mox.StubOutWithMock(compute_manager.LOG, 'info') compute_manager.LOG.info(_("%s has no volume."), i_ref['hostname']) - drivermock.plug_vifs(i_ref, []) - drivermock.ensure_filtering_rules_for_instance(i_ref, []) + drivermock.plug_vifs(i_ref, fake_nw_info()) + drivermock.ensure_filtering_rules_for_instance(i_ref, fake_nw_info()) + self.stubs.Set(self.compute, '_get_instance_nw_info', fake_nw_info) self.compute.db = dbmock self.compute.driver = drivermock @@ -702,6 +732,8 @@ class ComputeTestCase(test.TestCase): It retries and raise exception when timeout exceeded. """ + def fake_nw_info(*args, **kwargs): + return [(0, {'ips':['dummy']})] i_ref = self._get_dummy_instance() c = context.get_admin_context() @@ -713,13 +745,13 @@ class ComputeTestCase(test.TestCase): drivermock = self.mox.CreateMock(self.compute_driver) dbmock.instance_get(c, i_ref['id']).AndReturn(i_ref) - dbmock.instance_get_fixed_addresses(c, i_ref['id']).AndReturn('dummy') for i in range(len(i_ref['volumes'])): volmock.setup_compute_volume(c, i_ref['volumes'][i]['id']) for i in range(FLAGS.live_migration_retry_count): - drivermock.plug_vifs(i_ref, []).\ + drivermock.plug_vifs(i_ref, fake_nw_info()).\ AndRaise(exception.ProcessExecutionError()) + self.stubs.Set(self.compute, '_get_instance_nw_info', fake_nw_info) self.compute.db = dbmock self.compute.network_manager = netmock self.compute.volume_manager = volmock @@ -993,190 +1025,19 @@ class ComputeTestCase(test.TestCase): db.instance_destroy(c, instance_id2) db.instance_destroy(c, instance_id3) - def test_get_by_fixed_ip(self): - """Test getting 1 instance by Fixed IP""" - c = context.get_admin_context() - instance_id1 = self._create_instance() - instance_id2 = self._create_instance({'id': 20}) - instance_id3 = self._create_instance({'id': 30}) - - vif_ref1 = db.virtual_interface_create(c, - {'address': '12:34:56:78:90:12', - 'instance_id': instance_id1, - 'network_id': 1}) - vif_ref2 = db.virtual_interface_create(c, - {'address': '90:12:34:56:78:90', - 'instance_id': instance_id2, - 'network_id': 1}) - - db.fixed_ip_create(c, - {'address': '1.1.1.1', - 'instance_id': instance_id1, - 'virtual_interface_id': vif_ref1['id']}) - db.fixed_ip_create(c, - {'address': '1.1.2.1', - 'instance_id': instance_id2, - 'virtual_interface_id': vif_ref2['id']}) - - # regex not allowed - instances = self.compute_api.get_all(c, - search_opts={'fixed_ip': '.*'}) - self.assertEqual(len(instances), 0) - - instances = self.compute_api.get_all(c, - search_opts={'fixed_ip': '1.1.3.1'}) - self.assertEqual(len(instances), 0) - - instances = self.compute_api.get_all(c, - search_opts={'fixed_ip': '1.1.1.1'}) - self.assertEqual(len(instances), 1) - self.assertEqual(instances[0].id, instance_id1) - - instances = self.compute_api.get_all(c, - search_opts={'fixed_ip': '1.1.2.1'}) - self.assertEqual(len(instances), 1) - self.assertEqual(instances[0].id, instance_id2) - - db.virtual_interface_delete(c, vif_ref1['id']) - db.virtual_interface_delete(c, vif_ref2['id']) - db.instance_destroy(c, instance_id1) - db.instance_destroy(c, instance_id2) - - def test_get_all_by_ip_regexp(self): - """Test searching by Floating and Fixed IP""" - c = context.get_admin_context() - instance_id1 = self._create_instance({'display_name': 'woot'}) - instance_id2 = self._create_instance({ - 'display_name': 'woo', - 'id': 20}) - instance_id3 = self._create_instance({ - 'display_name': 'not-woot', - 'id': 30}) - - vif_ref1 = db.virtual_interface_create(c, - {'address': '12:34:56:78:90:12', - 'instance_id': instance_id1, - 'network_id': 1}) - vif_ref2 = db.virtual_interface_create(c, - {'address': '90:12:34:56:78:90', - 'instance_id': instance_id2, - 'network_id': 1}) - vif_ref3 = db.virtual_interface_create(c, - {'address': '34:56:78:90:12:34', - 'instance_id': instance_id3, - 'network_id': 1}) - - db.fixed_ip_create(c, - {'address': '1.1.1.1', - 'instance_id': instance_id1, - 'virtual_interface_id': vif_ref1['id']}) - db.fixed_ip_create(c, - {'address': '1.1.2.1', - 'instance_id': instance_id2, - 'virtual_interface_id': vif_ref2['id']}) - fix_addr = db.fixed_ip_create(c, - {'address': '1.1.3.1', - 'instance_id': instance_id3, - 'virtual_interface_id': vif_ref3['id']}) - fix_ref = db.fixed_ip_get_by_address(c, fix_addr) - flo_ref = db.floating_ip_create(c, - {'address': '10.0.0.2', - 'fixed_ip_id': fix_ref['id']}) - - # ends up matching 2nd octet here.. so all 3 match - instances = self.compute_api.get_all(c, - search_opts={'ip': '.*\.1'}) - self.assertEqual(len(instances), 3) - - instances = self.compute_api.get_all(c, - search_opts={'ip': '1.*'}) - self.assertEqual(len(instances), 3) - - instances = self.compute_api.get_all(c, - search_opts={'ip': '.*\.1.\d+$'}) - self.assertEqual(len(instances), 1) - instance_ids = [instance.id for instance in instances] - self.assertTrue(instance_id1 in instance_ids) - - instances = self.compute_api.get_all(c, - search_opts={'ip': '.*\.2.+'}) - self.assertEqual(len(instances), 1) - self.assertEqual(instances[0].id, instance_id2) - - instances = self.compute_api.get_all(c, - search_opts={'ip': '10.*'}) - self.assertEqual(len(instances), 1) - self.assertEqual(instances[0].id, instance_id3) - - db.virtual_interface_delete(c, vif_ref1['id']) - db.virtual_interface_delete(c, vif_ref2['id']) - db.virtual_interface_delete(c, vif_ref3['id']) - db.floating_ip_destroy(c, '10.0.0.2') - db.instance_destroy(c, instance_id1) - db.instance_destroy(c, instance_id2) - db.instance_destroy(c, instance_id3) - - def test_get_all_by_ipv6_regexp(self): - """Test searching by IPv6 address""" - - c = context.get_admin_context() - instance_id1 = self._create_instance({'display_name': 'woot'}) - instance_id2 = self._create_instance({ - 'display_name': 'woo', - 'id': 20}) - instance_id3 = self._create_instance({ - 'display_name': 'not-woot', - 'id': 30}) - - vif_ref1 = db.virtual_interface_create(c, - {'address': '12:34:56:78:90:12', - 'instance_id': instance_id1, - 'network_id': 1}) - vif_ref2 = db.virtual_interface_create(c, - {'address': '90:12:34:56:78:90', - 'instance_id': instance_id2, - 'network_id': 1}) - vif_ref3 = db.virtual_interface_create(c, - {'address': '34:56:78:90:12:34', - 'instance_id': instance_id3, - 'network_id': 1}) - - # This will create IPv6 addresses of: - # 1: fd00::1034:56ff:fe78:9012 - # 20: fd00::9212:34ff:fe56:7890 - # 30: fd00::3656:78ff:fe90:1234 - - instances = self.compute_api.get_all(c, - search_opts={'ip6': '.*1034.*'}) - self.assertEqual(len(instances), 1) - self.assertEqual(instances[0].id, instance_id1) - - instances = self.compute_api.get_all(c, - search_opts={'ip6': '^fd00.*'}) - self.assertEqual(len(instances), 3) - instance_ids = [instance.id for instance in instances] - self.assertTrue(instance_id1 in instance_ids) - self.assertTrue(instance_id2 in instance_ids) - self.assertTrue(instance_id3 in instance_ids) - - instances = self.compute_api.get_all(c, - search_opts={'ip6': '^.*12.*34.*'}) - self.assertEqual(len(instances), 2) - instance_ids = [instance.id for instance in instances] - self.assertTrue(instance_id2 in instance_ids) - self.assertTrue(instance_id3 in instance_ids) - - db.virtual_interface_delete(c, vif_ref1['id']) - db.virtual_interface_delete(c, vif_ref2['id']) - db.virtual_interface_delete(c, vif_ref3['id']) - db.instance_destroy(c, instance_id1) - db.instance_destroy(c, instance_id2) - db.instance_destroy(c, instance_id3) - def test_get_all_by_multiple_options_at_once(self): """Test searching by multiple options at once""" c = context.get_admin_context() - instance_id1 = self._create_instance({'display_name': 'woot'}) + network_manager = fake_network.FakeNetworkManager() + self.stubs.Set(self.compute_api.network_api, + 'get_instance_uuids_by_ip_filter', + network_manager.get_instance_uuids_by_ip_filter) + self.stubs.Set(network_manager.db, + 'instance_get_id_to_uuid_mapping', + db.instance_get_id_to_uuid_mapping) + + instance_id1 = self._create_instance({'display_name': 'woot', + 'id': 0}) instance_id2 = self._create_instance({ 'display_name': 'woo', 'id': 20}) @@ -1184,36 +1045,6 @@ class ComputeTestCase(test.TestCase): 'display_name': 'not-woot', 'id': 30}) - vif_ref1 = db.virtual_interface_create(c, - {'address': '12:34:56:78:90:12', - 'instance_id': instance_id1, - 'network_id': 1}) - vif_ref2 = db.virtual_interface_create(c, - {'address': '90:12:34:56:78:90', - 'instance_id': instance_id2, - 'network_id': 1}) - vif_ref3 = db.virtual_interface_create(c, - {'address': '34:56:78:90:12:34', - 'instance_id': instance_id3, - 'network_id': 1}) - - db.fixed_ip_create(c, - {'address': '1.1.1.1', - 'instance_id': instance_id1, - 'virtual_interface_id': vif_ref1['id']}) - db.fixed_ip_create(c, - {'address': '1.1.2.1', - 'instance_id': instance_id2, - 'virtual_interface_id': vif_ref2['id']}) - fix_addr = db.fixed_ip_create(c, - {'address': '1.1.3.1', - 'instance_id': instance_id3, - 'virtual_interface_id': vif_ref3['id']}) - fix_ref = db.fixed_ip_get_by_address(c, fix_addr) - flo_ref = db.floating_ip_create(c, - {'address': '10.0.0.2', - 'fixed_ip_id': fix_ref['id']}) - # ip ends up matching 2nd octet here.. so all 3 match ip # but 'name' only matches one instances = self.compute_api.get_all(c, @@ -1221,18 +1052,18 @@ class ComputeTestCase(test.TestCase): self.assertEqual(len(instances), 1) self.assertEqual(instances[0].id, instance_id3) - # ip ends up matching any ip with a '2' in it.. so instance - # 2 and 3.. but name should only match #2 + # ip ends up matching any ip with a '1' in the last octet.. + # so instance 1 and 3.. but name should only match #1 # but 'name' only matches one instances = self.compute_api.get_all(c, - search_opts={'ip': '.*2', 'name': '^woo.*'}) + search_opts={'ip': '.*\.1$', 'name': '^woo.*'}) self.assertEqual(len(instances), 1) - self.assertEqual(instances[0].id, instance_id2) + self.assertEqual(instances[0].id, instance_id1) # same as above but no match on name (name matches instance_id1 # but the ip query doesn't instances = self.compute_api.get_all(c, - search_opts={'ip': '.*2.*', 'name': '^woot.*'}) + search_opts={'ip': '.*\.2$', 'name': '^woot.*'}) self.assertEqual(len(instances), 0) # ip matches all 3... ipv6 matches #2+#3...name matches #3 @@ -1243,10 +1074,6 @@ class ComputeTestCase(test.TestCase): self.assertEqual(len(instances), 1) self.assertEqual(instances[0].id, instance_id3) - db.virtual_interface_delete(c, vif_ref1['id']) - db.virtual_interface_delete(c, vif_ref2['id']) - db.virtual_interface_delete(c, vif_ref3['id']) - db.floating_ip_destroy(c, '10.0.0.2') db.instance_destroy(c, instance_id1) db.instance_destroy(c, instance_id2) db.instance_destroy(c, instance_id3) @@ -1554,12 +1381,16 @@ class ComputeTestCase(test.TestCase): db.block_device_mapping_destroy(self.context, bdm['id']) self.compute.terminate_instance(self.context, instance_id) - def test_ephemeral_size(self): + def test_volume_size(self): local_size = 2 - inst_type = {'local_gb': local_size} - self.assertEqual(self.compute_api._ephemeral_size(inst_type, + swap_size = 3 + inst_type = {'local_gb': local_size, 'swap': swap_size} + self.assertEqual(self.compute_api._volume_size(inst_type, 'ephemeral0'), local_size) - self.assertEqual(self.compute_api._ephemeral_size(inst_type, - 'ephemeral1'), + self.assertEqual(self.compute_api._volume_size(inst_type, + 'ephemeral1'), 0) + self.assertEqual(self.compute_api._volume_size(inst_type, + 'swap'), + swap_size) diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index 60d7abd8c..81194e3f9 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -18,6 +18,8 @@ """Unit tests for the DB API""" +import datetime + from nova import test from nova import context from nova import db @@ -92,6 +94,32 @@ class DbApiTestCase(test.TestCase): db.instance_destroy(self.context, inst1.id) result = db.instance_get_all_by_filters(self.context.elevated(), {}) self.assertEqual(2, len(result)) - self.assertEqual(result[0].id, inst2.id) - self.assertEqual(result[1].id, inst1.id) - self.assertTrue(result[1].deleted) + self.assertIn(inst1.id, [result[0].id, result[1].id]) + self.assertIn(inst2.id, [result[0].id, result[1].id]) + if inst1.id == result[0].id: + self.assertTrue(result[0].deleted) + else: + self.assertTrue(result[1].deleted) + + def test_migration_get_all_unconfirmed(self): + ctxt = context.get_admin_context() + + # Ensure no migrations are returned. + results = db.migration_get_all_unconfirmed(ctxt, 10) + self.assertEqual(0, len(results)) + + # Ensure one migration older than 10 seconds is returned. + updated_at = datetime.datetime(2000, 01, 01, 12, 00, 00) + values = {"status": "FINISHED", "updated_at": updated_at} + migration = db.migration_create(ctxt, values) + results = db.migration_get_all_unconfirmed(ctxt, 10) + self.assertEqual(1, len(results)) + db.migration_update(ctxt, migration.id, {"status": "CONFIRMED"}) + + # Ensure the new migration is not returned. + updated_at = datetime.datetime.utcnow() + values = {"status": "FINISHED", "updated_at": updated_at} + migration = db.migration_create(ctxt, values) + results = db.migration_get_all_unconfirmed(ctxt, 10) + self.assertEqual(0, len(results)) + db.migration_update(ctxt, migration.id, {"status": "CONFIRMED"}) 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 8c6775b29..39aa4ad41 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -35,61 +35,56 @@ 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 FakeVirDomainSnapshot(object): + + def __init__(self, dom=None): + self.dom = dom + + def delete(self, flags): + pass + + +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 FakeVirDomainSnapshot(self) + + def createWithFlags(self, launch_flags): + pass + + def XMLDesc(self, *args): + return self._fake_dom_xml class CacheConcurrencyTestCase(test.TestCase): @@ -163,7 +158,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 +188,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 +225,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) @@ -321,7 +269,7 @@ class LibvirtConnTestCase(test.TestCase): instance_data = dict(self.test_instance) self._check_xml_and_container(instance_data) - def test_snapshot(self): + def test_snapshot_in_raw_format(self): if not self.lazy_load_library_exists(): return @@ -354,6 +302,44 @@ class LibvirtConnTestCase(test.TestCase): snapshot = image_service.show(context, recv_meta['id']) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') + self.assertEquals(snapshot['disk_format'], 'raw') + self.assertEquals(snapshot['name'], snapshot_name) + + def test_snapshot_in_qcow2_format(self): + if not self.lazy_load_library_exists(): + return + + self.flags(image_service='nova.image.fake.FakeImageService') + self.flags(snapshot_image_format='qcow2') + + # Start test + image_service = utils.import_object(FLAGS.image_service) + + # Assuming that base image already exists in image_service + instance_ref = db.instance_create(self.context, self.test_instance) + properties = {'instance_id': instance_ref['id'], + 'user_id': str(self.context.user_id)} + snapshot_name = 'test-snap' + sent_meta = {'name': snapshot_name, 'is_public': False, + 'status': 'creating', 'properties': properties} + # Create new image. It will be updated in snapshot method + # To work with it from snapshot, the single image_service is needed + recv_meta = image_service.create(context, sent_meta) + + self.mox.StubOutWithMock(connection.LibvirtConnection, '_conn') + connection.LibvirtConnection._conn.lookupByName = self.fake_lookup + self.mox.StubOutWithMock(connection.utils, 'execute') + connection.utils.execute = self.fake_execute + + self.mox.ReplayAll() + + conn = connection.LibvirtConnection(False) + conn.snapshot(self.context, instance_ref, recv_meta['id']) + + snapshot = image_service.show(context, recv_meta['id']) + self.assertEquals(snapshot['properties']['image_state'], 'available') + self.assertEquals(snapshot['status'], 'active') + self.assertEquals(snapshot['disk_format'], 'qcow2') self.assertEquals(snapshot['name'], snapshot_name) def test_snapshot_no_image_architecture(self): @@ -407,7 +393,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 +403,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 +418,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 +442,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 +487,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 +516,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 +631,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 +728,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 +780,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 +815,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 +825,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 +866,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 +950,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 +1013,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, @@ -1061,10 +1083,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) @@ -1078,7 +1107,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") @@ -1101,10 +1131,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 -p tcp ' '-m multiport --dports 80:81 -s 192.168.10.0/24') @@ -1114,24 +1145,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, @@ -1142,9 +1176,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() @@ -1170,8 +1204,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) @@ -1186,13 +1219,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 @@ -1256,7 +1288,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) @@ -1372,11 +1403,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', @@ -1392,17 +1421,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") @@ -1425,8 +1459,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 index 99577b88e..940af7b5f 100755 --- a/nova/tests/test_linux_net.py +++ b/nova/tests/test_linux_net.py @@ -345,3 +345,72 @@ class LinuxNetworkTestCase(test.TestCase): expected = ("10.0.0.1,fake_instance00.novalocal,192.168.0.100")
actual = self.driver._host_dhcp(fixed_ips[0])
self.assertEquals(actual, expected)
+
+ def _test_initialize_gateway(self, existing, expected):
+ self.flags(fake_network=False)
+ executes = []
+
+ def fake_execute(*args, **kwargs):
+ executes.append(args)
+ if args[0] == 'ip' and args[1] == 'addr' and args[2] == 'show':
+ return existing, ""
+ self.stubs.Set(utils, 'execute', fake_execute)
+ network = {'dhcp_server': '192.168.1.1',
+ 'cidr': '192.168.1.0/24',
+ 'broadcast': '192.168.1.255',
+ 'cidr_v6': '2001:db8::/64'}
+ self.driver.initialize_gateway_device('eth0', network)
+ self.assertEqual(executes, expected)
+
+ def test_initialize_gateway_moves_wrong_ip(self):
+ existing = ("2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> "
+ " mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 1000\n"
+ " link/ether de:ad:be:ef:be:ef brd ff:ff:ff:ff:ff:ff\n"
+ " inet 192.168.0.1/24 brd 192.168.0.255 scope global eth0\n"
+ " inet6 dead::beef:dead:beef:dead/64 scope link\n"
+ " valid_lft forever preferred_lft forever\n")
+ expected = [
+ ('ip', 'addr', 'show', 'dev', 'eth0', 'scope', 'global'),
+ ('ip', 'addr', 'del', '192.168.0.1/24',
+ 'brd', '192.168.0.255', 'scope', 'global', 'dev', 'eth0'),
+ ('ip', 'addr', 'add', '192.168.1.1/24',
+ 'brd', '192.168.1.255', 'dev', 'eth0'),
+ ('ip', 'addr', 'add', '192.168.0.1/24',
+ 'brd', '192.168.0.255', 'scope', 'global', 'dev', 'eth0'),
+ ('ip', '-f', 'inet6', 'addr', 'change',
+ '2001:db8::/64', 'dev', 'eth0'),
+ ('ip', 'link', 'set', 'dev', 'eth0', 'promisc', 'on'),
+ ]
+ self._test_initialize_gateway(existing, expected)
+
+ def test_initialize_gateway_no_move_right_ip(self):
+ existing = ("2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> "
+ " mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 1000\n"
+ " link/ether de:ad:be:ef:be:ef brd ff:ff:ff:ff:ff:ff\n"
+ " inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0\n"
+ " inet 192.168.0.1/24 brd 192.168.0.255 scope global eth0\n"
+ " inet6 dead::beef:dead:beef:dead/64 scope link\n"
+ " valid_lft forever preferred_lft forever\n")
+ expected = [
+ ('ip', 'addr', 'show', 'dev', 'eth0', 'scope', 'global'),
+ ('ip', '-f', 'inet6', 'addr', 'change',
+ '2001:db8::/64', 'dev', 'eth0'),
+ ('ip', 'link', 'set', 'dev', 'eth0', 'promisc', 'on'),
+ ]
+ self._test_initialize_gateway(existing, expected)
+
+ def test_initialize_gateway_add_if_blank(self):
+ existing = ("2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> "
+ " mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 1000\n"
+ " link/ether de:ad:be:ef:be:ef brd ff:ff:ff:ff:ff:ff\n"
+ " inet6 dead::beef:dead:beef:dead/64 scope link\n"
+ " valid_lft forever preferred_lft forever\n")
+ expected = [
+ ('ip', 'addr', 'show', 'dev', 'eth0', 'scope', 'global'),
+ ('ip', 'addr', 'add', '192.168.1.1/24',
+ 'brd', '192.168.1.255', 'dev', 'eth0'),
+ ('ip', '-f', 'inet6', 'addr', 'change',
+ '2001:db8::/64', 'dev', 'eth0'),
+ ('ip', 'link', 'set', 'dev', 'eth0', 'promisc', 'on'),
+ ]
+ self._test_initialize_gateway(existing, expected)
diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index b06e5c136..2f82132fa 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -19,22 +19,21 @@ """Tests for the testing the metadata code.""" import base64 -import httplib import webob from nova import exception from nova import test -from nova import wsgi from nova.api.ec2 import metadatarequesthandler from nova.db.sqlalchemy import api +from nova.tests import fake_network USER_DATA_STRING = ("This is an encoded string") ENCODE_USER_DATA_STRING = base64.b64encode(USER_DATA_STRING) -def return_non_existing_server_by_address(context, address): +def return_non_existing_server_by_address(context, address, *args, **kwarg): raise exception.NotFound() @@ -69,6 +68,10 @@ class MetadataTestCase(test.TestCase): self.stubs.Set(api, 'instance_get_all_by_filters', instance_get_list) self.stubs.Set(api, 'instance_get_floating_address', floating_get) self.app = metadatarequesthandler.MetadataRequestHandler() + network_manager = fake_network.FakeNetworkManager() + self.stubs.Set(self.app.cc.network_api, + 'get_instance_uuids_by_ip_filter', + network_manager.get_instance_uuids_by_ip_filter) def request(self, relative_url): request = webob.Request.blank(relative_url) diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index e4fa3a417..6344bc4c6 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') @@ -138,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() + fake_get_instance_nw_info = fake_network.fake_get_instance_nw_info - nw_info = self.network.get_instance_nw_info(None, 0, 0, None) + nw_info = fake_get_instance_nw_info(self.stubs, 0, 2) + self.assertFalse(nw_info) - self.assertTrue(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') @@ -297,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()) @@ -494,55 +484,23 @@ class VlanNetworkTestCase(test.TestCase): class CommonNetworkTestCase(test.TestCase): - - class FakeNetworkManager(network_manager.NetworkManager): - """This NetworkManager doesn't call the base class so we can bypass all - inherited service cruft and just perform unit tests. - """ - - class FakeDB: - def fixed_ip_get_by_instance(self, context, instance_id): - return [dict(address='10.0.0.0'), dict(address='10.0.0.1'), - dict(address='10.0.0.2')] - - def network_get_by_cidr(self, context, cidr): - raise exception.NetworkNotFoundForCidr() - - def network_create_safe(self, context, net): - fakenet = dict(net) - fakenet['id'] = 999 - return fakenet - - def network_get_all(self, context): - raise exception.NoNetworksFound() - - def __init__(self): - self.db = self.FakeDB() - self.deallocate_called = None - - def deallocate_fixed_ip(self, context, address): - self.deallocate_called = address - - def _create_fixed_ips(self, context, network_id): - pass - def fake_create_fixed_ips(self, context, network_id): return None def test_remove_fixed_ip_from_instance(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() manager.remove_fixed_ip_from_instance(None, 99, '10.0.0.1') self.assertEquals(manager.deallocate_called, '10.0.0.1') def test_remove_fixed_ip_from_instance_bad_input(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.assertRaises(exception.FixedIpNotFoundForSpecificInstance, manager.remove_fixed_ip_from_instance, None, 99, 'bad input') def test_validate_cidrs(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() nets = manager.create_networks(None, 'fake', '192.168.0.0/24', False, 1, 256, None, None, None, None) @@ -551,7 +509,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertTrue('192.168.0.0/24' in cidrs) def test_validate_cidrs_split_exact_in_half(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() nets = manager.create_networks(None, 'fake', '192.168.0.0/24', False, 2, 128, None, None, None, None) @@ -561,7 +519,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertTrue('192.168.0.128/25' in cidrs) def test_validate_cidrs_split_cidr_in_use_middle_of_range(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() manager.db.network_get_all(ctxt).AndReturn([{'id': 1, @@ -579,7 +537,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertFalse('192.168.2.0/24' in cidrs) def test_validate_cidrs_smaller_subnet_in_use(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() manager.db.network_get_all(ctxt).AndReturn([{'id': 1, @@ -592,7 +550,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertRaises(ValueError, manager.create_networks, *args) def test_validate_cidrs_split_smaller_cidr_in_use(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() manager.db.network_get_all(ctxt).AndReturn([{'id': 1, @@ -609,7 +567,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertFalse('192.168.2.0/24' in cidrs) def test_validate_cidrs_split_smaller_cidr_in_use2(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() manager.db.network_get_all(ctxt).AndReturn([{'id': 1, @@ -625,7 +583,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertFalse('192.168.2.0/27' in cidrs) def test_validate_cidrs_split_all_in_use(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() in_use = [{'id': 1, 'cidr': '192.168.2.9/29'}, @@ -641,14 +599,14 @@ class CommonNetworkTestCase(test.TestCase): self.assertRaises(ValueError, manager.create_networks, *args) def test_validate_cidrs_one_in_use(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() args = (None, 'fake', '192.168.0.0/24', False, 2, 256, None, None, None, None) # ValueError: network_size * num_networks exceeds cidr size self.assertRaises(ValueError, manager.create_networks, *args) def test_validate_cidrs_already_used(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() manager.db.network_get_all(ctxt).AndReturn([{'id': 1, @@ -660,7 +618,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertRaises(ValueError, manager.create_networks, *args) def test_validate_cidrs_too_many(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() args = (None, 'fake', '192.168.0.0/24', False, 200, 256, None, None, None, None) # ValueError: Not enough subnets avail to satisfy requested @@ -668,7 +626,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertRaises(ValueError, manager.create_networks, *args) def test_validate_cidrs_split_partial(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() nets = manager.create_networks(None, 'fake', '192.168.0.0/16', False, 2, 256, None, None, None, None) returned_cidrs = [str(net['cidr']) for net in nets] @@ -676,7 +634,7 @@ class CommonNetworkTestCase(test.TestCase): self.assertTrue('192.168.1.0/24' in returned_cidrs) def test_validate_cidrs_conflict_existing_supernet(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() fakecidr = [{'id': 1, 'cidr': '192.168.0.0/8'}] @@ -690,16 +648,15 @@ class CommonNetworkTestCase(test.TestCase): def test_create_networks(self): cidr = '192.168.0.0/24' - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.stubs.Set(manager, '_create_fixed_ips', self.fake_create_fixed_ips) args = [None, 'foo', cidr, None, 1, 256, 'fd00::/48', None, None, None] - result = manager.create_networks(*args) self.assertTrue(manager.create_networks(*args)) def test_create_networks_cidr_already_used(self): - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() fakecidr = [{'id': 1, 'cidr': '192.168.0.0/24'}] @@ -711,9 +668,124 @@ class CommonNetworkTestCase(test.TestCase): def test_create_networks_many(self): cidr = '192.168.0.0/16' - manager = self.FakeNetworkManager() + manager = fake_network.FakeNetworkManager() self.stubs.Set(manager, '_create_fixed_ips', self.fake_create_fixed_ips) args = [None, 'foo', cidr, None, 10, 256, 'fd00::/48', None, None, None] self.assertTrue(manager.create_networks(*args)) + + def test_get_instance_uuids_by_ip_regex(self): + manager = fake_network.FakeNetworkManager() + _vifs = manager.db.virtual_interface_get_all(None) + + # Greedy get eveything + res = manager.get_instance_uuids_by_ip_filter(None, {'ip': '.*'}) + self.assertEqual(len(res), len(_vifs)) + + # Doesn't exist + res = manager.get_instance_uuids_by_ip_filter(None, {'ip': '10.0.0.1'}) + self.assertFalse(res) + + # Get instance 1 + res = manager.get_instance_uuids_by_ip_filter(None, + {'ip': '172.16.0.2'}) + self.assertTrue(res) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]['instance_id'], _vifs[1]['instance_id']) + + # Get instance 2 + res = manager.get_instance_uuids_by_ip_filter(None, + {'ip': '173.16.0.2'}) + self.assertTrue(res) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]['instance_id'], _vifs[2]['instance_id']) + + # Get instance 0 and 1 + res = manager.get_instance_uuids_by_ip_filter(None, + {'ip': '172.16.0.*'}) + self.assertTrue(res) + self.assertEqual(len(res), 2) + self.assertEqual(res[0]['instance_id'], _vifs[0]['instance_id']) + self.assertEqual(res[1]['instance_id'], _vifs[1]['instance_id']) + + # Get instance 1 and 2 + res = manager.get_instance_uuids_by_ip_filter(None, + {'ip': '17..16.0.2'}) + self.assertTrue(res) + self.assertEqual(len(res), 2) + self.assertEqual(res[0]['instance_id'], _vifs[1]['instance_id']) + self.assertEqual(res[1]['instance_id'], _vifs[2]['instance_id']) + + def test_get_instance_uuids_by_ipv6_regex(self): + manager = fake_network.FakeNetworkManager() + _vifs = manager.db.virtual_interface_get_all(None) + + # Greedy get eveything + res = manager.get_instance_uuids_by_ip_filter(None, {'ip6': '.*'}) + self.assertEqual(len(res), len(_vifs)) + + # Doesn't exist + res = manager.get_instance_uuids_by_ip_filter(None, + {'ip6': '.*1034.*'}) + self.assertFalse(res) + + # Get instance 1 + res = manager.get_instance_uuids_by_ip_filter(None, + {'ip6': '2001:.*:2'}) + self.assertTrue(res) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]['instance_id'], _vifs[1]['instance_id']) + + # Get instance 2 + ip6 = '2002:db8::dcad:beff:feef:2' + res = manager.get_instance_uuids_by_ip_filter(None, {'ip6': ip6}) + self.assertTrue(res) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]['instance_id'], _vifs[2]['instance_id']) + + # Get instance 0 and 1 + res = manager.get_instance_uuids_by_ip_filter(None, {'ip6': '2001:.*'}) + self.assertTrue(res) + self.assertEqual(len(res), 2) + self.assertEqual(res[0]['instance_id'], _vifs[0]['instance_id']) + self.assertEqual(res[1]['instance_id'], _vifs[1]['instance_id']) + + # Get instance 1 and 2 + ip6 = '200.:db8::dcad:beff:feef:2' + res = manager.get_instance_uuids_by_ip_filter(None, {'ip6': ip6}) + self.assertTrue(res) + self.assertEqual(len(res), 2) + self.assertEqual(res[0]['instance_id'], _vifs[1]['instance_id']) + self.assertEqual(res[1]['instance_id'], _vifs[2]['instance_id']) + + def test_get_instance_uuids_by_ip(self): + manager = fake_network.FakeNetworkManager() + _vifs = manager.db.virtual_interface_get_all(None) + + # No regex for you! + res = manager.get_instance_uuids_by_ip_filter(None, + {'fixed_ip': '.*'}) + self.assertFalse(res) + + # Doesn't exist + ip = '10.0.0.1' + res = manager.get_instance_uuids_by_ip_filter(None, + {'fixed_ip': ip}) + self.assertFalse(res) + + # Get instance 1 + ip = '172.16.0.2' + res = manager.get_instance_uuids_by_ip_filter(None, + {'fixed_ip': ip}) + self.assertTrue(res) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]['instance_id'], _vifs[1]['instance_id']) + + # Get instance 2 + ip = '173.16.0.2' + res = manager.get_instance_uuids_by_ip_filter(None, + {'fixed_ip': ip}) + self.assertTrue(res) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]['instance_id'], _vifs[2]['instance_id']) diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py index 1ba794a1a..19a15332d 100644 --- a/nova/tests/test_utils.py +++ b/nova/tests/test_utils.py @@ -20,10 +20,14 @@ import tempfile import nova from nova import exception +from nova import flags from nova import test from nova import utils +FLAGS = flags.FLAGS + + class ExecuteTestCase(test.TestCase): def test_retry_on_failure(self): fd, tmpfilename = tempfile.mkstemp() @@ -291,6 +295,11 @@ class GenericUtilsTestCase(test.TestCase): self.assertFalse(utils.bool_from_str(None)) self.assertFalse(utils.bool_from_str('junk')) + def test_generate_glance_url(self): + generated_url = utils.generate_glance_url() + actual_url = "http://%s:%d" % (FLAGS.glance_host, FLAGS.glance_port) + self.assertEqual(generated_url, actual_url) + class IsUUIDLikeTestCase(test.TestCase): def assertUUIDLike(self, val, expected): diff --git a/nova/tests/test_virt_drivers.py b/nova/tests/test_virt_drivers.py index 480247c91..8e20e999f 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): @@ -176,6 +177,10 @@ class _VirtDriverTestCase(test.TestCase): self.connection.poll_rescued_instances(10) @catch_notimplementederror + def test_poll_unconfirmed_resizes(self): + self.connection.poll_unconfirmed_resizes(10) + + @catch_notimplementederror def test_migrate_disk_and_power_off(self): instance_ref = test_utils.get_test_instance() network_info = test_utils.get_test_network_info() 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 91b4161b0..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: @@ -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 81157a4cd..57c93d9d0 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -910,3 +910,10 @@ def convert_to_list_dict(lst, label): if not isinstance(lst, list): lst = [lst] return [{label: x} for x in lst] + + +def generate_glance_url(): + """Generate the URL to glance.""" + # TODO(jk0): This will eventually need to take SSL into consideration + # when supported in glance. + return "http://%s:%d" % (FLAGS.glance_host, FLAGS.glance_port) 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..7edb2cf1a 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() @@ -286,6 +287,14 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() + def power_off(self, instance): + """Power off the specified instance.""" + raise NotImplementedError() + + def power_on(self, instance): + """Power on the specified instance""" + raise NotImplementedError() + def update_available_resource(self, ctxt, host): """Updates compute manager resource info on ComputeNode table. @@ -468,6 +477,11 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() + def poll_unconfirmed_resizes(self, resize_confirm_window): + """Poll for unconfirmed resizes""" + # TODO(Vek): Need to pass context in for access to auth_token + raise NotImplementedError() + def host_power_action(self, host, action): """Reboots, shuts down or powers up the host.""" raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index d5e2bf31b..96f521ee7 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): @@ -130,6 +130,9 @@ class FakeConnection(driver.ComputeDriver): def poll_rescued_instances(self, timeout): pass + def poll_unconfirmed_resizes(self, resize_confirm_window): + pass + def migrate_disk_and_power_off(self, instance, dest): pass diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 03a78db1f..fbf898317 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: @@ -487,6 +487,9 @@ class HyperVConnection(driver.ComputeDriver): def poll_rescued_instances(self, timeout): pass + def poll_unconfirmed_resizes(self, resize_confirm_window): + pass + def update_available_resource(self, ctxt, host): """This method is supported only by libvirt.""" return diff --git a/nova/virt/images.py b/nova/virt/images.py index 54c691a40..968fe1692 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -21,6 +21,9 @@ Handling of VM disk images. """ +import os + +from nova import exception from nova import flags from nova.image import glance as glance_image_service import nova.image @@ -37,7 +40,64 @@ 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 + + +def fetch_to_raw(context, image_href, path, user_id, project_id): + path_tmp = "%s.part" % path + metadata = fetch(context, image_href, path_tmp, user_id, project_id) + + def _qemu_img_info(path): + + out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C', + 'qemu-img', 'info', path) + + # output of qemu-img is 'field: value' + # the fields of interest are 'file format' and 'backing file' + data = {} + for line in out.splitlines(): + (field, val) = line.split(':', 1) + if val[0] == " ": + val = val[1:] + data[field] = val + + return(data) + + data = _qemu_img_info(path_tmp) + + fmt = data.get("file format", None) + if fmt == None: + os.unlink(path_tmp) + raise exception.ImageUnacceptable( + reason=_("'qemu-img info' parsing failed."), image_id=image_href) + + if fmt != "raw": + staged = "%s.converted" % path + if "backing file" in data: + backing_file = data['backing file'] + os.unlink(path_tmp) + raise exception.ImageUnacceptable(image_id=image_href, + reason=_("fmt=%(fmt)s backed by: %(backing_file)s") % locals()) + + LOG.debug("%s was %s, converting to raw" % (image_href, fmt)) + out, err = utils.execute('qemu-img', 'convert', '-O', 'raw', + path_tmp, staged) + os.unlink(path_tmp) + + data = _qemu_img_info(staged) + if data.get('file format', None) != "raw": + os.unlink(staged) + raise exception.ImageUnacceptable(image_id=image_href, + reason=_("Converted to raw, but format is now %s") % + data.get('file format', None)) + + os.rename(staged, path) + + else: + os.rename(path_tmp, path) + + return metadata diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 363a20ed0..064c2688f 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -29,15 +29,16 @@ Supports KVM, LXC, QEMU, UML, and XEN. (default: kvm). :libvirt_uri: Override for the default libvirt URI (depends on libvirt_type). :libvirt_xml_template: Libvirt XML Template. -:rescue_image_id: Rescue ami image (default: ami-rescue). -:rescue_kernel_id: Rescue aki image (default: aki-rescue). -:rescue_ramdisk_id: Rescue ari image (default: ari-rescue). +:rescue_image_id: Rescue ami image (None = original image). +:rescue_kernel_id: Rescue aki image (None = original image). +:rescue_ramdisk_id: Rescue ari image (None = original image). :injected_network_template: Template file for injected network :allow_same_net_traffic: Whether to allow in project network traffic """ import hashlib +import functools import multiprocessing import netaddr import os @@ -83,9 +84,9 @@ LOG = logging.getLogger('nova.virt.libvirt_conn') FLAGS = flags.FLAGS flags.DECLARE('live_migration_retry_count', 'nova.compute.manager') # TODO(vish): These flags should probably go into a shared location -flags.DEFINE_string('rescue_image_id', 'ami-rescue', 'Rescue ami image') -flags.DEFINE_string('rescue_kernel_id', 'aki-rescue', 'Rescue aki image') -flags.DEFINE_string('rescue_ramdisk_id', 'ari-rescue', 'Rescue ari image') +flags.DEFINE_string('rescue_image_id', None, 'Rescue ami image') +flags.DEFINE_string('rescue_kernel_id', None, 'Rescue aki image') +flags.DEFINE_string('rescue_ramdisk_id', None, 'Rescue ari image') flags.DEFINE_string('libvirt_xml_template', utils.abspath('virt/libvirt.xml.template'), 'Libvirt XML Template') @@ -124,8 +125,10 @@ flags.DEFINE_string('block_migration_flag', 'Define block migration behavior.') flags.DEFINE_integer('live_migration_bandwidth', 0, 'Define live migration behavior') -flags.DEFINE_string('qemu_img', 'qemu-img', - 'binary to use for qemu-img commands') +flags.DEFINE_string('snapshot_image_format', None, + 'Snapshot image format (valid options are : ' + 'raw, qcow2, vmdk, vdi).' + 'Defaults to same as source image') flags.DEFINE_string('libvirt_vif_type', 'bridge', 'Type of VIF to create.') flags.DEFINE_string('libvirt_vif_driver', @@ -196,7 +199,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 \ @@ -390,18 +393,15 @@ class LibvirtConnection(driver.ComputeDriver): def snapshot(self, context, instance, image_href): """Create snapshot from a running VM instance. - This command only works with qemu 0.14+, the qemu_img flag is - provided so that a locally compiled binary of qemu-img can be used - to support this command. - + This command only works with qemu 0.14+ """ 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, @@ -419,8 +419,11 @@ class LibvirtConnection(driver.ComputeDriver): arch = base['properties']['architecture'] metadata['properties']['architecture'] = arch - if 'disk_format' in base: - metadata['disk_format'] = base['disk_format'] + source_format = base.get('disk_format') or 'raw' + image_format = FLAGS.snapshot_image_format or source_format + if FLAGS.use_cow_images: + source_format = 'qcow2' + metadata['disk_format'] = image_format if 'container_format' in base: metadata['container_format'] = base['container_format'] @@ -443,12 +446,12 @@ class LibvirtConnection(driver.ComputeDriver): # Export the snapshot to a raw image temp_dir = tempfile.mkdtemp() out_path = os.path.join(temp_dir, snapshot_name) - qemu_img_cmd = (FLAGS.qemu_img, + qemu_img_cmd = ('qemu-img', 'convert', '-f', - 'qcow2', + source_format, '-O', - 'raw', + image_format, '-s', snapshot_name, disk_path, @@ -464,9 +467,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=None, xml=None): """Reboot a virtual machine, given an instance reference. This method actually destroys and re-creates the domain to ensure the @@ -477,7 +481,9 @@ class LibvirtConnection(driver.ComputeDriver): # NOTE(itoumsn): Use XML delived from the running instance # instead of using to_xml(instance, network_info). This is almost # the ultimate stupid workaround. - xml = virt_dom.XMLDesc(0) + if not xml: + xml = virt_dom.XMLDesc(0) + # NOTE(itoumsn): self.shutdown() and wait instead of self.destroy() is # better because we cannot ensure flushing dirty buffers # in the guest OS. But, in case of KVM, shutdown() does not work... @@ -541,48 +547,51 @@ class LibvirtConnection(driver.ComputeDriver): data recovery. """ - self.destroy(instance, network_info, cleanup=False) - - xml = self.to_xml(instance, network_info, rescue=True) - rescue_images = {'image_id': FLAGS.rescue_image_id, - 'kernel_id': FLAGS.rescue_kernel_id, - 'ramdisk_id': FLAGS.rescue_ramdisk_id} - self._create_image(context, instance, xml, '.rescue', rescue_images) - self._create_new_domain(xml) - - def _wait_for_rescue(): - """Called at an interval until the VM is running again.""" - instance_name = instance['name'] - - try: - state = self.get_info(instance_name)['state'] - except exception.NotFound: - msg = _("During reboot, %s disappeared.") % instance_name - LOG.error(msg) - raise utils.LoopingCallDone - if state == power_state.RUNNING: - msg = _("Instance %s rescued successfully.") % instance_name - LOG.info(msg) - raise utils.LoopingCallDone + virt_dom = self._conn.lookupByName(instance['name']) + unrescue_xml = virt_dom.XMLDesc(0) + unrescue_xml_path = os.path.join(FLAGS.instances_path, + instance['name'], + 'unrescue.xml') + f = open(unrescue_xml_path, 'w') + f.write(unrescue_xml) + f.close() - timer = utils.LoopingCall(_wait_for_rescue) - return timer.start(interval=0.5, now=True) + xml = self.to_xml(instance, network_info, rescue=True) + rescue_images = { + 'image_id': FLAGS.rescue_image_id or instance['image_ref'], + 'kernel_id': FLAGS.rescue_kernel_id or instance['kernel_id'], + 'ramdisk_id': FLAGS.rescue_ramdisk_id or instance['ramdisk_id'], + } + self._create_image(context, instance, xml, '.rescue', rescue_images, + network_info=network_info) + self.reboot(instance, network_info, xml=xml) @exception.wrap_exception() - def unrescue(self, instance, network_info): + def unrescue(self, instance, callback, network_info): """Reboot the VM which is being rescued back into primary images. Because reboot destroys and re-creates instances, unresue should simply call reboot. """ - self.reboot(instance, network_info) + unrescue_xml_path = os.path.join(FLAGS.instances_path, + instance['name'], + 'unrescue.xml') + f = open(unrescue_xml_path) + unrescue_xml = f.read() + f.close() + os.remove(unrescue_xml_path) + self.reboot(instance, network_info, xml=unrescue_xml) @exception.wrap_exception() def poll_rescued_instances(self, timeout): pass + @exception.wrap_exception() + def poll_unconfirmed_resizes(self, resize_confirm_window): + pass + # NOTE(ilyaalekseyev): Implementation like in multinics # for xenapi(tr3buchet) @exception.wrap_exception() @@ -764,23 +773,27 @@ class LibvirtConnection(driver.ComputeDriver): def _fetch_image(self, context, target, image_id, user_id, project_id, size=None): """Grab image and optionally attempt to resize it""" - images.fetch(context, image_id, target, user_id, project_id) + images.fetch_to_raw(context, image_id, target, user_id, project_id) if size: disk.extend(target, size) - def _create_local(self, target, local_size, prefix='G', fs_format=None): + def _create_local(self, target, local_size, unit='G', fs_format=None): """Create a blank image of specified size""" if not fs_format: fs_format = FLAGS.default_local_format - utils.execute('truncate', target, '-s', "%d%c" % (local_size, prefix)) + utils.execute('truncate', target, '-s', "%d%c" % (local_size, unit)) if fs_format: utils.execute('mkfs', '-t', fs_format, target) - def _create_swap(self, target, swap_gb): + 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_mb): """Create a swap file of specified size""" - self._create_local(target, swap_gb) + self._create_local(target, swap_mb, unit='M') utils.execute('mkswap', target) def _create_image(self, context, inst, libvirt_xml, suffix='', @@ -808,8 +821,10 @@ class LibvirtConnection(driver.ComputeDriver): utils.execute('mkdir', '-p', container_dir) # NOTE(vish): No need add the suffix to console.log - os.close(os.open(basepath('console.log', ''), - os.O_CREAT | os.O_WRONLY, 0660)) + console_log = basepath('console.log', '') + if os.path.exists(console_log): + utils.execute('chown', os.getuid(), console_log, run_as_root=True) + os.close(os.open(console_log, os.O_CREAT | os.O_WRONLY, 0660)) if not disk_images: disk_images = {'image_id': inst['image_ref'], @@ -859,35 +874,43 @@ class LibvirtConnection(driver.ComputeDriver): local_gb = inst['local_gb'] if local_gb and not self._volume_in_mapping( self.default_local_device, block_device_info): - self._cache_image(fn=self._create_local, + fn = functools.partial(self._create_ephemeral, + fs_label='ephemeral0', + os_type=inst.os_type) + self._cache_image(fn=fn, target=basepath('disk.local'), - fname="local_%s" % local_gb, + fname="ephemeral_%s_%s_%s" % + ("0", local_gb, inst.os_type), cow=FLAGS.use_cow_images, 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']) - swap_gb = 0 + swap_mb = 0 swap = driver.block_device_info_get_swap(block_device_info) if driver.swap_is_usable(swap): - swap_gb = swap['swap_size'] + swap_mb = swap['swap_size'] elif (inst_type['swap'] > 0 and not self._volume_in_mapping(self.default_swap_device, block_device_info)): - swap_gb = inst_type['swap'] + swap_mb = inst_type['swap'] - if swap_gb > 0: + if swap_mb > 0: self._cache_image(fn=self._create_swap, target=basepath('disk.swap'), - fname="swap_%s" % swap_gb, + fname="swap_%s" % swap_mb, cow=FLAGS.use_cow_images, - swap_gb=swap_gb) + swap_mb=swap_mb) # For now, we assume that if we're not using a kernel, we're using a # partitioned disk image where the target partition is the first @@ -908,10 +931,10 @@ class LibvirtConnection(driver.ComputeDriver): target=basepath('disk.config'), fname=fname, image_id=config_drive_id, - user=user, - project=project) + user_id=inst['user_id'], + project_id=inst['project_id'],) elif config_drive: - self._create_local(basepath('disk.config'), 64, prefix="M", + self._create_local(basepath('disk.config'), 64, unit='M', fs_format='msdos') # 64MB if inst['key_data']: @@ -981,15 +1004,16 @@ class LibvirtConnection(driver.ComputeDriver): nbd=FLAGS.use_cow_images, tune2fs=tune2fs) - if FLAGS.libvirt_type == 'lxc': - disk.setup_container(basepath('disk'), - container_dir=container_dir, - nbd=FLAGS.use_cow_images) except Exception as e: # This could be a windows image, or a vmdk format disk LOG.warn(_('instance %(inst_name)s: ignoring error injecting' ' data into image %(img_id)s (%(e)s)') % locals()) + if FLAGS.libvirt_type == 'lxc': + disk.setup_container(basepath('disk'), + container_dir=container_dir, + nbd=FLAGS.use_cow_images) + if FLAGS.libvirt_type == 'uml': utils.execute('chown', 'root', basepath('disk'), run_as_root=True) @@ -1102,6 +1126,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 +1139,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 +1728,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 +1820,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 c2f4f91e8..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.\ @@ -663,7 +663,9 @@ class IptablesFirewallDriver(FirewallDriver): if version == 6 and rule.protocol == 'icmp': protocol = 'icmpv6' - args = ['-j ACCEPT', '-p', protocol] + args = ['-j ACCEPT'] + if protocol: + args += ['-p', protocol] if protocol in ['udp', 'tcp']: if rule.from_port == rule.to_port: 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 9c138ee41..988007bae 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -38,6 +38,7 @@ from nova import ipv6 from nova import log as logging from nova import utils +from nova.compute import api as compute from nova.compute import power_state from nova.virt import driver from nova.virt.xenapi.network_utils import NetworkHelper @@ -48,9 +49,9 @@ XenAPI = None LOG = logging.getLogger("nova.virt.xenapi.vmops") FLAGS = flags.FLAGS -flags.DEFINE_integer('windows_version_timeout', 300, - 'number of seconds to wait for windows agent to be ' - 'fully operational') +flags.DEFINE_integer('agent_version_timeout', 300, + 'number of seconds to wait for agent to be fully ' + 'operational') flags.DEFINE_string('xenapi_vif_driver', 'nova.virt.xenapi.vif.XenAPIBridgeDriver', 'The XenAPI VIF driver using XenServer Network APIs.') @@ -77,6 +78,7 @@ class VMOps(object): """ def __init__(self, session): self.XenAPI = session.get_imported_xenapi() + self.compute_api = compute.API() self._session = session self.poll_rescue_last_ran = None VMHelper.XenAPI = self.XenAPI @@ -135,7 +137,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 +178,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: @@ -253,6 +255,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, @@ -320,15 +324,8 @@ class VMOps(object): def _check_agent_version(): LOG.debug(_("Querying agent version")) - if instance.os_type == 'windows': - # Windows will generally perform a setup process on first boot - # that can take a couple of minutes and then reboot. So we - # need to be more patient than normal as well as watch for - # domid changes - version = self.get_agent_version(instance, - timeout=FLAGS.windows_version_timeout) - else: - version = self.get_agent_version(instance) + + version = self.get_agent_version(instance) if not version: return @@ -624,15 +621,26 @@ 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): + def get_agent_version(self, instance): """Get the version of the agent running on the VM instance.""" + # The agent can be slow to start for a variety of reasons. On Windows, + # it will generally perform a setup process on first boot that can + # take a couple of minutes and then reboot. On Linux, the system can + # also take a while to boot. So we need to be more patient than + # normal as well as watch for domid changes + def _call(): # Send the encrypted password transaction_id = str(uuid.uuid4()) @@ -646,27 +654,26 @@ class VMOps(object): # (ie CRLF escaped) for some reason. Strip that off. return resp['message'].replace('\\r\\n', '') - if timeout: - vm_ref = self._get_vm_opaque_ref(instance) + vm_ref = self._get_vm_opaque_ref(instance) + vm_rec = self._session.get_xenapi().VM.get_record(vm_ref) + + domid = vm_rec['domid'] + + expiration = time.time() + FLAGS.agent_version_timeout + while time.time() < expiration: + ret = _call() + if ret: + return ret + vm_rec = self._session.get_xenapi().VM.get_record(vm_ref) + if vm_rec['domid'] != domid: + LOG.info(_('domid changed from %(olddomid)s to ' + '%(newdomid)s') % { + 'olddomid': domid, + 'newdomid': vm_rec['domid']}) + domid = vm_rec['domid'] - domid = vm_rec['domid'] - - expiration = time.time() + timeout - while time.time() < expiration: - ret = _call() - if ret: - return ret - - vm_rec = self._session.get_xenapi().VM.get_record(vm_ref) - if vm_rec['domid'] != domid: - LOG.info(_('domid changed from %(olddomid)s to ' - '%(newdomid)s') % { - 'olddomid': domid, - 'newdomid': vm_rec['domid']}) - domid = vm_rec['domid'] - else: - return _call() + return None def agent_update(self, instance, url, md5sum): """Update agent on the VM instance.""" @@ -995,6 +1002,16 @@ class VMOps(object): self._release_bootlock(original_vm_ref) self._start(instance, original_vm_ref) + def power_off(self, instance): + """Power off the specified instance.""" + vm_ref = self._get_vm_opaque_ref(instance) + self._shutdown(instance, vm_ref, hard=True) + + def power_on(self, instance): + """Power on the specified instance.""" + vm_ref = self._get_vm_opaque_ref(instance) + self._start(instance, vm_ref) + def poll_rescued_instances(self, timeout): """Look for expirable rescued instances. @@ -1034,6 +1051,27 @@ class VMOps(object): self._session.call_xenapi("VM.start", original_vm_ref, False, False) + def poll_unconfirmed_resizes(self, resize_confirm_window): + """Poll for unconfirmed resizes. + + Look for any unconfirmed resizes that are older than + `resize_confirm_window` and automatically confirm them. + """ + ctxt = nova_context.get_admin_context() + migrations = db.migration_get_all_unconfirmed(ctxt, + resize_confirm_window) + + migrations_info = dict(migration_count=len(migrations), + confirm_window=FLAGS.resize_confirm_window) + + if migrations_info["migration_count"] > 0: + LOG.info(_("Found %(migration_count)d unconfirmed migrations " + "older than %(confirm_window)d seconds") % migrations_info) + + for migration in migrations: + LOG.info(_("Automatically confirming migration %d"), migration.id) + self.compute_api.confirm_resize(ctxt, migration.instance_uuid) + def get_info(self, instance): """Return data about VM instance.""" vm_ref = self._get_vm_opaque_ref(instance) @@ -1153,6 +1191,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..79b02891d 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""" @@ -250,10 +250,22 @@ class XenAPIConnection(driver.ComputeDriver): """Unrescue the specified instance""" self._vmops.unrescue(instance, _callback) + def power_off(self, instance): + """Power off the specified instance""" + self._vmops.power_off(instance) + + def power_on(self, instance): + """Power on the specified instance""" + self._vmops.power_on(instance) + def poll_rescued_instances(self, timeout): """Poll for rescued instances""" self._vmops.poll_rescued_instances(timeout) + def poll_unconfirmed_resizes(self, resize_confirm_window): + """Poll for unconfirmed resizes""" + self._vmops.poll_unconfirmed_resizes(resize_confirm_window) + def reset_network(self, instance): """reset networking for specified instance""" self._vmops.reset_network(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) 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 |
