summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNaveed Massjouni <naveedm9@gmail.com>2011-09-08 13:44:37 -0400
committerNaveed Massjouni <naveedm9@gmail.com>2011-09-08 13:44:37 -0400
commit236cbc0c13deaef69daba38e83cc7c294ea0bac2 (patch)
tree8f6a482ad293d698fbbf3abbe95339f208b24157
parentd155f4224cb97e43cacd4102ba01f0e1775dfbdf (diff)
parentbb3b61b61dc4b45fba4bdab7ead69af239eac40a (diff)
downloadnova-236cbc0c13deaef69daba38e83cc7c294ea0bac2.tar.gz
nova-236cbc0c13deaef69daba38e83cc7c294ea0bac2.tar.xz
nova-236cbc0c13deaef69daba38e83cc7c294ea0bac2.zip
Merge from trunk
-rw-r--r--.mailmap1
-rw-r--r--Authors2
-rwxr-xr-xbin/nova-manage41
-rw-r--r--nova/api/ec2/cloud.py13
-rw-r--r--nova/api/openstack/contrib/createserverext.py26
-rw-r--r--nova/api/openstack/create_instance_helper.py2
-rw-r--r--nova/api/openstack/schemas/v1.1/server.rng2
-rw-r--r--nova/api/openstack/servers.py17
-rw-r--r--nova/api/openstack/views/addresses.py1
-rw-r--r--nova/api/openstack/views/servers.py2
-rw-r--r--nova/compute/api.py9
-rw-r--r--nova/db/api.py10
-rw-r--r--nova/db/sqlalchemy/api.py29
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/045_add_network_priority.py44
-rw-r--r--nova/db/sqlalchemy/models.py1
-rw-r--r--nova/exception.py8
-rw-r--r--nova/network/manager.py42
-rw-r--r--nova/network/quantum/__init__.py16
-rw-r--r--nova/network/quantum/client.py307
-rw-r--r--nova/network/quantum/manager.py324
-rw-r--r--nova/network/quantum/melange_connection.py141
-rw-r--r--nova/network/quantum/melange_ipam_lib.py205
-rw-r--r--nova/network/quantum/nova_ipam_lib.py195
-rw-r--r--nova/network/quantum/quantum_connection.py118
-rw-r--r--nova/scheduler/host_filter.py6
-rw-r--r--nova/scheduler/manager.py10
-rw-r--r--nova/tests/api/openstack/common.py22
-rw-r--r--nova/tests/api/openstack/contrib/test_createserverext.py99
-rw-r--r--nova/tests/api/openstack/test_servers.py159
-rw-r--r--nova/tests/test_compute.py13
-rw-r--r--nova/tests/test_network.py7
-rw-r--r--nova/tests/test_quantum.py323
-rw-r--r--nova/tests/test_xenapi.py2
-rw-r--r--nova/utils.py9
-rw-r--r--nova/virt/libvirt/vif.py2
35 files changed, 2122 insertions, 86 deletions
diff --git a/.mailmap b/.mailmap
index 5c8df80e0..f2f59d81b 100644
--- a/.mailmap
+++ b/.mailmap
@@ -15,6 +15,7 @@
<code@term.ie> <termie@preciousroy.local>
<corywright@gmail.com> <cory.wright@rackspace.com>
<dan@nicira.com> <danwent@dan-xs3-cs>
+<dan@nicira.com> danwent@gmail.com
<devin.carlen@gmail.com> <devcamcar@illian.local>
<ewan.mellor@citrix.com> <emellor@silver>
<itoumsn@nttdata.co.jp> <itoumsn@shayol>
diff --git a/Authors b/Authors
index b9e7a7d23..a6598103f 100644
--- a/Authors
+++ b/Authors
@@ -11,6 +11,7 @@ Antony Messerli <ant@openstack.org>
Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
Arvind Somya <asomya@cisco.com>
Bilal Akhtar <bilalakhtar@ubuntu.com>
+Brad Hall <brad@nicira.com>
Brian Lamar <brian.lamar@rackspace.com>
Brian Schott <bschott@isi.edu>
Brian Waldon <brian.waldon@rackspace.com>
@@ -30,6 +31,7 @@ Devendra Modium <dmodium@isi.edu>
Devin Carlen <devin.carlen@gmail.com>
Donal Lafferty <donal.lafferty@citrix.com>
Ed Leafe <ed@leafe.com>
+Edouard Thuleau <thuleau@gmail.com>
Eldar Nugaev <reldan@oscloud.ru>
Eric Day <eday@oddments.org>
Eric Windisch <eric@cloudscaling.com>
diff --git a/bin/nova-manage b/bin/nova-manage
index c3b2c71ce..bc191b2f0 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -59,11 +59,11 @@ import glob
import json
import math
import netaddr
+from optparse import OptionParser
import os
import sys
import time
-from optparse import OptionParser
# If ../nova/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
@@ -685,10 +685,17 @@ class NetworkCommands(object):
help='Multi host')
@args('--dns1', dest="dns1", metavar="<DNS Address>", help='First DNS')
@args('--dns2', dest="dns2", metavar="<DNS Address>", help='Second DNS')
+ @args('--uuid', dest="net_uuid", metavar="<network uuid>",
+ help='Network UUID')
+ @args('--project_id', dest="project_id", metavar="<project id>",
+ help='Project id')
+ @args('--priority', dest="priority", metavar="<number>",
+ help='Network interface priority')
def create(self, label=None, fixed_range_v4=None, num_networks=None,
network_size=None, multi_host=None, vlan_start=None,
vpn_start=None, fixed_range_v6=None, gateway_v6=None,
- bridge=None, bridge_interface=None, dns1=None, dns2=None):
+ bridge=None, bridge_interface=None, dns1=None, dns2=None,
+ project_id=None, priority=None, uuid=None):
"""Creates fixed ips for host by range"""
# check for certain required inputs
@@ -765,7 +772,10 @@ class NetworkCommands(object):
bridge=bridge,
bridge_interface=bridge_interface,
dns1=dns1,
- dns2=dns2)
+ dns2=dns2,
+ project_id=project_id,
+ priority=priority,
+ uuid=uuid)
def list(self):
"""List all created networks"""
@@ -790,16 +800,29 @@ class NetworkCommands(object):
network.project_id,
network.uuid)
+ def quantum_list(self):
+ """List all created networks with Quantum-relevant fields"""
+ _fmt = "%-32s\t%-10s\t%-10s\t%s , %s"
+ print _fmt % (_('uuid'),
+ _('project'),
+ _('priority'),
+ _('cidr_v4'),
+ _('cidr_v6'))
+ for network in db.network_get_all(context.get_admin_context()):
+ print _fmt % (network.uuid,
+ network.project_id,
+ network.priority,
+ network.cidr,
+ network.cidr_v6)
+
@args('--network', dest="fixed_range", metavar='<x.x.x.x/yy>',
help='Network to delete')
def delete(self, fixed_range):
"""Deletes a network"""
- network = db.network_get_by_cidr(context.get_admin_context(), \
- fixed_range)
- if network.project_id is not None:
- raise ValueError(_('Network must be disassociated from project %s'
- ' before delete' % network.project_id))
- db.network_delete_safe(context.get_admin_context(), network.id)
+
+ # delete the network
+ net_manager = utils.import_object(FLAGS.network_manager)
+ net_manager.delete_network(context.get_admin_context(), fixed_range)
@args('--network', dest="fixed_range", metavar='<x.x.x.x/yy>',
help='Network to modify')
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index fe44191c8..049ca6f93 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -1020,14 +1020,6 @@ class CloudController(object):
'status': volume['attach_status'],
'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
- @staticmethod
- def _convert_to_set(lst, label):
- if lst is None or lst == []:
- return None
- if not isinstance(lst, list):
- lst = [lst]
- return [{label: x} for x in lst]
-
def _format_kernel_id(self, instance_ref, result, key):
kernel_id = instance_ref['kernel_id']
if kernel_id is None:
@@ -1186,7 +1178,7 @@ class CloudController(object):
if instance.get('security_groups'):
for security_group in instance['security_groups']:
security_group_names.append(security_group['name'])
- result['groupSet'] = CloudController._convert_to_set(
+ result['groupSet'] = utils.convert_to_list_dict(
security_group_names, 'groupId')
def _format_instances(self, context, instance_id=None, use_v6=False,
@@ -1250,7 +1242,8 @@ class CloudController(object):
i['keyName'] = '%s (%s, %s)' % (i['keyName'],
instance['project_id'],
instance['host'])
- i['productCodesSet'] = self._convert_to_set([], 'product_codes')
+ i['productCodesSet'] = utils.convert_to_list_dict([],
+ 'product_codes')
self._format_instance_type(instance, i)
i['launchTime'] = instance['created_at']
i['amiLaunchIndex'] = instance['launch_index']
diff --git a/nova/api/openstack/contrib/createserverext.py b/nova/api/openstack/contrib/createserverext.py
index ba72fdb0b..af7f37f13 100644
--- a/nova/api/openstack/contrib/createserverext.py
+++ b/nova/api/openstack/contrib/createserverext.py
@@ -14,18 +14,34 @@
# License for the specific language governing permissions and limitations
# under the License
+from nova import utils
from nova.api.openstack import create_instance_helper as helper
from nova.api.openstack import extensions
from nova.api.openstack import servers
from nova.api.openstack import wsgi
-class Createserverext(extensions.ExtensionDescriptor):
- """The servers create ext
+class CreateServerController(servers.ControllerV11):
+ def _build_view(self, req, instance, is_detail=False):
+ server = super(CreateServerController, self)._build_view(req,
+ instance,
+ is_detail)
+ if is_detail:
+ self._build_security_groups(server['server'], instance)
+ return server
+
+ def _build_security_groups(self, response, inst):
+ sg_names = []
+ sec_groups = inst.get('security_groups')
+ if sec_groups:
+ sg_names = [sec_group['name'] for sec_group in sec_groups]
- Exposes addFixedIp and removeFixedIp actions on servers.
+ response['security_groups'] = utils.convert_to_list_dict(sg_names,
+ 'name')
- """
+
+class Createserverext(extensions.ExtensionDescriptor):
+ """The servers create ext"""
def get_name(self):
return "Createserverext"
@@ -58,7 +74,7 @@ class Createserverext(extensions.ExtensionDescriptor):
deserializer = wsgi.RequestDeserializer(body_deserializers)
res = extensions.ResourceExtension('os-create-server-ext',
- controller=servers.ControllerV11(),
+ controller=CreateServerController(),
deserializer=deserializer,
serializer=serializer)
resources.append(res)
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index 29e071609..67e669c17 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -282,7 +282,7 @@ class CreateInstanceHelper(object):
try:
ramdisk_id = image_meta['properties']['ramdisk_id']
except KeyError:
- raise exception.RamdiskNotFoundForImage(image_id=image_id)
+ ramdisk_id = None
return kernel_id, ramdisk_id
diff --git a/nova/api/openstack/schemas/v1.1/server.rng b/nova/api/openstack/schemas/v1.1/server.rng
index 68f86373c..4eb1a0b85 100644
--- a/nova/api/openstack/schemas/v1.1/server.rng
+++ b/nova/api/openstack/schemas/v1.1/server.rng
@@ -1,6 +1,8 @@
<element name="server" ns="http://docs.openstack.org/compute/api/v1.1"
xmlns="http://relaxng.org/ns/structure/1.0">
<attribute name="name"> <text/> </attribute>
+ <attribute name="userId"> <text/> </attribute>
+ <attribute name="tenantId"> <text/> </attribute>
<attribute name="id"> <text/> </attribute>
<attribute name="uuid"> <text/> </attribute>
<attribute name="updated"> <text/> </attribute>
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 571185169..6159041e8 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -897,6 +897,8 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer):
server_elem.set('id', str(server_dict['id']))
if detailed:
server_elem.set('uuid', str(server_dict['uuid']))
+ server_elem.set('userId', str(server_dict['user_id']))
+ server_elem.set('tenantId', str(server_dict['tenant_id']))
server_elem.set('updated', str(server_dict['updated']))
server_elem.set('created', str(server_dict['created']))
server_elem.set('hostId', str(server_dict['hostId']))
@@ -961,6 +963,19 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer):
self._populate_server(server, server_dict['server'], True)
return self._to_xml(server)
+ 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
+
def create_resource(version='1.0'):
controller = {
@@ -972,7 +987,7 @@ def create_resource(version='1.0'):
"attributes": {
"server": ["id", "imageId", "name", "flavorId", "hostId",
"status", "progress", "adminPass", "flavorRef",
- "imageRef"],
+ "imageRef", "userId", "tenantId"],
"link": ["rel", "type", "href"],
},
"dict_collections": {
diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py
index 8f07a2289..8d38bc9c3 100644
--- a/nova/api/openstack/views/addresses.py
+++ b/nova/api/openstack/views/addresses.py
@@ -88,7 +88,6 @@ class ViewBuilderV11(ViewBuilder):
try:
return interface['network']['label']
except (TypeError, KeyError) as exc:
- LOG.exception(exc)
raise TypeError
def _extract_ipv4_addresses(self, interface):
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index 3a13d15f1..ac09b5864 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -66,6 +66,8 @@ class ViewBuilder(object):
inst_dict = {
'id': inst['id'],
'name': inst['display_name'],
+ 'user_id': inst.get('user_id', ''),
+ 'tenant_id': inst.get('project_id', ''),
'status': common.status_from_state(vm_state, task_state)}
# Return the metadata as a dictionary
diff --git a/nova/compute/api.py b/nova/compute/api.py
index e045ef3de..4e2944bb7 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -383,10 +383,6 @@ class API(base.Base):
If you are changing this method, be sure to update both
call paths.
"""
- instance = dict(launch_index=num, **base_options)
- instance = self.db.instance_create(context, instance)
- instance_id = instance['id']
-
elevated = context.elevated()
if security_group is None:
security_group = ['default']
@@ -400,6 +396,10 @@ class API(base.Base):
security_group_name)
security_groups.append(group['id'])
+ instance = dict(launch_index=num, **base_options)
+ instance = self.db.instance_create(context, instance)
+ instance_id = instance['id']
+
for security_group_id in security_groups:
self.db.instance_add_security_group(elevated,
instance_id,
@@ -877,6 +877,7 @@ class API(base.Base):
'image': 'image_ref',
'name': 'display_name',
'instance_name': 'name',
+ 'tenant_id': 'project_id',
'recurse_zones': None,
'flavor': _remap_flavor_filter,
'fixed_ip': _remap_fixed_ip_filter}
diff --git a/nova/db/api.py b/nova/db/api.py
index 148887635..c03a86671 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -420,6 +420,11 @@ def virtual_interface_get_by_address(context, address):
return IMPL.virtual_interface_get_by_address(context, address)
+def virtual_interface_get_by_uuid(context, vif_uuid):
+ """Gets a virtual interface from the table filtering on vif uuid."""
+ return IMPL.virtual_interface_get_by_uuid(context, vif_uuid)
+
+
def virtual_interface_get_by_fixed_ip(context, fixed_ip_id):
"""Gets the virtual interface fixed_ip is associated with."""
return IMPL.virtual_interface_get_by_fixed_ip(context, fixed_ip_id)
@@ -715,6 +720,11 @@ def network_get_by_bridge(context, bridge):
return IMPL.network_get_by_bridge(context, bridge)
+def network_get_by_uuid(context, uuid):
+ """Get a network by uuid or raise if it does not exist."""
+ return IMPL.network_get_by_uuid(context, uuid)
+
+
def network_get_by_cidr(context, cidr):
"""Get a network by cidr or raise if it does not exist"""
return IMPL.network_get_by_cidr(context, cidr)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index b99667afc..523258841 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -945,6 +945,22 @@ def virtual_interface_get_by_address(context, address):
@require_context
+def virtual_interface_get_by_uuid(context, vif_uuid):
+ """Gets a virtual interface from the table.
+
+ :param vif_uuid: the uuid of the interface you're looking to get
+ """
+ session = get_session()
+ vif_ref = session.query(models.VirtualInterface).\
+ filter_by(uuid=vif_uuid).\
+ options(joinedload('network')).\
+ options(joinedload('instance')).\
+ options(joinedload('fixed_ips')).\
+ first()
+ return vif_ref
+
+
+@require_context
def virtual_interface_get_by_fixed_ip(context, fixed_ip_id):
"""Gets the virtual interface fixed_ip is associated with.
@@ -1858,6 +1874,19 @@ def network_get_by_bridge(context, bridge):
@require_admin_context
+def network_get_by_uuid(context, uuid):
+ session = get_session()
+ result = session.query(models.Network).\
+ filter_by(uuid=uuid).\
+ filter_by(deleted=False).\
+ first()
+
+ if not result:
+ raise exception.NetworkNotFoundForUUID(uuid=uuid)
+ return result
+
+
+@require_admin_context
def network_get_by_cidr(context, cidr):
session = get_session()
result = session.query(models.Network).\
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/045_add_network_priority.py b/nova/db/sqlalchemy/migrate_repo/versions/045_add_network_priority.py
new file mode 100644
index 000000000..b9b0ea37c
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/045_add_network_priority.py
@@ -0,0 +1,44 @@
+# Copyright 2011 Nicira, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from sqlalchemy import *
+from migrate import *
+
+from nova import log as logging
+from nova import utils
+
+
+meta = MetaData()
+
+networks = Table('networks', meta,
+ Column("id", Integer(), primary_key=True, nullable=False))
+
+# Add priority column to networks table
+priority = Column('priority', Integer())
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+
+ try:
+ networks.create_column(priority)
+ except Exception:
+ logging.error(_("priority column not added to networks table"))
+ raise
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+ networks.drop_column(priority)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 854034f12..211049112 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -628,6 +628,7 @@ class Network(BASE, NovaBase):
dhcp_start = Column(String(255))
project_id = Column(String(255))
+ priority = Column(Integer)
host = Column(String(255)) # , ForeignKey('hosts.id'))
uuid = Column(String(36))
diff --git a/nova/exception.py b/nova/exception.py
index df6ff25cd..8e73fdfc8 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -403,10 +403,6 @@ class KernelNotFoundForImage(ImageNotFound):
message = _("Kernel not found for image %(image_id)s.")
-class RamdiskNotFoundForImage(ImageNotFound):
- message = _("Ramdisk not found for image %(image_id)s.")
-
-
class UserNotFound(NotFound):
message = _("User %(user_id)s could not be found.")
@@ -439,6 +435,10 @@ class NetworkNotFoundForBridge(NetworkNotFound):
message = _("Network could not be found for bridge %(bridge)s")
+class NetworkNotFoundForUUID(NetworkNotFound):
+ message = _("Network could not be found for uuid %(uuid)s")
+
+
class NetworkNotFoundForCidr(NetworkNotFound):
message = _("Network could not be found with cidr %(cidr)s.")
diff --git a/nova/network/manager.py b/nova/network/manager.py
index e6b30d1a0..a7bdcbc30 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -74,7 +74,7 @@ flags.DEFINE_string('flat_network_bridge', None,
'Bridge for simple network instances')
flags.DEFINE_string('flat_network_dns', '8.8.4.4',
'Dns for simple network')
-flags.DEFINE_bool('flat_injected', True,
+flags.DEFINE_bool('flat_injected', False,
'Whether to attempt to inject network setup into guest')
flags.DEFINE_string('flat_interface', None,
'FlatDhcp will bridge into this interface if set')
@@ -448,7 +448,7 @@ class NetworkManager(manager.SchedulerDependentManager):
try:
fixed_ips = kwargs.get('fixed_ips') or \
self.db.fixed_ip_get_by_instance(context, instance_id)
- except exceptions.FixedIpNotFoundForInstance:
+ except exception.FixedIpNotFoundForInstance:
fixed_ips = []
LOG.debug(_("network deallocation for instance |%s|"), instance_id,
context=context)
@@ -484,6 +484,9 @@ class NetworkManager(manager.SchedulerDependentManager):
for vif in vifs:
network = vif['network']
+ if network is None:
+ continue
+
# determine which of the instance's IPs belong to this network
network_IPs = [fixed_ip['address'] for fixed_ip in fixed_ips if
fixed_ip['network_id'] == network['id']]
@@ -546,21 +549,23 @@ class NetworkManager(manager.SchedulerDependentManager):
def _allocate_mac_addresses(self, context, instance_id, networks):
"""Generates mac addresses and creates vif rows in db for them."""
for network in networks:
- vif = {'address': self.generate_mac_address(),
+ self.add_virtual_interface(context, instance_id, network['id'])
+
+ def add_virtual_interface(self, context, instance_id, network_id):
+ vif = {'address': self.generate_mac_address(),
'instance_id': instance_id,
- 'network_id': network['id'],
+ 'network_id': network_id,
'uuid': str(utils.gen_uuid())}
- # try FLAG times to create a vif record with a unique mac_address
- for i in range(FLAGS.create_unique_mac_address_attempts):
- try:
- self.db.virtual_interface_create(context, vif)
- break
- except exception.VirtualInterfaceCreateException:
- vif['address'] = self.generate_mac_address()
- else:
- self.db.virtual_interface_delete_by_instance(context,
+ # try FLAG times to create a vif record with a unique mac_address
+ for _ in xrange(FLAGS.create_unique_mac_address_attempts):
+ try:
+ return self.db.virtual_interface_create(context, vif)
+ except exception.VirtualInterfaceCreateException:
+ vif['address'] = self.generate_mac_address()
+ else:
+ self.db.virtual_interface_delete_by_instance(context,
instance_id)
- raise exception.VirtualInterfaceMacAddressException()
+ raise exception.VirtualInterfaceMacAddressException()
def generate_mac_address(self):
"""Generate an Ethernet MAC address."""
@@ -789,6 +794,15 @@ class NetworkManager(manager.SchedulerDependentManager):
self._create_fixed_ips(context, network['id'])
return networks
+ def delete_network(self, context, fixed_range, require_disassociated=True):
+
+ network = db.network_get_by_cidr(context, fixed_range)
+
+ if require_disassociated and network.project_id is not None:
+ raise ValueError(_('Network must be disassociated from project %s'
+ ' before delete' % network.project_id))
+ db.network_delete_safe(context, network.id)
+
@property
def _bottom_reserved_ips(self): # pylint: disable=R0201
"""Number of reserved ips at the bottom of the range."""
diff --git a/nova/network/quantum/__init__.py b/nova/network/quantum/__init__.py
new file mode 100644
index 000000000..f7fbfb511
--- /dev/null
+++ b/nova/network/quantum/__init__.py
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Nicira Networks
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
diff --git a/nova/network/quantum/client.py b/nova/network/quantum/client.py
new file mode 100644
index 000000000..40c68dfdc
--- /dev/null
+++ b/nova/network/quantum/client.py
@@ -0,0 +1,307 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Citrix Systems
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+# @author: Tyler Smith, Cisco Systems
+
+import httplib
+import json
+import socket
+import urllib
+
+
+# FIXME(danwent): All content in this file should be removed once the
+# packaging work for the quantum client libraries is complete.
+# At that point, we will be able to just install the libraries as a
+# dependency and import from quantum.client.* and quantum.common.*
+# Until then, we have simplified versions of these classes in this file.
+
+class JSONSerializer(object):
+ """This is a simple json-only serializer to use until we can just grab
+ the standard serializer from the quantum library.
+ """
+ def serialize(self, data, content_type):
+ try:
+ return json.dumps(data)
+ except TypeError:
+ pass
+ return json.dumps(to_primitive(data))
+
+ def deserialize(self, data, content_type):
+ return json.loads(data)
+
+
+# The full client lib will expose more
+# granular exceptions, for now, just try to distinguish
+# between the cases we care about.
+class QuantumNotFoundException(Exception):
+ """Indicates that Quantum Server returned 404"""
+ pass
+
+
+class QuantumServerException(Exception):
+ """Indicates any non-404 error from Quantum Server"""
+ pass
+
+
+class QuantumIOException(Exception):
+ """Indicates network IO trouble reaching Quantum Server"""
+ pass
+
+
+class api_call(object):
+ """A Decorator to add support for format and tenant overriding"""
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, instance, owner):
+ def with_params(*args, **kwargs):
+ """Temporarily set format and tenant for this request"""
+ (format, tenant) = (instance.format, instance.tenant)
+
+ if 'format' in kwargs:
+ instance.format = kwargs['format']
+ if 'tenant' in kwargs:
+ instance.tenant = kwargs['tenant']
+
+ ret = None
+ try:
+ ret = self.func(instance, *args)
+ finally:
+ (instance.format, instance.tenant) = (format, tenant)
+ return ret
+ return with_params
+
+
+class Client(object):
+ """A base client class - derived from Glance.BaseClient"""
+
+ action_prefix = '/v1.0/tenants/{tenant_id}'
+
+ """Action query strings"""
+ networks_path = "/networks"
+ network_path = "/networks/%s"
+ ports_path = "/networks/%s/ports"
+ port_path = "/networks/%s/ports/%s"
+ attachment_path = "/networks/%s/ports/%s/attachment"
+
+ def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None,
+ format="xml", testing_stub=None, key_file=None,
+ cert_file=None, logger=None):
+ """Creates a new client to some service.
+
+ :param host: The host where service resides
+ :param port: The port where service resides
+ :param use_ssl: True to use SSL, False to use HTTP
+ :param tenant: The tenant ID to make requests with
+ :param format: The format to query the server with
+ :param testing_stub: A class that stubs basic server methods for tests
+ :param key_file: The SSL key file to use if use_ssl is true
+ :param cert_file: The SSL cert file to use if use_ssl is true
+ """
+ self.host = host
+ self.port = port
+ self.use_ssl = use_ssl
+ self.tenant = tenant
+ self.format = format
+ self.connection = None
+ self.testing_stub = testing_stub
+ self.key_file = key_file
+ self.cert_file = cert_file
+ self.logger = logger
+
+ def get_connection_type(self):
+ """Returns the proper connection type"""
+ if self.testing_stub:
+ return self.testing_stub
+ elif self.use_ssl:
+ return httplib.HTTPSConnection
+ else:
+ return httplib.HTTPConnection
+
+ def do_request(self, method, action, body=None,
+ headers=None, params=None):
+ """Connects to the server and issues a request.
+ Returns the result data, or raises an appropriate exception if
+ HTTP status code is not 2xx
+
+ :param method: HTTP method ("GET", "POST", "PUT", etc...)
+ :param body: string of data to send, or None (default)
+ :param headers: mapping of key/value pairs to add as headers
+ :param params: dictionary of key/value pairs to add to append
+ to action
+ """
+
+ # Ensure we have a tenant id
+ if not self.tenant:
+ raise Exception(_("Tenant ID not set"))
+
+ # Add format and tenant_id
+ action += ".%s" % self.format
+ action = Client.action_prefix + action
+ action = action.replace('{tenant_id}', self.tenant)
+
+ if type(params) is dict:
+ action += '?' + urllib.urlencode(params)
+
+ try:
+ connection_type = self.get_connection_type()
+ headers = headers or {"Content-Type":
+ "application/%s" % self.format}
+
+ # Open connection and send request, handling SSL certs
+ certs = {'key_file': self.key_file, 'cert_file': self.cert_file}
+ certs = dict((x, certs[x]) for x in certs if certs[x] != None)
+
+ if self.use_ssl and len(certs):
+ c = connection_type(self.host, self.port, **certs)
+ else:
+ c = connection_type(self.host, self.port)
+
+ if self.logger:
+ self.logger.debug(
+ _("Quantum Client Request:\n%(method)s %(action)s\n" %
+ locals()))
+ if body:
+ self.logger.debug(body)
+
+ c.request(method, action, body, headers)
+ res = c.getresponse()
+ status_code = self.get_status_code(res)
+ data = res.read()
+
+ if self.logger:
+ self.logger.debug("Quantum Client Reply (code = %s) :\n %s" \
+ % (str(status_code), data))
+
+ if status_code == httplib.NOT_FOUND:
+ raise QuantumNotFoundException(
+ _("Quantum entity not found: %s" % data))
+
+ if status_code in (httplib.OK,
+ httplib.CREATED,
+ httplib.ACCEPTED,
+ httplib.NO_CONTENT):
+ if data is not None and len(data):
+ return self.deserialize(data, status_code)
+ else:
+ raise QuantumServerException(
+ _("Server %(status_code)s error: %(data)s"
+ % locals()))
+
+ except (socket.error, IOError), e:
+ raise QuantumIOException(_("Unable to connect to "
+ "server. Got error: %s" % e))
+
+ def get_status_code(self, response):
+ """Returns the integer status code from the response, which
+ can be either a Webob.Response (used in testing) or httplib.Response
+ """
+ if hasattr(response, 'status_int'):
+ return response.status_int
+ else:
+ return response.status
+
+ def serialize(self, data):
+ if not data:
+ return None
+ elif type(data) is dict:
+ return JSONSerializer().serialize(data, self.content_type())
+ else:
+ raise Exception(_("unable to deserialize object of type = '%s'" %
+ type(data)))
+
+ def deserialize(self, data, status_code):
+ if status_code == 202:
+ return data
+ return JSONSerializer().deserialize(data, self.content_type())
+
+ def content_type(self, format=None):
+ if not format:
+ format = self.format
+ return "application/%s" % (format)
+
+ @api_call
+ def list_networks(self):
+ """Fetches a list of all networks for a tenant"""
+ return self.do_request("GET", self.networks_path)
+
+ @api_call
+ def show_network_details(self, network):
+ """Fetches the details of a certain network"""
+ return self.do_request("GET", self.network_path % (network))
+
+ @api_call
+ def create_network(self, body=None):
+ """Creates a new network"""
+ body = self.serialize(body)
+ return self.do_request("POST", self.networks_path, body=body)
+
+ @api_call
+ def update_network(self, network, body=None):
+ """Updates a network"""
+ body = self.serialize(body)
+ return self.do_request("PUT", self.network_path % (network), body=body)
+
+ @api_call
+ def delete_network(self, network):
+ """Deletes the specified network"""
+ return self.do_request("DELETE", self.network_path % (network))
+
+ @api_call
+ def list_ports(self, network):
+ """Fetches a list of ports on a given network"""
+ return self.do_request("GET", self.ports_path % (network))
+
+ @api_call
+ def show_port_details(self, network, port):
+ """Fetches the details of a certain port"""
+ return self.do_request("GET", self.port_path % (network, port))
+
+ @api_call
+ def create_port(self, network, body=None):
+ """Creates a new port on a given network"""
+ body = self.serialize(body)
+ return self.do_request("POST", self.ports_path % (network), body=body)
+
+ @api_call
+ def delete_port(self, network, port):
+ """Deletes the specified port from a network"""
+ return self.do_request("DELETE", self.port_path % (network, port))
+
+ @api_call
+ def set_port_state(self, network, port, body=None):
+ """Sets the state of the specified port"""
+ body = self.serialize(body)
+ return self.do_request("PUT",
+ self.port_path % (network, port), body=body)
+
+ @api_call
+ def show_port_attachment(self, network, port):
+ """Fetches the attachment-id associated with the specified port"""
+ return self.do_request("GET", self.attachment_path % (network, port))
+
+ @api_call
+ def attach_resource(self, network, port, body=None):
+ """Sets the attachment-id of the specified port"""
+ body = self.serialize(body)
+ return self.do_request("PUT",
+ self.attachment_path % (network, port), body=body)
+
+ @api_call
+ def detach_resource(self, network, port):
+ """Removes the attachment-id of the specified port"""
+ return self.do_request("DELETE",
+ self.attachment_path % (network, port))
diff --git a/nova/network/quantum/manager.py b/nova/network/quantum/manager.py
new file mode 100644
index 000000000..23a9aba0d
--- /dev/null
+++ b/nova/network/quantum/manager.py
@@ -0,0 +1,324 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Nicira Networks, Inc
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova import db
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import manager
+from nova.network import manager
+from nova.network.quantum import quantum_connection
+from nova import utils
+
+LOG = logging.getLogger("nova.network.quantum.manager")
+
+FLAGS = flags.FLAGS
+
+flags.DEFINE_string('quantum_ipam_lib',
+ 'nova.network.quantum.nova_ipam_lib',
+ "Indicates underlying IP address management library")
+
+
+class QuantumManager(manager.FlatManager):
+ """NetworkManager class that communicates with a Quantum service
+ via a web services API to provision VM network connectivity.
+
+ For IP Address management, QuantumManager can be configured to
+ use either Nova's local DB or the Melange IPAM service.
+
+ Currently, the QuantumManager does NOT support any of the 'gateway'
+ functionality implemented by the Nova VlanManager, including:
+ * floating IPs
+ * DHCP
+ * NAT gateway
+
+ Support for these capabilities are targted for future releases.
+ """
+
+ def __init__(self, q_conn=None, ipam_lib=None, *args, **kwargs):
+ """Initialize two key libraries, the connection to a
+ Quantum service, and the library for implementing IPAM.
+
+ Calls inherited FlatManager constructor.
+ """
+
+ if not q_conn:
+ q_conn = quantum_connection.QuantumClientConnection()
+ self.q_conn = q_conn
+
+ if not ipam_lib:
+ ipam_lib = FLAGS.quantum_ipam_lib
+ self.ipam = utils.import_object(ipam_lib).get_ipam_lib(self)
+
+ super(QuantumManager, self).__init__(*args, **kwargs)
+
+ def create_networks(self, context, label, cidr, multi_host, num_networks,
+ network_size, cidr_v6, gateway_v6, bridge,
+ bridge_interface, dns1=None, dns2=None, uuid=None,
+ **kwargs):
+ """Unlike other NetworkManagers, with QuantumManager, each
+ create_networks calls should create only a single network.
+
+ Two scenarios exist:
+ - no 'uuid' is specified, in which case we contact
+ Quantum and create a new network.
+ - an existing 'uuid' is specified, corresponding to
+ a Quantum network created out of band.
+
+ In both cases, we initialize a subnet using the IPAM lib.
+ """
+ if num_networks != 1:
+ raise Exception(_("QuantumManager requires that only one"
+ " network is created per call"))
+ q_tenant_id = kwargs["project_id"] or FLAGS.quantum_default_tenant_id
+ quantum_net_id = uuid
+ if quantum_net_id:
+ if not self.q_conn.network_exists(q_tenant_id, quantum_net_id):
+ raise Exception(_("Unable to find existing quantum " \
+ " network for tenant '%(q_tenant_id)s' with "
+ "net-id '%(quantum_net_id)s'" % locals()))
+ else:
+ # otherwise, create network from default quantum pool
+ quantum_net_id = self.q_conn.create_network(q_tenant_id, label)
+
+ ipam_tenant_id = kwargs.get("project_id", None)
+ priority = kwargs.get("priority", 0)
+ self.ipam.create_subnet(context, label, ipam_tenant_id, quantum_net_id,
+ priority, cidr, gateway_v6, cidr_v6, dns1, dns2)
+
+ def delete_network(self, context, fixed_range):
+ """Lookup network by IPv4 cidr, delete both the IPAM
+ subnet and the corresponding Quantum network.
+ """
+ project_id = context.project_id
+ quantum_net_id = self.ipam.get_network_id_by_cidr(
+ context, fixed_range, project_id)
+ self.ipam.delete_subnets_by_net_id(context, quantum_net_id,
+ project_id)
+ q_tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ self.q_conn.delete_network(q_tenant_id, quantum_net_id)
+
+ def allocate_for_instance(self, context, **kwargs):
+ """Called by compute when it is creating a new VM.
+
+ There are three key tasks:
+ - Determine the number and order of vNICs to create
+ - Allocate IP addresses
+ - Create ports on a Quantum network and attach vNICs.
+
+ We support two approaches to determining vNICs:
+ - By default, a VM gets a vNIC for any network belonging
+ to the VM's project, and a vNIC for any "global" network
+ that has a NULL project_id. vNIC order is determined
+ by the network's 'priority' field.
+ - If the 'os-create-server-ext' was used to create the VM,
+ only the networks in 'requested_networks' are used to
+ create vNICs, and the vNIC order is determiend by the
+ order in the requested_networks array.
+
+ For each vNIC, use the FlatManager to create the entries
+ in the virtual_interfaces table, contact Quantum to
+ create a port and attachment the vNIC, and use the IPAM
+ lib to allocate IP addresses.
+ """
+ instance_id = kwargs.pop('instance_id')
+ instance_type_id = kwargs['instance_type_id']
+ host = kwargs.pop('host')
+ project_id = kwargs.pop('project_id')
+ LOG.debug(_("network allocations for instance %s"), instance_id)
+
+ requested_networks = kwargs.get('requested_networks')
+
+ if requested_networks:
+ net_proj_pairs = [(net_id, project_id) \
+ for (net_id, _i) in requested_networks]
+ else:
+ net_proj_pairs = self.ipam.get_project_and_global_net_ids(context,
+ project_id)
+
+ # Create a port via quantum and attach the vif
+ for (quantum_net_id, project_id) in net_proj_pairs:
+
+ # FIXME(danwent): We'd like to have the manager be
+ # completely decoupled from the nova networks table.
+ # However, other parts of nova sometimes go behind our
+ # back and access network data directly from the DB. So
+ # for now, the quantum manager knows that there is a nova
+ # networks DB table and accesses it here. updating the
+ # virtual_interfaces table to use UUIDs would be one
+ # solution, but this would require significant work
+ # elsewhere.
+ admin_context = context.elevated()
+ network_ref = db.network_get_by_uuid(admin_context,
+ quantum_net_id)
+
+ vif_rec = manager.FlatManager.add_virtual_interface(self,
+ context, instance_id, network_ref['id'])
+
+ # talk to Quantum API to create and attach port.
+ q_tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ self.q_conn.create_and_attach_port(q_tenant_id, quantum_net_id,
+ vif_rec['uuid'])
+ self.ipam.allocate_fixed_ip(context, project_id, quantum_net_id,
+ vif_rec)
+
+ return self.get_instance_nw_info(context, instance_id,
+ instance_type_id, host)
+
+ def get_instance_nw_info(self, context, instance_id,
+ instance_type_id, host):
+ """This method is used by compute to fetch all network data
+ that should be used when creating the VM.
+
+ The method simply loops through all virtual interfaces
+ stored in the nova DB and queries the IPAM lib to get
+ the associated IP data.
+
+ The format of returned data is 'defined' by the initial
+ set of NetworkManagers found in nova/network/manager.py .
+ Ideally this 'interface' will be more formally defined
+ in the future.
+ """
+ network_info = []
+ instance = db.instance_get(context, instance_id)
+ project_id = instance.project_id
+
+ admin_context = context.elevated()
+ vifs = db.virtual_interface_get_by_instance(admin_context,
+ instance_id)
+ for vif in vifs:
+ q_tenant_id = project_id
+ ipam_tenant_id = project_id
+ net_id, port_id = self.q_conn.get_port_by_attachment(q_tenant_id,
+ vif['uuid'])
+ if not net_id:
+ q_tenant_id = FLAGS.quantum_default_tenant_id
+ ipam_tenant_id = None
+ net_id, port_id = self.q_conn.get_port_by_attachment(
+ q_tenant_id, vif['uuid'])
+ if not net_id:
+ # TODO(bgh): We need to figure out a way to tell if we
+ # should actually be raising this exception or not.
+ # In the case that a VM spawn failed it may not have
+ # attached the vif and raising the exception here
+ # prevents deletion of the VM. In that case we should
+ # probably just log, continue, and move on.
+ raise Exception(_("No network for for virtual interface %s") %
+ vif['uuid'])
+ (v4_subnet, v6_subnet) = self.ipam.get_subnets_by_net_id(context,
+ ipam_tenant_id, net_id)
+ v4_ips = self.ipam.get_v4_ips_by_interface(context,
+ net_id, vif['uuid'],
+ project_id=ipam_tenant_id)
+ v6_ips = self.ipam.get_v6_ips_by_interface(context,
+ net_id, vif['uuid'],
+ project_id=ipam_tenant_id)
+
+ quantum_net_id = v4_subnet['network_id'] or v6_subnet['network_id']
+
+ def ip_dict(ip, subnet):
+ return {
+ "ip": ip,
+ "netmask": subnet["netmask"],
+ "enabled": "1"}
+
+ network_dict = {
+ 'cidr': v4_subnet['cidr'],
+ 'injected': True,
+ 'multi_host': False}
+
+ info = {
+ 'gateway': v4_subnet['gateway'],
+ 'dhcp_server': v4_subnet['gateway'],
+ 'broadcast': v4_subnet['broadcast'],
+ 'mac': vif['address'],
+ 'vif_uuid': vif['uuid'],
+ 'dns': [],
+ 'ips': [ip_dict(ip, v4_subnet) for ip in v4_ips]}
+
+ if v6_subnet:
+ if v6_subnet['cidr']:
+ network_dict['cidr_v6'] = v6_subnet['cidr']
+ info['ip6s'] = [ip_dict(ip, v6_subnet) for ip in v6_ips]
+
+ if v6_subnet['gateway']:
+ info['gateway6'] = v6_subnet['gateway']
+
+ dns_dict = {}
+ for s in [v4_subnet, v6_subnet]:
+ for k in ['dns1', 'dns2']:
+ if s and s[k]:
+ dns_dict[s[k]] = None
+ info['dns'] = [d for d in dns_dict.keys()]
+
+ network_info.append((network_dict, info))
+ return network_info
+
+ def deallocate_for_instance(self, context, **kwargs):
+ """Called when a VM is terminated. Loop through each virtual
+ interface in the Nova DB and remove the Quantum port and
+ clear the IP allocation using the IPAM. Finally, remove
+ the virtual interfaces from the Nova DB.
+ """
+ instance_id = kwargs.get('instance_id')
+ project_id = kwargs.pop('project_id', None)
+
+ admin_context = context.elevated()
+ vifs = db.virtual_interface_get_by_instance(admin_context,
+ instance_id)
+ for vif_ref in vifs:
+ interface_id = vif_ref['uuid']
+ q_tenant_id = project_id
+ ipam_tenant_id = project_id
+ (net_id, port_id) = self.q_conn.get_port_by_attachment(q_tenant_id,
+ interface_id)
+ if not net_id:
+ q_tenant_id = FLAGS.quantum_default_tenant_id
+ ipam_tenant_id = None
+ (net_id, port_id) = self.q_conn.get_port_by_attachment(
+ q_tenant_id, interface_id)
+ if not net_id:
+ LOG.error("Unable to find port with attachment: %s" %
+ (interface_id))
+ continue
+ self.q_conn.detach_and_delete_port(q_tenant_id,
+ net_id, port_id)
+
+ self.ipam.deallocate_ips_by_vif(context, ipam_tenant_id,
+ net_id, vif_ref)
+
+ try:
+ db.virtual_interface_delete_by_instance(admin_context,
+ instance_id)
+ except exception.InstanceNotFound:
+ LOG.error(_("Attempted to deallocate non-existent instance: %s" %
+ (instance_id)))
+
+ def validate_networks(self, context, networks):
+ """Validates that this tenant has quantum networks with the associated
+ UUIDs. This is called by the 'os-create-server-ext' API extension
+ code so that we can return an API error code to the caller if they
+ request an invalid network.
+ """
+ if networks is None:
+ return
+
+ project_id = context.project_id
+ for (net_id, _i) in networks:
+ self.ipam.verify_subnet_exists(context, project_id, net_id)
+ if not self.q_conn.network_exists(project_id, net_id):
+ raise exception.NetworkNotFound(network_id=net_id)
diff --git a/nova/network/quantum/melange_connection.py b/nova/network/quantum/melange_connection.py
new file mode 100644
index 000000000..71ac9b5f1
--- /dev/null
+++ b/nova/network/quantum/melange_connection.py
@@ -0,0 +1,141 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import httplib
+import socket
+import urllib
+import json
+
+from nova import flags
+
+
+FLAGS = flags.FLAGS
+
+flags.DEFINE_string('melange_host',
+ '127.0.0.1',
+ 'HOST for connecting to melange')
+
+flags.DEFINE_string('melange_port',
+ '9898',
+ 'PORT for connecting to melange')
+
+json_content_type = {'Content-type': "application/json"}
+
+
+# FIXME(danwent): talk to the Melange folks about creating a
+# client lib that we can import as a library, instead of
+# have to have all of the client code in here.
+class MelangeConnection(object):
+
+ def __init__(self, host=None, port=None, use_ssl=False):
+ if host is None:
+ host = FLAGS.melange_host
+ if port is None:
+ port = int(FLAGS.melange_port)
+ self.host = host
+ self.port = port
+ self.use_ssl = use_ssl
+ self.version = "v0.1"
+
+ def get(self, path, params=None, headers=None):
+ return self.do_request("GET", path, params=params, headers=headers)
+
+ def post(self, path, body=None, headers=None):
+ return self.do_request("POST", path, body=body, headers=headers)
+
+ def delete(self, path, headers=None):
+ return self.do_request("DELETE", path, headers=headers)
+
+ def _get_connection(self):
+ if self.use_ssl:
+ return httplib.HTTPSConnection(self.host, self.port)
+ else:
+ return httplib.HTTPConnection(self.host, self.port)
+
+ def do_request(self, method, path, body=None, headers=None, params=None):
+ headers = headers or {}
+ params = params or {}
+
+ url = "/%s/%s.json" % (self.version, path)
+ if params:
+ url += "?%s" % urllib.urlencode(params)
+ try:
+ connection = self._get_connection()
+ connection.request(method, url, body, headers)
+ response = connection.getresponse()
+ response_str = response.read()
+ if response.status < 400:
+ return response_str
+ raise Exception(_("Server returned error: %s" % response_str))
+ except (socket.error, IOError), e:
+ raise Exception(_("Unable to connect to "
+ "server. Got error: %s" % e))
+
+ def allocate_ip(self, network_id, vif_id,
+ project_id=None, mac_address=None):
+ tenant_scope = "/tenants/%s" % project_id if project_id else ""
+ request_body = (json.dumps(dict(network=dict(mac_address=mac_address,
+ tenant_id=project_id)))
+ if mac_address else None)
+ url = ("ipam%(tenant_scope)s/networks/%(network_id)s/"
+ "interfaces/%(vif_id)s/ip_allocations" % locals())
+ response = self.post(url, body=request_body,
+ headers=json_content_type)
+ return json.loads(response)['ip_addresses']
+
+ def create_block(self, network_id, cidr,
+ project_id=None, dns1=None, dns2=None):
+ tenant_scope = "/tenants/%s" % project_id if project_id else ""
+
+ url = "ipam%(tenant_scope)s/ip_blocks" % locals()
+
+ req_params = dict(ip_block=dict(cidr=cidr, network_id=network_id,
+ type='private', dns1=dns1, dns2=dns2))
+ self.post(url, body=json.dumps(req_params),
+ headers=json_content_type)
+
+ def delete_block(self, block_id, project_id=None):
+ tenant_scope = "/tenants/%s" % project_id if project_id else ""
+
+ url = "ipam%(tenant_scope)s/ip_blocks/%(block_id)s" % locals()
+
+ self.delete(url, headers=json_content_type)
+
+ def get_blocks(self, project_id=None):
+ tenant_scope = "/tenants/%s" % project_id if project_id else ""
+
+ url = "ipam%(tenant_scope)s/ip_blocks" % locals()
+
+ response = self.get(url, headers=json_content_type)
+ return json.loads(response)
+
+ def get_allocated_ips(self, network_id, vif_id, project_id=None):
+ tenant_scope = "/tenants/%s" % project_id if project_id else ""
+
+ url = ("ipam%(tenant_scope)s/networks/%(network_id)s/"
+ "interfaces/%(vif_id)s/ip_allocations" % locals())
+
+ response = self.get(url, headers=json_content_type)
+ return json.loads(response)['ip_addresses']
+
+ def deallocate_ips(self, network_id, vif_id, project_id=None):
+ tenant_scope = "/tenants/%s" % project_id if project_id else ""
+
+ url = ("ipam%(tenant_scope)s/networks/%(network_id)s/"
+ "interfaces/%(vif_id)s/ip_allocations" % locals())
+
+ self.delete(url, headers=json_content_type)
diff --git a/nova/network/quantum/melange_ipam_lib.py b/nova/network/quantum/melange_ipam_lib.py
new file mode 100644
index 000000000..a0ac10fd3
--- /dev/null
+++ b/nova/network/quantum/melange_ipam_lib.py
@@ -0,0 +1,205 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Nicira Networks, Inc
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from netaddr import IPNetwork
+
+from nova import db
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova.network.quantum import melange_connection
+
+
+LOG = logging.getLogger("nova.network.quantum.melange_ipam_lib")
+
+FLAGS = flags.FLAGS
+
+
+def get_ipam_lib(net_man):
+ return QuantumMelangeIPAMLib()
+
+
+class QuantumMelangeIPAMLib(object):
+ """Implements Quantum IP Address Management (IPAM) interface
+ using the Melange service, which is access using the Melange
+ web services API.
+ """
+
+ def __init__(self):
+ """Initialize class used to connect to Melange server"""
+ self.m_conn = melange_connection.MelangeConnection()
+
+ def create_subnet(self, context, label, project_id,
+ quantum_net_id, priority, cidr=None,
+ gateway_v6=None, cidr_v6=None,
+ dns1=None, dns2=None):
+ """Contact Melange and create a subnet for any non-NULL
+ IPv4 or IPv6 subnets.
+
+ Also create a entry in the Nova networks DB, but only
+ to store values not represented in Melange or to
+ temporarily provide compatibility with Nova code that
+ accesses IPAM data directly via the DB (e.g., nova-api)
+ """
+ tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ if cidr:
+ self.m_conn.create_block(quantum_net_id, cidr,
+ project_id=tenant_id,
+ dns1=dns1, dns2=dns2)
+ if cidr_v6:
+ self.m_conn.create_block(quantum_net_id, cidr_v6,
+ project_id=tenant_id,
+ dns1=dns1, dns2=dns2)
+
+ net = {"uuid": quantum_net_id,
+ "project_id": project_id,
+ "priority": priority,
+ "label": label}
+ admin_context = context.elevated()
+ network = db.network_create_safe(admin_context, net)
+
+ def allocate_fixed_ip(self, context, project_id, quantum_net_id, vif_ref):
+ """Pass call to allocate fixed IP on to Melange"""
+ tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ self.m_conn.allocate_ip(quantum_net_id,
+ vif_ref['uuid'], project_id=tenant_id,
+ mac_address=vif_ref['address'])
+
+ def get_network_id_by_cidr(self, context, cidr, project_id):
+ """Find the Quantum UUID associated with a IPv4 CIDR
+ address for the specified tenant.
+ """
+ tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ all_blocks = self.m_conn.get_blocks(tenant_id)
+ for b in all_blocks['ip_blocks']:
+ if b['cidr'] == cidr:
+ return b['network_id']
+ raise exception.NotFound(_("No network found for cidr %s" % cidr))
+
+ def delete_subnets_by_net_id(self, context, net_id, project_id):
+ """Find Melange block associated with the Quantum UUID,
+ then tell Melange to delete that block.
+ """
+ admin_context = context.elevated()
+ tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ all_blocks = self.m_conn.get_blocks(tenant_id)
+ for b in all_blocks['ip_blocks']:
+ if b['network_id'] == net_id:
+ self.m_conn.delete_block(b['id'], tenant_id)
+
+ network = db.network_get_by_uuid(admin_context, net_id)
+ db.network_delete_safe(context, network['id'])
+
+ def get_project_and_global_net_ids(self, context, project_id):
+ """Fetches all networks associated with this project, or
+ that are "global" (i.e., have no project set).
+ Returns list sorted by 'priority' (lowest integer value
+ is highest priority).
+ """
+ if project_id is None:
+ raise Exception(_("get_project_and_global_net_ids must be called"
+ " with a non-null project_id"))
+
+ admin_context = context.elevated()
+
+ # Decorate with priority
+ priority_nets = []
+ for tenant_id in (project_id, FLAGS.quantum_default_tenant_id):
+ blocks = self.m_conn.get_blocks(tenant_id)
+ for ip_block in blocks['ip_blocks']:
+ network_id = ip_block['network_id']
+ network = db.network_get_by_uuid(admin_context, network_id)
+ if network:
+ priority = network['priority']
+ priority_nets.append((priority, network_id, tenant_id))
+
+ # Sort by priority
+ priority_nets.sort()
+
+ # Undecorate
+ return [(network_id, tenant_id)
+ for priority, network_id, tenant_id in priority_nets]
+
+ def get_subnets_by_net_id(self, context, project_id, net_id):
+ """Returns information about the IPv4 and IPv6 subnets
+ associated with a Quantum Network UUID.
+ """
+
+ # FIXME(danwent): Melange actually returns the subnet info
+ # when we query for a particular interface. We may want to
+ # rework the ipam_manager python API to let us take advantage of
+ # this, as right now we have to get all blocks and cycle through
+ # them.
+ subnet_v4 = None
+ subnet_v6 = None
+ tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ all_blocks = self.m_conn.get_blocks(tenant_id)
+ for b in all_blocks['ip_blocks']:
+ if b['network_id'] == net_id:
+ subnet = {'network_id': b['network_id'],
+ 'cidr': b['cidr'],
+ 'gateway': b['gateway'],
+ 'broadcast': b['broadcast'],
+ 'netmask': b['netmask'],
+ 'dns1': b['dns1'],
+ 'dns2': b['dns2']}
+
+ if IPNetwork(b['cidr']).version == 6:
+ subnet_v6 = subnet
+ else:
+ subnet_v4 = subnet
+ return (subnet_v4, subnet_v6)
+
+ def get_v4_ips_by_interface(self, context, net_id, vif_id, project_id):
+ """Returns a list of IPv4 address strings associated with
+ the specified virtual interface.
+ """
+ return self._get_ips_by_interface(context, net_id, vif_id,
+ project_id, 4)
+
+ def get_v6_ips_by_interface(self, context, net_id, vif_id, project_id):
+ """Returns a list of IPv6 address strings associated with
+ the specified virtual interface.
+ """
+ return self._get_ips_by_interface(context, net_id, vif_id,
+ project_id, 6)
+
+ def _get_ips_by_interface(self, context, net_id, vif_id, project_id,
+ ip_version):
+ """Helper method to fetch v4 or v6 addresses for a particular
+ virtual interface.
+ """
+ tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ ip_list = self.m_conn.get_allocated_ips(net_id, vif_id, tenant_id)
+ return [ip['address'] for ip in ip_list
+ if IPNetwork(ip['address']).version == ip_version]
+
+ def verify_subnet_exists(self, context, project_id, quantum_net_id):
+ """Confirms that a subnet exists that is associated with the
+ specified Quantum Network UUID.
+ """
+ tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ v4_subnet, v6_subnet = self.get_subnets_by_net_id(context, tenant_id,
+ quantum_net_id)
+ return v4_subnet is not None
+
+ def deallocate_ips_by_vif(self, context, project_id, net_id, vif_ref):
+ """Deallocate all fixed IPs associated with the specified
+ virtual interface.
+ """
+ tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ self.m_conn.deallocate_ips(net_id, vif_ref['uuid'], tenant_id)
diff --git a/nova/network/quantum/nova_ipam_lib.py b/nova/network/quantum/nova_ipam_lib.py
new file mode 100644
index 000000000..21dee8f6a
--- /dev/null
+++ b/nova/network/quantum/nova_ipam_lib.py
@@ -0,0 +1,195 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Nicira Networks, Inc
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import netaddr
+
+from nova import db
+from nova import exception
+from nova import flags
+from nova import ipv6
+from nova import log as logging
+from nova.network import manager
+from nova.network.quantum import melange_connection as melange
+from nova import utils
+
+
+LOG = logging.getLogger("nova.network.quantum.nova_ipam_lib")
+
+FLAGS = flags.FLAGS
+
+
+def get_ipam_lib(net_man):
+ return QuantumNovaIPAMLib(net_man)
+
+
+class QuantumNovaIPAMLib(object):
+ """Implements Quantum IP Address Management (IPAM) interface
+ using the local Nova database. This implementation is inline
+ with how IPAM is used by other NetworkManagers.
+ """
+
+ def __init__(self, net_manager):
+ """Holds a reference to the "parent" network manager, used
+ to take advantage of various FlatManager methods to avoid
+ code duplication.
+ """
+ self.net_manager = net_manager
+
+ def create_subnet(self, context, label, tenant_id,
+ quantum_net_id, priority, cidr=None,
+ gateway_v6=None, cidr_v6=None,
+ dns1=None, dns2=None):
+ """Re-use the basic FlatManager create_networks method to
+ initialize the networks and fixed_ips tables in Nova DB.
+
+ Also stores a few more fields in the networks table that
+ are needed by Quantum but not the FlatManager.
+ """
+ admin_context = context.elevated()
+ subnet_size = len(netaddr.IPNetwork(cidr))
+ networks = manager.FlatManager.create_networks(self.net_manager,
+ admin_context, label, cidr,
+ False, 1, subnet_size, cidr_v6,
+ gateway_v6, quantum_net_id, None, dns1, dns2)
+
+ if len(networks) != 1:
+ raise Exception(_("Error creating network entry"))
+
+ network = networks[0]
+ net = {"project_id": tenant_id,
+ "priority": priority,
+ "uuid": quantum_net_id}
+ db.network_update(admin_context, network['id'], net)
+
+ def get_network_id_by_cidr(self, context, cidr, project_id):
+ """ Grabs Quantum network UUID based on IPv4 CIDR. """
+ admin_context = context.elevated()
+ network = db.network_get_by_cidr(admin_context, cidr)
+ if not network:
+ raise Exception(_("No network with fixed_range = %s" %
+ fixed_range))
+ return network['uuid']
+
+ def delete_subnets_by_net_id(self, context, net_id, project_id):
+ """Deletes a network based on Quantum UUID. Uses FlatManager
+ delete_network to avoid duplication.
+ """
+ admin_context = context.elevated()
+ network = db.network_get_by_uuid(admin_context, net_id)
+ if not network:
+ raise Exception(_("No network with net_id = %s" % net_id))
+ manager.FlatManager.delete_network(self.net_manager,
+ admin_context, network['cidr'],
+ require_disassociated=False)
+
+ def get_project_and_global_net_ids(self, context, project_id):
+ """Fetches all networks associated with this project, or
+ that are "global" (i.e., have no project set).
+ Returns list sorted by 'priority'.
+ """
+ admin_context = context.elevated()
+ networks = db.project_get_networks(admin_context, project_id, False)
+ networks.extend(db.project_get_networks(admin_context, None, False))
+ id_priority_map = {}
+ net_list = []
+ for n in networks:
+ net_id = n['uuid']
+ net_list.append((net_id, n["project_id"]))
+ id_priority_map[net_id] = n['priority']
+ return sorted(net_list, key=lambda x: id_priority_map[x[0]])
+
+ def allocate_fixed_ip(self, context, tenant_id, quantum_net_id, vif_rec):
+ """Allocates a single fixed IPv4 address for a virtual interface."""
+ admin_context = context.elevated()
+ network = db.network_get_by_uuid(admin_context, quantum_net_id)
+ if network['cidr']:
+ address = db.fixed_ip_associate_pool(admin_context,
+ network['id'],
+ vif_rec['instance_id'])
+ values = {'allocated': True,
+ 'virtual_interface_id': vif_rec['id']}
+ db.fixed_ip_update(admin_context, address, values)
+
+ def get_subnets_by_net_id(self, context, tenant_id, net_id):
+ """Returns information about the IPv4 and IPv6 subnets
+ associated with a Quantum Network UUID.
+ """
+ n = db.network_get_by_uuid(context.elevated(), net_id)
+ subnet_data_v4 = {
+ 'network_id': n['uuid'],
+ 'cidr': n['cidr'],
+ 'gateway': n['gateway'],
+ 'broadcast': n['broadcast'],
+ 'netmask': n['netmask'],
+ 'dns1': n['dns1'],
+ 'dns2': n['dns2']}
+ subnet_data_v6 = {
+ 'network_id': n['uuid'],
+ 'cidr': n['cidr_v6'],
+ 'gateway': n['gateway_v6'],
+ 'broadcast': None,
+ 'netmask': None,
+ 'dns1': None,
+ 'dns2': None}
+ return (subnet_data_v4, subnet_data_v6)
+
+ def get_v4_ips_by_interface(self, context, net_id, vif_id, project_id):
+ """Returns a list of IPv4 address strings associated with
+ the specified virtual interface, based on the fixed_ips table.
+ """
+ vif_rec = db.virtual_interface_get_by_uuid(context, vif_id)
+ fixed_ips = db.fixed_ip_get_by_virtual_interface(context,
+ vif_rec['id'])
+ return [fixed_ip['address'] for fixed_ip in fixed_ips]
+
+ def get_v6_ips_by_interface(self, context, net_id, vif_id, project_id):
+ """Returns a list containing a single IPv6 address strings
+ associated with the specified virtual interface.
+ """
+ admin_context = context.elevated()
+ network = db.network_get_by_uuid(admin_context, net_id)
+ vif_rec = db.virtual_interface_get_by_uuid(context, vif_id)
+ if network['cidr_v6']:
+ ip = ipv6.to_global(network['cidr_v6'],
+ vif_rec['address'],
+ project_id)
+ return [ip]
+ return []
+
+ def verify_subnet_exists(self, context, tenant_id, quantum_net_id):
+ """Confirms that a subnet exists that is associated with the
+ specified Quantum Network UUID. Raises an exception if no
+ such subnet exists.
+ """
+ admin_context = context.elevated()
+ db.network_get_by_uuid(admin_context, quantum_net_id)
+
+ def deallocate_ips_by_vif(self, context, tenant_id, net_id, vif_ref):
+ """Deallocate all fixed IPs associated with the specified
+ virtual interface.
+ """
+ try:
+ admin_context = context.elevated()
+ fixed_ips = db.fixed_ip_get_by_virtual_interface(admin_context,
+ vif_ref['id'])
+ for fixed_ip in fixed_ips:
+ db.fixed_ip_update(admin_context, fixed_ip['address'],
+ {'allocated': False,
+ 'virtual_interface_id': None})
+ except exception.FixedIpNotFoundForInstance:
+ LOG.error(_('No fixed IPs to deallocate for vif %s' %
+ vif_ref['id']))
diff --git a/nova/network/quantum/quantum_connection.py b/nova/network/quantum/quantum_connection.py
new file mode 100644
index 000000000..21917653c
--- /dev/null
+++ b/nova/network/quantum/quantum_connection.py
@@ -0,0 +1,118 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Nicira Networks
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova import flags
+from nova import log as logging
+from nova.network.quantum import client as quantum_client
+from nova import utils
+
+
+LOG = logging.getLogger("nova.network.quantum.quantum_connection")
+FLAGS = flags.FLAGS
+
+flags.DEFINE_string('quantum_connection_host',
+ '127.0.0.1',
+ 'HOST for connecting to quantum')
+
+flags.DEFINE_string('quantum_connection_port',
+ '9696',
+ 'PORT for connecting to quantum')
+
+flags.DEFINE_string('quantum_default_tenant_id',
+ "default",
+ 'Default tenant id when creating quantum networks')
+
+
+class QuantumClientConnection(object):
+ """Abstracts connection to Quantum service into higher level
+ operations performed by the QuantumManager.
+
+ Separating this out as a class also let's us create a 'fake'
+ version of this class for unit tests.
+ """
+
+ def __init__(self):
+ """Initialize Quantum client class based on flags."""
+ self.client = quantum_client.Client(FLAGS.quantum_connection_host,
+ FLAGS.quantum_connection_port,
+ format="json",
+ logger=LOG)
+
+ def create_network(self, tenant_id, network_name):
+ """Create network using specified name, return Quantum
+ network UUID.
+ """
+ data = {'network': {'name': network_name}}
+ resdict = self.client.create_network(data, tenant=tenant_id)
+ return resdict["network"]["id"]
+
+ def delete_network(self, tenant_id, net_id):
+ """Deletes Quantum network with specified UUID."""
+ self.client.delete_network(net_id, tenant=tenant_id)
+
+ def network_exists(self, tenant_id, net_id):
+ """Determine if a Quantum network exists for the
+ specified tenant.
+ """
+ try:
+ self.client.show_network_details(net_id, tenant=tenant_id)
+ return True
+ except client.QuantumNotFoundException:
+ # Not really an error. Real errors will be propogated to caller
+ return False
+
+ def create_and_attach_port(self, tenant_id, net_id, interface_id):
+ """Creates a Quantum port on the specified network, sets
+ status to ACTIVE to enable traffic, and attaches the
+ vNIC with the specified interface-id.
+ """
+ LOG.debug(_("Connecting interface %(interface_id)s to "
+ "net %(net_id)s for %(tenant_id)s" % locals()))
+ port_data = {'port': {'state': 'ACTIVE'}}
+ resdict = self.client.create_port(net_id, port_data, tenant=tenant_id)
+ port_id = resdict["port"]["id"]
+
+ attach_data = {'attachment': {'id': interface_id}}
+ self.client.attach_resource(net_id, port_id, attach_data,
+ tenant=tenant_id)
+
+ def detach_and_delete_port(self, tenant_id, net_id, port_id):
+ """Detach and delete the specified Quantum port."""
+ LOG.debug(_("Deleting port %(port_id)s on net %(net_id)s"
+ " for %(tenant_id)s" % locals()))
+
+ self.client.detach_resource(net_id, port_id, tenant=tenant_id)
+ self.client.delete_port(net_id, port_id, tenant=tenant_id)
+
+ def get_port_by_attachment(self, tenant_id, attachment_id):
+ """Given a tenant, search for the Quantum network and port
+ UUID that has the specified interface-id attachment.
+ """
+ # FIXME(danwent): this will be inefficient until the Quantum
+ # API implements querying a port by the interface-id
+ net_list_resdict = self.client.list_networks(tenant=tenant_id)
+ for n in net_list_resdict["networks"]:
+ net_id = n['id']
+ port_list_resdict = self.client.list_ports(net_id,
+ tenant=tenant_id)
+ for p in port_list_resdict["ports"]:
+ port_id = p["id"]
+ port_get_resdict = self.client.show_port_attachment(net_id,
+ port_id, tenant=tenant_id)
+ if attachment_id == port_get_resdict["attachment"]["id"]:
+ return (net_id, port_id)
+ return (None, None)
diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py
index 826a99b0a..9f7d34ea7 100644
--- a/nova/scheduler/host_filter.py
+++ b/nova/scheduler/host_filter.py
@@ -32,6 +32,12 @@ from nova import exception
from nova import flags
import nova.scheduler
+# NOTE(Vek): Even though we don't use filters in here anywhere, we
+# depend on default_host_filter being available in FLAGS,
+# and that happens only when filters/abstract_filter.py is
+# imported.
+from nova.scheduler import filters
+
FLAGS = flags.FLAGS
diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py
index 0e395ee79..bf18abc6c 100644
--- a/nova/scheduler/manager.py
+++ b/nova/scheduler/manager.py
@@ -93,12 +93,14 @@ class SchedulerManager(manager.Manager):
driver_method = 'schedule_%s' % method
elevated = context.elevated()
try:
- host = getattr(self.driver, driver_method)(elevated, *args,
- **kwargs)
+ real_meth = getattr(self.driver, driver_method)
+ args = (elevated,) + args
except AttributeError, e:
LOG.warning(_("Driver Method %(driver_method)s missing: %(e)s."
- "Reverting to schedule()") % locals())
- host = self.driver.schedule(elevated, topic, *args, **kwargs)
+ "Reverting to schedule()") % locals())
+ real_meth = self.driver.schedule
+ args = (elevated, topic) + args
+ host = real_meth(*args, **kwargs)
if not host:
LOG.debug(_("%(topic)s %(method)s handled in Scheduler")
diff --git a/nova/tests/api/openstack/common.py b/nova/tests/api/openstack/common.py
index 74bb8729a..19515ca67 100644
--- a/nova/tests/api/openstack/common.py
+++ b/nova/tests/api/openstack/common.py
@@ -34,3 +34,25 @@ def webob_factory(url):
req.body = json.dumps(body)
return req
return web_request
+
+
+def compare_links(actual, expected):
+ """Compare xml atom links."""
+
+ return compare_tree_to_dict(actual, expected, ('rel', 'href', 'type'))
+
+
+def compare_media_types(actual, expected):
+ """Compare xml media types."""
+
+ return compare_tree_to_dict(actual, expected, ('base', 'type'))
+
+
+def compare_tree_to_dict(actual, expected, keys):
+ """Compare parts of lxml.etree objects to dicts."""
+
+ for elem, data in zip(actual, expected):
+ for key in keys:
+ if elem.get(key) != data.get(key):
+ return False
+ return True
diff --git a/nova/tests/api/openstack/contrib/test_createserverext.py b/nova/tests/api/openstack/contrib/test_createserverext.py
index 089c8e59d..078b72d67 100644
--- a/nova/tests/api/openstack/contrib/test_createserverext.py
+++ b/nova/tests/api/openstack/contrib/test_createserverext.py
@@ -16,6 +16,7 @@
# under the License.
import base64
+import datetime
import json
import unittest
from xml.dom import minidom
@@ -27,15 +28,7 @@ from nova import db
from nova import exception
from nova import flags
from nova import test
-from nova import utils
import nova.api.openstack
-from nova.api.openstack import servers
-from nova.api.openstack.contrib import createserverext
-import nova.compute.api
-
-import nova.scheduler.api
-import nova.image.fake
-import nova.rpc
from nova.tests.api.openstack import fakes
@@ -52,22 +45,45 @@ DUPLICATE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'),
INVALID_NETWORKS = [('invalid', 'invalid-ip-address')]
+INSTANCE = {
+ "id": 1,
+ "display_name": "test_server",
+ "uuid": FAKE_UUID,
+ "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"}]
+ }
+
+
+def return_server_by_id(context, id, session=None):
+ INSTANCE['id'] = id
+ return INSTANCE
+
+
+def return_security_group_non_existing(context, project_id, group_name):
+ raise exception.SecurityGroupNotFoundForProject(project_id=project_id,
+ security_group_id=group_name)
+
+
+def return_security_group_get_by_name(context, project_id, group_name):
+ return {'id': 1, 'name': group_name}
+
+
+def return_security_group_get(context, security_group_id, session):
+ return {'id': security_group_id}
+
+
+def return_instance_add_security_group(context, instance_id,
+ security_group_id):
+ pass
+
class CreateserverextTest(test.TestCase):
def setUp(self):
super(CreateserverextTest, self).setUp()
- self.stubs = stubout.StubOutForTesting()
- fakes.FakeAuthManager.auth_data = {}
- fakes.FakeAuthDatabase.data = {}
- fakes.stub_out_auth(self.stubs)
- fakes.stub_out_image_service(self.stubs)
- fakes.stub_out_key_pair_funcs(self.stubs)
- self.allow_admin = FLAGS.allow_admin_api
def tearDown(self):
- self.stubs.UnsetAll()
- FLAGS.allow_admin_api = self.allow_admin
super(CreateserverextTest, self).tearDown()
def _setup_mock_compute_api(self):
@@ -96,6 +112,8 @@ class CreateserverextTest(test.TestCase):
return [{'id': '1234', 'display_name': 'fakeinstance',
'uuid': FAKE_UUID,
+ 'user_id': 'fake',
+ 'project_id': 'fake',
'created_at': "",
'updated_at': ""}]
@@ -114,6 +132,18 @@ class CreateserverextTest(test.TestCase):
'_get_kernel_ramdisk_from_image', make_stub_method((1, 1)))
return compute_api
+ def _create_security_group_request_dict(self, security_groups):
+ server = {}
+ server['name'] = 'new-server-test'
+ server['imageRef'] = 1
+ server['flavorRef'] = 1
+ if security_groups is not None:
+ sg_list = []
+ for name in security_groups:
+ sg_list.append({'name': name})
+ server['security_groups'] = sg_list
+ return {'server': server}
+
def _create_networks_request_dict(self, networks):
server = {}
server['name'] = 'new-server-test'
@@ -348,3 +378,38 @@ class CreateserverextTest(test.TestCase):
self._create_instance_with_user_data_json(user_data_contents)
self.assertEquals(response.status_int, 400)
self.assertEquals(user_data, None)
+
+ def test_create_instance_with_security_group_json(self):
+ security_groups = ['test', 'test1']
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
+ return_security_group_get_by_name)
+ self.stubs.Set(nova.db.api, 'instance_add_security_group',
+ return_instance_add_security_group)
+ body_dict = self._create_security_group_request_dict(security_groups)
+ request = self._get_create_request_json(body_dict)
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEquals(response.status_int, 202)
+
+ def test_get_server_by_id_verify_security_groups_json(self):
+ self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
+ req = webob.Request.blank('/v1.1/123/os-create-server-ext/1')
+ req.headers['Content-Type'] = 'application/json'
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEquals(response.status_int, 200)
+ res_dict = json.loads(response.body)
+ expected_security_group = [{"name": "test"}]
+ self.assertEquals(res_dict['server']['security_groups'],
+ expected_security_group)
+
+ def test_get_server_by_id_verify_security_groups_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
+ req = webob.Request.blank('/v1.1/123/os-create-server-ext/1')
+ req.headers['Accept'] = 'application/xml'
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEquals(response.status_int, 200)
+ dom = minidom.parseString(response.body)
+ server = dom.childNodes[0]
+ sec_groups = server.getElementsByTagName('security_groups')[0]
+ sec_group = sec_groups.getElementsByTagName('security_group')[0]
+ self.assertEqual(INSTANCE['security_groups'][0]['name'],
+ sec_group.getAttribute("name"))
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 00640c094..c3203eb43 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -52,6 +52,10 @@ from nova.tests.api.openstack import fakes
FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
NS = "{http://docs.openstack.org/compute/api/v1.1}"
ATOMNS = "{http://www.w3.org/2005/Atom}"
+XPATH_NS = {
+ 'atom': 'http://www.w3.org/2005/Atom',
+ 'ns': 'http://docs.openstack.org/compute/api/v1.1'
+}
def fake_gen_uuid():
@@ -347,6 +351,8 @@ class ServersTest(test.TestCase):
"server": {
"id": 1,
"uuid": FAKE_UUID,
+ "user_id": "fake",
+ "tenant_id": "fake",
"updated": "2010-11-11T11:00:00Z",
"created": "2010-10-10T12:00:00Z",
"progress": 0,
@@ -410,12 +416,7 @@ class ServersTest(test.TestCase):
def test_get_server_by_id_v1_1_xml(self):
image_bookmark = "http://localhost/fake/images/10"
- flavor_ref = "http://localhost/v1.1/fake/flavors/1"
- flavor_id = "1"
flavor_bookmark = "http://localhost/fake/flavors/1"
- server_href = "http://localhost/v1.1/fake/servers/1"
- server_bookmark = "http://localhost/fake/servers/1"
-
public_ip = '192.168.0.3'
private_ip = '172.19.0.1'
interfaces = [
@@ -444,6 +445,83 @@ class ServersTest(test.TestCase):
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"
flavor_ref = "http://localhost/v1.1/fake/flavors/1"
@@ -477,6 +555,8 @@ class ServersTest(test.TestCase):
"server": {
"id": 1,
"uuid": FAKE_UUID,
+ "user_id": "fake",
+ "tenant_id": "fake",
"updated": "2010-11-11T11:00:00Z",
"created": "2010-10-10T12:00:00Z",
"progress": 100,
@@ -572,6 +652,8 @@ class ServersTest(test.TestCase):
"server": {
"id": 1,
"uuid": FAKE_UUID,
+ "user_id": "fake",
+ "tenant_id": "fake",
"updated": "2010-11-11T11:00:00Z",
"created": "2010-10-10T12:00:00Z",
"progress": 100,
@@ -1161,6 +1243,26 @@ class ServersTest(test.TestCase):
self.assertEqual(len(servers), 1)
self.assertEqual(servers[0]['id'], 100)
+ def test_tenant_id_filter_converts_to_project_id_for_admin(self):
+ def fake_get_all(context, filters=None):
+ self.assertNotEqual(filters, None)
+ self.assertEqual(filters['project_id'], 'faketenant')
+ self.assertFalse(filters.get('tenant_id'))
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.db.api, 'instance_get_all_by_filters',
+ fake_get_all)
+ self.flags(allow_admin_api=True)
+
+ req = webob.Request.blank('/v1.1/fake/servers?tenant_id=faketenant')
+ # Use admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ res_dict = json.loads(res.body)
+ # Failure in fake_get_all returns non 200 status code
+ self.assertEqual(res.status_int, 200)
+
def test_get_servers_allows_flavor_v1_1(self):
def fake_get_all(compute_self, context, search_opts=None):
self.assertNotEqual(search_opts, None)
@@ -1417,6 +1519,8 @@ class ServersTest(test.TestCase):
'access_ip_v4': '1.2.3.4',
'access_ip_v6': 'fead::1234',
'image_ref': image_ref,
+ 'user_id': 'fake',
+ 'project_id': 'fake',
"created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
"updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
"config_drive": self.config_drive,
@@ -3065,7 +3169,7 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "1",
- "networks": []
+ "networks": [],
}}
self.assertEquals(request['body'], expected)
@@ -3304,6 +3408,8 @@ class TestServerInstanceCreation(test.TestCase):
self.injected_files = None
return [{'id': '1234', 'display_name': 'fakeinstance',
+ 'user_id': 'fake',
+ 'project_id': 'fake',
'uuid': FAKE_UUID}]
def set_admin_password(self, *args, **kwargs):
@@ -3557,10 +3663,14 @@ class TestGetKernelRamdiskFromImage(test.TestCase):
self.assertRaises(exception.NotFound, self._get_k_r, image_meta)
def test_ami_no_ramdisk(self):
- """If an ami is missing a ramdisk it should raise NotFound"""
+ """If an ami is missing a ramdisk, return kernel ID and None for
+ ramdisk ID
+ """
image_meta = {'id': 1, 'status': 'active', 'container_format': 'ami',
'properties': {'kernel_id': 1}}
- self.assertRaises(exception.NotFound, self._get_k_r, image_meta)
+ kernel_id, ramdisk_id = self._get_k_r(image_meta)
+ self.assertEqual(kernel_id, 1)
+ self.assertEqual(ramdisk_id, None)
def test_ami_kernel_ramdisk_present(self):
"""Return IDs if both kernel and ramdisk are present"""
@@ -3595,8 +3705,8 @@ class ServersViewBuilderV11Test(test.TestCase):
"created_at": created_at,
"updated_at": updated_at,
"admin_pass": "",
- "user_id": "",
- "project_id": "",
+ "user_id": "fake",
+ "project_id": "fake",
"image_ref": "5",
"kernel_id": "",
"ramdisk_id": "",
@@ -3621,7 +3731,6 @@ class ServersViewBuilderV11Test(test.TestCase):
"terminated_at": utils.utcnow(),
"availability_zone": "",
"display_name": "test_server",
- "display_description": "",
"locked": False,
"metadata": [],
"accessIPv4": "1.2.3.4",
@@ -3704,6 +3813,8 @@ class ServersViewBuilderV11Test(test.TestCase):
"server": {
"id": 1,
"uuid": self.instance['uuid'],
+ "user_id": "fake",
+ "tenant_id": "fake",
"updated": "2010-11-11T11:00:00Z",
"created": "2010-10-10T12:00:00Z",
"progress": 0,
@@ -3759,6 +3870,8 @@ class ServersViewBuilderV11Test(test.TestCase):
"server": {
"id": 1,
"uuid": self.instance['uuid'],
+ "user_id": "fake",
+ "tenant_id": "fake",
"updated": "2010-11-11T11:00:00Z",
"created": "2010-10-10T12:00:00Z",
"progress": 100,
@@ -3815,6 +3928,8 @@ class ServersViewBuilderV11Test(test.TestCase):
"server": {
"id": 1,
"uuid": self.instance['uuid'],
+ "user_id": "fake",
+ "tenant_id": "fake",
"updated": "2010-11-11T11:00:00Z",
"created": "2010-10-10T12:00:00Z",
"progress": 0,
@@ -3871,6 +3986,8 @@ class ServersViewBuilderV11Test(test.TestCase):
"server": {
"id": 1,
"uuid": self.instance['uuid'],
+ "user_id": "fake",
+ "tenant_id": "fake",
"updated": "2010-11-11T11:00:00Z",
"created": "2010-10-10T12:00:00Z",
"progress": 0,
@@ -3930,6 +4047,8 @@ class ServersViewBuilderV11Test(test.TestCase):
"server": {
"id": 1,
"uuid": self.instance['uuid'],
+ "user_id": "fake",
+ "tenant_id": "fake",
"updated": "2010-11-11T11:00:00Z",
"created": "2010-10-10T12:00:00Z",
"progress": 0,
@@ -3997,8 +4116,10 @@ class ServerXMLSerializationTest(test.TestCase):
fixture = {
"server": {
- "id": 1,
- "uuid": FAKE_UUID,
+ 'id': 1,
+ 'uuid': FAKE_UUID,
+ 'user_id': 'fake_user_id',
+ 'tenant_id': 'fake_tenant_id',
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
"progress": 0,
@@ -4075,6 +4196,8 @@ class ServerXMLSerializationTest(test.TestCase):
fixture = {
"server": {
"id": 1,
+ "user_id": "fake",
+ "tenant_id": "fake",
"uuid": FAKE_UUID,
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
@@ -4212,6 +4335,8 @@ class ServerXMLSerializationTest(test.TestCase):
"server": {
"id": 1,
"uuid": FAKE_UUID,
+ "user_id": "fake",
+ "tenant_id": "fake",
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
"progress": 0,
@@ -4412,6 +4537,8 @@ class ServerXMLSerializationTest(test.TestCase):
{
"id": 1,
"uuid": FAKE_UUID,
+ "user_id": "fake",
+ "tenant_id": "fake",
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
"progress": 0,
@@ -4467,6 +4594,8 @@ class ServerXMLSerializationTest(test.TestCase):
{
"id": 2,
"uuid": FAKE_UUID,
+ "user_id": 'fake',
+ "tenant_id": 'fake',
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
"progress": 100,
@@ -4586,6 +4715,8 @@ class ServerXMLSerializationTest(test.TestCase):
fixture = {
"server": {
"id": 1,
+ "user_id": "fake",
+ "tenant_id": "fake",
"uuid": FAKE_UUID,
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
@@ -4722,6 +4853,8 @@ class ServerXMLSerializationTest(test.TestCase):
"server": {
"id": 1,
"uuid": FAKE_UUID,
+ "user_id": "fake",
+ "tenant_id": "fake",
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
"progress": 0,
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 766a7da9b..65fdffbd6 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -161,6 +161,19 @@ class ComputeTestCase(test.TestCase):
db.security_group_destroy(self.context, group['id'])
db.instance_destroy(self.context, ref[0]['id'])
+ def test_create_instance_with_invalid_security_group_raises(self):
+ instance_type = instance_types.get_default_instance_type()
+
+ pre_build_len = len(db.instance_get_all(context.get_admin_context()))
+ self.assertRaises(exception.SecurityGroupNotFoundForProject,
+ self.compute_api.create,
+ self.context,
+ instance_type=instance_type,
+ image_href=None,
+ security_group=['this_is_a_fake_sec_group'])
+ self.assertEqual(pre_build_len,
+ len(db.instance_get_all(context.get_admin_context())))
+
def test_create_instance_associates_config_drive(self):
"""Make sure create associates a config drive."""
diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py
index 25ff940f0..2ae5a35e3 100644
--- a/nova/tests/test_network.py
+++ b/nova/tests/test_network.py
@@ -118,9 +118,14 @@ vifs = [{'id': 0,
{'id': 1,
'address': 'DE:AD:BE:EF:00:01',
'uuid': '00000000-0000-0000-0000-0000000000000001',
- 'network_id': 0,
'network_id': 1,
'network': FakeModel(**networks[1]),
+ 'instance_id': 0},
+ {'id': 2,
+ 'address': 'DE:AD:BE:EF:00:02',
+ 'uuid': '00000000-0000-0000-0000-0000000000000002',
+ 'network_id': 2,
+ 'network': None,
'instance_id': 0}]
diff --git a/nova/tests/test_quantum.py b/nova/tests/test_quantum.py
new file mode 100644
index 000000000..0feec9b99
--- /dev/null
+++ b/nova/tests/test_quantum.py
@@ -0,0 +1,323 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Nicira, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova import context
+from nova import db
+from nova.db.sqlalchemy import models
+from nova.db.sqlalchemy.session import get_session
+from nova import exception
+from nova import ipv6
+from nova import log as logging
+from nova.network.quantum import manager as quantum_manager
+from nova import test
+from nova import utils
+
+LOG = logging.getLogger('nova.tests.quantum_network')
+
+
+# this class can be used for unit functional/testing on nova,
+# as it does not actually make remote calls to the Quantum service
+class FakeQuantumClientConnection(object):
+
+ def __init__(self):
+ self.nets = {}
+
+ def get_networks_for_tenant(self, tenant_id):
+ net_ids = []
+ for net_id, n in self.nets.items():
+ if n['tenant-id'] == tenant_id:
+ net_ids.append(net_id)
+ return net_ids
+
+ def create_network(self, tenant_id, network_name):
+
+ uuid = str(utils.gen_uuid())
+ self.nets[uuid] = {'net-name': network_name,
+ 'tenant-id': tenant_id,
+ 'ports': {}}
+ return uuid
+
+ def delete_network(self, tenant_id, net_id):
+ if self.nets[net_id]['tenant-id'] == tenant_id:
+ del self.nets[net_id]
+
+ def network_exists(self, tenant_id, net_id):
+ try:
+ return self.nets[net_id]['tenant-id'] == tenant_id
+ except KeyError:
+ return False
+
+ def _confirm_not_attached(self, interface_id):
+ for n in self.nets.values():
+ for p in n['ports'].values():
+ if p['attachment-id'] == interface_id:
+ raise Exception(_("interface '%s' is already attached" %
+ interface_id))
+
+ def create_and_attach_port(self, tenant_id, net_id, interface_id):
+ if not self.network_exists(tenant_id, net_id):
+ raise Exception(
+ _("network %(net_id)s does not exist for tenant %(tenant_id)"
+ % locals()))
+
+ self._confirm_not_attached(interface_id)
+ uuid = str(utils.gen_uuid())
+ self.nets[net_id]['ports'][uuid] = \
+ {"port-state": "ACTIVE",
+ "attachment-id": interface_id}
+
+ def detach_and_delete_port(self, tenant_id, net_id, port_id):
+ if not self.network_exists(tenant_id, net_id):
+ raise exception.NotFound(
+ _("network %(net_id)s does not exist "
+ "for tenant %(tenant_id)s" % locals()))
+ del self.nets[net_id]['ports'][port_id]
+
+ def get_port_by_attachment(self, tenant_id, attachment_id):
+ for net_id, n in self.nets.items():
+ if n['tenant-id'] == tenant_id:
+ for port_id, p in n['ports'].items():
+ if p['attachment-id'] == attachment_id:
+ return (net_id, port_id)
+
+ return (None, None)
+
+networks = [{'label': 'project1-net1',
+ 'injected': False,
+ 'multi_host': False,
+ 'cidr': '192.168.0.0/24',
+ 'cidr_v6': '2001:1db8::/64',
+ 'gateway_v6': '2001:1db8::1',
+ 'netmask_v6': '64',
+ 'netmask': '255.255.255.0',
+ 'bridge': None,
+ 'bridge_interface': None,
+ 'gateway': '192.168.0.1',
+ 'broadcast': '192.168.0.255',
+ 'dns1': '192.168.0.1',
+ 'dns2': '192.168.0.2',
+ 'vlan': None,
+ 'host': None,
+ 'vpn_public_address': None,
+ 'project_id': 'fake_project1',
+ 'priority': 1},
+ {'label': 'project2-net1',
+ 'injected': False,
+ 'multi_host': False,
+ 'cidr': '192.168.1.0/24',
+ 'cidr_v6': '2001:1db9::/64',
+ 'gateway_v6': '2001:1db9::1',
+ 'netmask_v6': '64',
+ 'netmask': '255.255.255.0',
+ 'bridge': None,
+ 'bridge_interface': None,
+ 'gateway': '192.168.1.1',
+ 'broadcast': '192.168.1.255',
+ 'dns1': '192.168.0.1',
+ 'dns2': '192.168.0.2',
+ 'vlan': None,
+ 'host': None,
+ 'project_id': 'fake_project2',
+ 'priority': 1},
+ {'label': "public",
+ 'injected': False,
+ 'multi_host': False,
+ 'cidr': '10.0.0.0/24',
+ 'cidr_v6': '2001:1dba::/64',
+ 'gateway_v6': '2001:1dba::1',
+ 'netmask_v6': '64',
+ 'netmask': '255.255.255.0',
+ 'bridge': None,
+ 'bridge_interface': None,
+ 'gateway': '10.0.0.1',
+ 'broadcast': '10.0.0.255',
+ 'dns1': '10.0.0.1',
+ 'dns2': '10.0.0.2',
+ 'vlan': None,
+ 'host': None,
+ 'project_id': None,
+ 'priority': 0},
+ {'label': "project2-net2",
+ 'injected': False,
+ 'multi_host': False,
+ 'cidr': '9.0.0.0/24',
+ 'cidr_v6': '2001:1dbb::/64',
+ 'gateway_v6': '2001:1dbb::1',
+ 'netmask_v6': '64',
+ 'netmask': '255.255.255.0',
+ 'bridge': None,
+ 'bridge_interface': None,
+ 'gateway': '9.0.0.1',
+ 'broadcast': '9.0.0.255',
+ 'dns1': '9.0.0.1',
+ 'dns2': '9.0.0.2',
+ 'vlan': None,
+ 'host': None,
+ 'project_id': "fake_project2",
+ 'priority': 2}]
+
+
+# this is a base class to be used by all other Quantum Test classes
+class QuantumTestCaseBase(object):
+
+ def test_create_and_delete_nets(self):
+ self._create_nets()
+ self._delete_nets()
+
+ def _create_nets(self):
+ for n in networks:
+ ctx = context.RequestContext('user1', n['project_id'])
+ self.net_man.create_networks(ctx,
+ label=n['label'], cidr=n['cidr'],
+ multi_host=n['multi_host'],
+ num_networks=1, network_size=256, cidr_v6=n['cidr_v6'],
+ gateway_v6=n['gateway_v6'], bridge=None,
+ bridge_interface=None, dns1=n['dns1'],
+ dns2=n['dns2'], project_id=n['project_id'],
+ priority=n['priority'])
+
+ def _delete_nets(self):
+ for n in networks:
+ ctx = context.RequestContext('user1', n['project_id'])
+ self.net_man.delete_network(ctx, n['cidr'])
+
+ def test_allocate_and_deallocate_instance_static(self):
+ self._create_nets()
+
+ project_id = "fake_project1"
+ ctx = context.RequestContext('user1', project_id)
+
+ instance_ref = db.api.instance_create(ctx,
+ {"project_id": project_id})
+ nw_info = self.net_man.allocate_for_instance(ctx,
+ instance_id=instance_ref['id'], host="",
+ instance_type_id=instance_ref['instance_type_id'],
+ project_id=project_id)
+
+ self.assertEquals(len(nw_info), 2)
+
+ # we don't know which order the NICs will be in until we
+ # introduce the notion of priority
+ # v4 cidr
+ self.assertTrue(nw_info[0][0]['cidr'].startswith("10."))
+ self.assertTrue(nw_info[1][0]['cidr'].startswith("192."))
+
+ # v4 address
+ self.assertTrue(nw_info[0][1]['ips'][0]['ip'].startswith("10."))
+ self.assertTrue(nw_info[1][1]['ips'][0]['ip'].startswith("192."))
+
+ # v6 cidr
+ self.assertTrue(nw_info[0][0]['cidr_v6'].startswith("2001:1dba:"))
+ self.assertTrue(nw_info[1][0]['cidr_v6'].startswith("2001:1db8:"))
+
+ # v6 address
+ self.assertTrue(
+ nw_info[0][1]['ip6s'][0]['ip'].startswith("2001:1dba:"))
+ self.assertTrue(
+ nw_info[1][1]['ip6s'][0]['ip'].startswith("2001:1db8:"))
+
+ self.net_man.deallocate_for_instance(ctx,
+ instance_id=instance_ref['id'],
+ project_id=project_id)
+
+ self._delete_nets()
+
+ def test_allocate_and_deallocate_instance_dynamic(self):
+ self._create_nets()
+ project_id = "fake_project2"
+ ctx = context.RequestContext('user1', project_id)
+
+ net_ids = self.net_man.q_conn.get_networks_for_tenant(project_id)
+ requested_networks = [(net_id, None) for net_id in net_ids]
+
+ self.net_man.validate_networks(ctx, requested_networks)
+
+ instance_ref = db.api.instance_create(ctx,
+ {"project_id": project_id})
+ nw_info = self.net_man.allocate_for_instance(ctx,
+ instance_id=instance_ref['id'], host="",
+ instance_type_id=instance_ref['instance_type_id'],
+ project_id=project_id,
+ requested_networks=requested_networks)
+
+ self.assertEquals(len(nw_info), 2)
+
+ # we don't know which order the NICs will be in until we
+ # introduce the notion of priority
+ # v4 cidr
+ self.assertTrue(nw_info[0][0]['cidr'].startswith("9.") or
+ nw_info[1][0]['cidr'].startswith("9."))
+ self.assertTrue(nw_info[0][0]['cidr'].startswith("192.") or
+ nw_info[1][0]['cidr'].startswith("192."))
+
+ # v4 address
+ self.assertTrue(nw_info[0][1]['ips'][0]['ip'].startswith("9.") or
+ nw_info[1][1]['ips'][0]['ip'].startswith("9."))
+ self.assertTrue(nw_info[0][1]['ips'][0]['ip'].startswith("192.") or
+ nw_info[1][1]['ips'][0]['ip'].startswith("192."))
+
+ # v6 cidr
+ self.assertTrue(nw_info[0][0]['cidr_v6'].startswith("2001:1dbb:") or
+ nw_info[1][0]['cidr_v6'].startswith("2001:1dbb:"))
+ self.assertTrue(nw_info[0][0]['cidr_v6'].startswith("2001:1db9:") or
+ nw_info[1][0]['cidr_v6'].startswith("2001:1db9:"))
+
+ # v6 address
+ self.assertTrue(
+ nw_info[0][1]['ip6s'][0]['ip'].startswith("2001:1dbb:") or
+ nw_info[1][1]['ip6s'][0]['ip'].startswith("2001:1dbb:"))
+ self.assertTrue(
+ nw_info[0][1]['ip6s'][0]['ip'].startswith("2001:1db9:") or
+ nw_info[1][1]['ip6s'][0]['ip'].startswith("2001:1db9:"))
+
+ self.net_man.deallocate_for_instance(ctx,
+ instance_id=instance_ref['id'],
+ project_id=project_id)
+
+ self._delete_nets()
+
+ def test_validate_bad_network(self):
+ ctx = context.RequestContext('user1', 'fake_project1')
+ self.assertRaises(exception.NetworkNotFound,
+ self.net_man.validate_networks, ctx, [("", None)])
+
+
+class QuantumNovaIPAMTestCase(QuantumTestCaseBase, test.TestCase):
+
+ def setUp(self):
+ super(QuantumNovaIPAMTestCase, self).setUp()
+
+ self.net_man = quantum_manager.QuantumManager(
+ ipam_lib="nova.network.quantum.nova_ipam_lib",
+ q_conn=FakeQuantumClientConnection())
+
+ # Tests seem to create some networks by default, which
+ # we don't want. So we delete them.
+
+ ctx = context.RequestContext('user1', 'fake_project1').elevated()
+ for n in db.network_get_all(ctx):
+ db.network_delete_safe(ctx, n['id'])
+
+ # Other unit tests (e.g., test_compute.py) have a nasty
+ # habit of of creating fixed IPs and not cleaning up, which
+ # can confuse these tests, so we remove all existing fixed
+ # ips before starting.
+ session = get_session()
+ result = session.query(models.FixedIp).all()
+ with session.begin():
+ for fip_ref in result:
+ session.delete(fip_ref)
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index 45dad3516..91b4161b0 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -494,6 +494,7 @@ class XenAPIVMTestCase(test.TestCase):
self.check_vm_params_for_linux_with_external_kernel()
def test_spawn_netinject_file(self):
+ self.flags(flat_injected=True)
db_fakes.stub_out_db_instance_api(self.stubs, injected=True)
self._tee_executed = False
@@ -611,7 +612,6 @@ class XenAPIVMTestCase(test.TestCase):
str(3 * 1024))
def test_rescue(self):
- self.flags(flat_injected=False)
instance = self._create_instance()
conn = xenapi_conn.get_connection(False)
conn.rescue(self.context, instance, None, [])
diff --git a/nova/utils.py b/nova/utils.py
index 21e6221b2..81157a4cd 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -901,3 +901,12 @@ def monkey_patch():
func = import_class("%s.%s" % (module, key))
setattr(sys.modules[module], key,\
decorator("%s.%s" % (module, key), func))
+
+
+def convert_to_list_dict(lst, label):
+ """Convert a value or list into a list of dicts"""
+ if not lst:
+ return None
+ if not isinstance(lst, list):
+ lst = [lst]
+ return [{label: x} for x in lst]
diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py
index 0b7438011..077c32474 100644
--- a/nova/virt/libvirt/vif.py
+++ b/nova/virt/libvirt/vif.py
@@ -101,7 +101,7 @@ class LibvirtOpenVswitchDriver(VIFDriver):
"""VIF driver for Open vSwitch."""
def get_dev_name(_self, iface_id):
- return "tap-" + iface_id[0:15]
+ return "tap" + iface_id[0:11]
def plug(self, instance, network, mapping):
iface_id = mapping['vif_uuid']