summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris Behrens <cbehrens@codestud.com>2011-09-21 09:00:18 +0000
committerChris Behrens <cbehrens@codestud.com>2011-09-21 09:00:18 +0000
commita63ca427e75c73611dd129bc7b5a625f6c06fa44 (patch)
treed80bef65fe63bb45fbf201f0c0475d3d699fa045
parent4d0bb8730a076b44d0a37fd0770c743b834e5751 (diff)
parentd9752d46554ffa87360bfd740177b40871cfbea6 (diff)
downloadnova-a63ca427e75c73611dd129bc7b5a625f6c06fa44.tar.gz
nova-a63ca427e75c73611dd129bc7b5a625f6c06fa44.tar.xz
nova-a63ca427e75c73611dd129bc7b5a625f6c06fa44.zip
Merged trunk
-rw-r--r--.mailmap1
-rw-r--r--Authors1
-rw-r--r--MANIFEST.in2
-rwxr-xr-xbin/nova-manage71
-rwxr-xr-xbin/nova-vncproxy15
-rw-r--r--etc/nova/api-paste.ini30
-rw-r--r--nova/api/auth.py31
-rw-r--r--nova/api/ec2/__init__.py51
-rw-r--r--nova/api/ec2/cloud.py68
-rw-r--r--nova/api/ec2/ec2utils.py2
-rw-r--r--nova/api/openstack/common.py93
-rw-r--r--nova/api/openstack/contrib/deferred_delete.py76
-rw-r--r--nova/api/openstack/contrib/flavorextradata.py46
-rw-r--r--nova/api/openstack/contrib/rescue.py13
-rw-r--r--nova/api/openstack/contrib/virtual_interfaces.py16
-rw-r--r--nova/api/openstack/contrib/volumes.py3
-rw-r--r--nova/api/openstack/create_instance_helper.py7
-rw-r--r--nova/api/openstack/flavors.py69
-rw-r--r--nova/api/openstack/image_metadata.py58
-rw-r--r--nova/api/openstack/images.py137
-rw-r--r--nova/api/openstack/ips.py59
-rw-r--r--nova/api/openstack/limits.py78
-rw-r--r--nova/api/openstack/schemas/v1.1/addresses.rng14
-rw-r--r--nova/api/openstack/schemas/v1.1/flavor.rng14
-rw-r--r--nova/api/openstack/schemas/v1.1/flavors.rng6
-rw-r--r--nova/api/openstack/schemas/v1.1/flavors_index.rng12
-rw-r--r--nova/api/openstack/schemas/v1.1/image.rng30
-rw-r--r--nova/api/openstack/schemas/v1.1/images.rng6
-rw-r--r--nova/api/openstack/schemas/v1.1/images_index.rng12
-rw-r--r--nova/api/openstack/schemas/v1.1/limits.rng28
-rw-r--r--nova/api/openstack/schemas/v1.1/metadata.rng9
-rw-r--r--nova/api/openstack/schemas/v1.1/server.rng6
-rw-r--r--nova/api/openstack/servers.py267
-rw-r--r--nova/api/openstack/versions.py246
-rw-r--r--nova/api/openstack/views/flavors.py3
-rw-r--r--nova/api/openstack/views/images.py41
-rw-r--r--nova/api/openstack/views/servers.py7
-rw-r--r--nova/api/openstack/views/versions.py2
-rw-r--r--nova/api/openstack/wsgi.py50
-rw-r--r--nova/compute/api.py153
-rw-r--r--nova/compute/manager.py109
-rw-r--r--nova/compute/task_states.py2
-rw-r--r--nova/compute/vm_states.py1
-rw-r--r--nova/context.py9
-rw-r--r--nova/db/api.py29
-rw-r--r--nova/db/sqlalchemy/api.py139
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/046_add_instance_swap.py48
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/047_remove_instances_fk_from_vif.py60
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/047_sqlite_downgrade.sql47
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/047_sqlite_upgrade.sql45
-rw-r--r--nova/db/sqlalchemy/models.py7
-rw-r--r--nova/exception.py2
-rw-r--r--nova/flags.py7
-rw-r--r--nova/image/__init__.py56
-rw-r--r--nova/image/fake.py3
-rw-r--r--nova/image/glance.py169
-rw-r--r--nova/image/s3.py3
-rw-r--r--nova/image/service.py200
-rw-r--r--nova/network/api.py14
-rwxr-xr-xnova/network/linux_net.py75
-rw-r--r--nova/network/manager.py67
-rw-r--r--nova/scheduler/abstract_scheduler.py2
-rw-r--r--nova/scheduler/base_scheduler.py7
-rw-r--r--nova/tests/api/ec2/public_key/dummy.fingerprint (renamed from nova/tests/public_key/dummy.fingerprint)0
-rw-r--r--nova/tests/api/ec2/public_key/dummy.pub (renamed from nova/tests/public_key/dummy.pub)0
-rw-r--r--nova/tests/api/ec2/test_cloud.py (renamed from nova/tests/test_cloud.py)63
-rw-r--r--nova/tests/api/openstack/common.py22
-rw-r--r--nova/tests/api/openstack/contrib/test_createserverext.py6
-rw-r--r--nova/tests/api/openstack/contrib/test_rescue.py23
-rw-r--r--nova/tests/api/openstack/contrib/test_virtual_interfaces.py17
-rw-r--r--nova/tests/api/openstack/contrib/test_volumes.py77
-rw-r--r--nova/tests/api/openstack/fakes.py129
-rw-r--r--nova/tests/api/openstack/test_api.py25
-rw-r--r--nova/tests/api/openstack/test_common.py176
-rw-r--r--nova/tests/api/openstack/test_extensions.py2
-rw-r--r--nova/tests/api/openstack/test_flavors.py259
-rw-r--r--nova/tests/api/openstack/test_image_metadata.py163
-rw-r--r--nova/tests/api/openstack/test_images.py1200
-rw-r--r--nova/tests/api/openstack/test_limits.py104
-rw-r--r--nova/tests/api/openstack/test_server_actions.py3
-rw-r--r--nova/tests/api/openstack/test_servers.py383
-rw-r--r--nova/tests/api/openstack/test_versions.py604
-rw-r--r--nova/tests/api/openstack/test_wsgi.py57
-rw-r--r--nova/tests/db/fakes.py4
-rw-r--r--nova/tests/fake_network.py253
-rw-r--r--nova/tests/glance/stubs.py74
-rw-r--r--nova/tests/image/test_glance.py637
-rw-r--r--nova/tests/integrated/integrated_helpers.py10
-rw-r--r--nova/tests/integrated/test_servers.py146
-rw-r--r--nova/tests/integrated/test_xml.py12
-rw-r--r--nova/tests/test_api.py2
-rw-r--r--nova/tests/test_compute.py321
-rw-r--r--nova/tests/test_db_api.py34
-rw-r--r--nova/tests/test_direct.py2
-rw-r--r--nova/tests/test_libvirt.py331
-rwxr-xr-xnova/tests/test_linux_net.py69
-rw-r--r--nova/tests/test_metadata.py9
-rw-r--r--nova/tests/test_network.py256
-rw-r--r--nova/tests/test_utils.py9
-rw-r--r--nova/tests/test_virt_drivers.py7
-rw-r--r--nova/tests/test_vmwareapi.py3
-rw-r--r--nova/tests/test_xenapi.py6
-rw-r--r--nova/tests/vmwareapi/stubs.py2
-rw-r--r--nova/utils.py7
-rw-r--r--nova/version.py2
-rw-r--r--nova/virt/disk.py61
-rw-r--r--nova/virt/driver.py16
-rw-r--r--nova/virt/fake.py5
-rw-r--r--nova/virt/hyperv.py5
-rw-r--r--nova/virt/images.py62
-rw-r--r--nova/virt/libvirt/connection.py182
-rw-r--r--nova/virt/libvirt/firewall.py8
-rw-r--r--nova/virt/vmwareapi/fake.py8
-rw-r--r--nova/virt/vmwareapi/vif.py27
-rw-r--r--nova/virt/vmwareapi/vm_util.py26
-rw-r--r--nova/virt/vmwareapi/vmops.py110
-rw-r--r--nova/virt/vmwareapi/vmware_images.py69
-rw-r--r--nova/virt/vmwareapi_conn.py2
-rw-r--r--nova/virt/xenapi/vm_utils.py17
-rw-r--r--nova/virt/xenapi/vmops.py120
-rw-r--r--nova/virt/xenapi_conn.py16
-rw-r--r--nova/volume/driver.py9
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/glance6
-rw-r--r--tools/esx/guest_tool.py70
-rw-r--r--tools/pip-requires1
125 files changed, 5446 insertions, 3817 deletions
diff --git a/.mailmap b/.mailmap
index f2f59d81b..b6ae040d6 100644
--- a/.mailmap
+++ b/.mailmap
@@ -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>
diff --git a/Authors b/Authors
index 4e0848692..8d6837ea4 100644
--- a/Authors
+++ b/Authors
@@ -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