summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Wendlandt <dan@nicira.com>2011-08-22 22:18:43 -0700
committerDan Wendlandt <dan@nicira.com>2011-08-22 22:18:43 -0700
commit632526f0cf7a5be3a26c3ae14683b75bfb6afbfd (patch)
treee04256815f4b4699e494e1c0243aca50eae3801b
parentc2fb9485f956482a5e6d628bb80e86d3e8d90d3a (diff)
pulling all qmanager changes into a branch based on trunk, as they were previously stacked on top of melange
-rwxr-xr-xbin/nova-manage35
-rw-r--r--nova/db/api.py5
-rw-r--r--nova/db/sqlalchemy/api.py20
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/041_add_network_priority.py45
-rw-r--r--nova/db/sqlalchemy/models.py1
-rw-r--r--nova/network/manager.py37
-rw-r--r--nova/network/quantum/__init__.py16
-rw-r--r--nova/network/quantum/client.py306
-rw-r--r--nova/network/quantum/fake.py213
-rw-r--r--nova/network/quantum/manager.py232
-rw-r--r--nova/network/quantum/melange_connection.py133
-rw-r--r--nova/network/quantum/melange_ipam_lib.py135
-rw-r--r--nova/network/quantum/nova_ipam_lib.py152
-rw-r--r--nova/network/quantum/quantum_connection.py97
-rw-r--r--nova/tests/test_quantum.py261
15 files changed, 1660 insertions, 28 deletions
diff --git a/bin/nova-manage b/bin/nova-manage
index 1b29d7196..9819ef206 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -58,11 +58,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...
@@ -681,10 +681,15 @@ 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('--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):
"""Creates fixed ips for host by range"""
# check for certain required inputs
@@ -761,11 +766,14 @@ class NetworkCommands(object):
bridge=bridge,
bridge_interface=bridge_interface,
dns1=dns1,
- dns2=dns2)
+ dns2=dns2,
+ project_id=project_id,
+ priority=priority)
def list(self):
"""List all created networks"""
- _fmt = "%-5s\t%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s"
+ _fmt = "%-5s\t%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s"\
+ "\t%-15s\t%-15s\t-15s\t-15s"
print _fmt % (_('id'),
_('IPv4'),
_('IPv6'),
@@ -774,7 +782,9 @@ class NetworkCommands(object):
_('DNS2'),
_('VlanID'),
_('project'),
- _("uuid"))
+ _("uuid"),
+ _('priority'),
+ _('bridge'))
for network in db.network_get_all(context.get_admin_context()):
print _fmt % (network.id,
network.cidr,
@@ -784,18 +794,19 @@ class NetworkCommands(object):
network.dns2,
network.vlan,
network.project_id,
- network.uuid)
+ network.uuid,
+ network.priority,
+ network.bridge)
@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)
+
class VmCommands(object):
diff --git a/nova/db/api.py b/nova/db/api.py
index 2d854f24c..9ff3a1c74 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -419,6 +419,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)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 04b5405f6..d96b951a1 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -688,10 +688,8 @@ def fixed_ip_associate(context, address, instance_id, network_id=None):
def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None):
session = get_session()
with session.begin():
- network_or_none = or_(models.FixedIp.network_id == network_id,
- models.FixedIp.network_id == None)
fixed_ip_ref = session.query(models.FixedIp).\
- filter(network_or_none).\
+ filter_by(network_id=network_id).\
filter_by(reserved=False).\
filter_by(deleted=False).\
filter_by(instance=None).\
@@ -929,6 +927,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.
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/041_add_network_priority.py b/nova/db/sqlalchemy/migrate_repo/versions/041_add_network_priority.py
new file mode 100644
index 000000000..e619b1fcd
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/041_add_network_priority.py
@@ -0,0 +1,45 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import datetime
+
+from sqlalchemy import *
+from migrate import *
+
+from nova import log as logging
+from nova import utils
+
+meta = MetaData()
+
+# Add priority column to networks table
+priority = Column('priority', Integer())
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+
+ # grab tables and (column for dropping later)
+ networks = Table('networks', meta, autoload=True)
+
+ 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 19dc3302e..11b147802 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -560,6 +560,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/network/manager.py b/nova/network/manager.py
index aa2a3700c..b778377a0 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -443,7 +443,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)
@@ -541,21 +541,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 i in range(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."""
@@ -784,6 +786,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))
+ self.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..a0c7dc6d8
--- /dev/null
+++ b/nova/network/quantum/client.py
@@ -0,0 +1,306 @@
+# 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
+
+
+# this is a simple json-only serializer to use until
+# we can just grab the standard serializer
+# from the quantum library
+class Serializer:
+
+ 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)
+
+
+class api_call(object):
+ """A Decorator to add support for format and tenant overriding"""
+ def __init__(self, f):
+ self.f = f
+
+ 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 = self.f(instance, *args)
+ (instance.format, instance.tenant) = (format, tenant)
+ return ret
+ return with_params
+
+
+class Client(object):
+
+ """A base client class - derived from Glance.BaseClient"""
+
+ action_prefix = '/v0.1/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", testingStub=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 testingStub: 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.testingStub = testingStub
+ 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.testingStub:
+ return self.testingStub
+ if 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 + " " + action + "\n")
+ 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 in (httplib.OK,
+ httplib.CREATED,
+ httplib.ACCEPTED,
+ httplib.NO_CONTENT):
+ return self.deserialize(data, status_code)
+ else:
+ raise Exception("Server returned error: %s" % res.read())
+
+ except (socket.error, IOError), e:
+ raise Exception("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 data is None:
+ return None
+ elif type(data) is dict:
+ return Serializer().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 Serializer().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/fake.py b/nova/network/quantum/fake.py
new file mode 100644
index 000000000..ff2b1e9d5
--- /dev/null
+++ b/nova/network/quantum/fake.py
@@ -0,0 +1,213 @@
+# 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 exception
+from nova import ipv6
+from nova import log as logging
+from nova import utils
+import math
+from netaddr import IPNetwork
+
+
+LOG = logging.getLogger("network.quantum.fake")
+
+
+# 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:
+
+ 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:
+ 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 %s does not exist for tenant %s" %\
+ (net_id, tenant_id))
+
+ 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("network %s does not exist for tenant %s" %\
+ (net_id, tenant_id))
+ 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)
+
+
+def get_ipam_lib(net_man):
+ return FakeQuantumIPAMLib()
+
+
+class FakeQuantumIPAMLib():
+
+ def __init__(self):
+ self.subnets = {}
+
+ def create_subnet(self, context, label, tenant_id, quantum_net_id,
+ cidr=None, gateway_v6=None, cidr_v6=None,
+ dns1=None, dns2=None):
+ if int(cidr.split("/")[1]) != 24:
+ raise Exception("fake ipam_lib only supports /24s")
+ v4_ips = []
+ net_start = cidr[0:cidr.rfind(".") + 1]
+ subnet_size = int(math.pow(2, (32 - int(cidr.split("/")[1]))))
+ for i in xrange(2, subnet_size - 1):
+ v4_ips.append({"ip": net_start + str(i),
+ "allocated": False,
+ "virtual_interface_id": None,
+ "instance_id": None})
+ self.subnets[quantum_net_id] = {\
+ "label": label,
+ "gateway": net_start + "1",
+ "netmask": "255.255.255.0",
+ "broadcast": net_start + "255",
+ "cidr": cidr,
+ "gateway_v6": gateway_v6,
+ "cidr_v6": cidr_v6,
+ "dns1": dns1,
+ "dns2": dns2,
+ "project_id": tenant_id,
+ "v4_ips": v4_ips}
+
+ def get_network_id_by_cidr(self, context, cidr, project_id):
+ for net_id, s in self.subnets.items():
+ if s['cidr'] == cidr or s['cidr_v6'] == cidr:
+ return net_id
+ return None
+
+ def delete_subnets_by_net_id(self, context, net_id, project_id):
+ self.verify_subnet_exists(context, project_id, net_id)
+ del self.subnets[net_id]
+
+ def get_project_and_global_net_ids(self, context, project_id):
+ net_ids = []
+ for nid, s in self.subnets.items():
+ if s['project_id'] == project_id or \
+ s['project_id'] == None:
+ net_ids.append((nid, s['project_id']))
+ return net_ids
+
+ def allocate_fixed_ip(self, context, tenant_id, quantum_net_id, vif_rec):
+ subnet = self.subnets[quantum_net_id]
+ for i in xrange(0, len(subnet['v4_ips'])):
+ ip = subnet['v4_ips'][i]
+ if not ip['allocated']:
+ subnet['v4_ips'][i] = {'ip': ip['ip'],
+ 'allocated': True,
+ 'virtual_interface_id': vif_rec['uuid'],
+ 'instance_id': vif_rec['instance_id']}
+ return
+ raise Exception("Unable to find available IP for net '%s'" %\
+ quantum_net_id)
+
+ def get_subnets_by_net_id(self, context, tenant_id, net_id):
+ self.verify_subnet_exists(context, tenant_id, net_id)
+
+ subnet_data = self.subnets[net_id]
+ subnet_data_v4 = {
+ 'network_id': net_id,
+ 'cidr': subnet_data['cidr'],
+ 'gateway': subnet_data['gateway'],
+ 'broadcast': subnet_data['broadcast'],
+ 'netmask': subnet_data['netmask'],
+ 'dns1': subnet_data['dns1'],
+ 'dns2': subnet_data['dns2']}
+ subnet_data_v6 = {
+ 'network_id': net_id,
+ 'cidr': subnet_data['cidr_v6'],
+ 'gateway': subnet_data['gateway_v6'],
+ 'broadcast': None,
+ 'netmask': None,
+ 'dns1': None,
+ 'dns2': None}
+ return (subnet_data_v4, subnet_data_v6)
+
+ def get_v6_ips_by_interface(self, context, net_id, vif_id, project_id):
+ self.verify_subnet_exists(context, project_id, net_id)
+
+ subnet_data = self.subnets[net_id]
+ if subnet_data['cidr_v6']:
+ ip = ipv6.to_global(subnet_data['cidr_v6'],
+ "ca:fe:de:ad:be:ef",
+ project_id)
+ return [ip]
+ return []
+
+ def get_v4_ips_by_interface(self, context, net_id, vif_id, project_id):
+ self.verify_subnet_exists(context, project_id, net_id)
+
+ subnet_data = self.subnets[net_id]
+ for ip in subnet_data['v4_ips']:
+ if ip['virtual_interface_id'] == vif_id:
+ return [ip['ip']]
+ return []
+
+ def verify_subnet_exists(self, context, tenant_id, quantum_net_id):
+ if quantum_net_id not in self.subnets:
+ raise exception.NetworkNotFound(network_id=quantum_net_id)
+
+ def deallocate_ips_by_vif(self, context, tenant_id, net_id, vif_ref):
+ s = self.subnets[net_id]
+ for ip in s['v4_ips']:
+ if ip['virtual_interface_id'] == vif_ref['id']:
+ ip['allocated'] = False
+ ip['instance_id'] = None
+ ip['virtual_interface_id'] = None
diff --git a/nova/network/quantum/manager.py b/nova/network/quantum/manager.py
new file mode 100644
index 000000000..f712a93c4
--- /dev/null
+++ b/nova/network/quantum/manager.py
@@ -0,0 +1,232 @@
+# 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 import utils
+from nova.network import manager
+from nova.network.quantum import quantum_connection
+from nova.network.quantum import fake
+
+LOG = logging.getLogger("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):
+
+ def __init__(self, ipam_lib=None, *args, **kwargs):
+
+ if FLAGS.fake_network:
+ self.q_conn = fake.FakeQuantumClientConnection()
+ else:
+ self.q_conn = quantum_connection.QuantumClientConnection()
+
+ 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, **kwargs):
+ 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 = bridge
+ if quantum_net_id:
+ if not q_conn.network_exists(q_tenant_id, quantum_net_id):
+ raise Exception("Unable to find existing quantum " \
+ " network for tenant '%s' with net-id '%s'" % \
+ (q_tenant_id, quantum_net_id))
+ 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)
+ self.ipam.create_subnet(context, label, ipam_tenant_id, quantum_net_id,
+ cidr, gateway_v6, cidr_v6, dns1, dns2)
+
+ def delete_network(self, context, fixed_range):
+ 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)
+ try:
+ q_tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ self.q_conn.delete_network(q_tenant_id, quantum_net_id)
+ except Exception, e:
+ raise Exception("Unable to delete Quantum Network with "
+ "fixed_range = %s (ports still in use?)." % fixed_range)
+
+ def allocate_for_instance(self, context, **kwargs):
+ 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)
+
+ # if using the create-server-networks extension, 'requested_networks'
+ # will be defined, otherwise, just grab relevant nets from IPAM
+ 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 (net_id, project_id) in net_proj_pairs:
+ vif_rec = manager.FlatManager.add_virtual_interface(self,
+ context, instance, None)
+
+ q_tenant_id = project_id or FLAGS.quantum_default_tenant_id
+ self.q_conn.create_and_attach_port(q_tenant_id, net_id,
+ vif_rec['uuid'])
+ self.ipam.allocate_fixed_ip(context, project_id, 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):
+ network_info = []
+ project_id = context.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:
+ 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['cidr']:
+ network_dict['cidr_v6'] = v6_subnet['cidr']
+ info['ip6s'] = [ip_dict(ip, v6_subnet) for ip in v6_ips]
+ # TODO(tr3buchet): handle ip6 routes here as well
+ 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[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):
+ 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)
+
+ self.net_manager.db.virtual_interface_delete_by_instance(admin_context,
+ instance_id)
+ self._do_trigger_security_group_members_refresh_for_instance(
+ instance_id)
+
+ # 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.
+ def validate_networks(self, context, networks):
+ 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..b3955138d
--- /dev/null
+++ b/nova/network/quantum/melange_connection.py
@@ -0,0 +1,133 @@
+# 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"}
+
+
+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
+
+ def get(self, path, params={}, headers={}):
+ return self.do_request("GET", path, params=params, headers=headers)
+
+ def post(self, path, body=None, headers={}):
+ return self.do_request("POST", path, body=body, headers=headers)
+
+ def delete(self, path, headers={}):
+ 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={}, params={}):
+
+ url = path + '.json?' + 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..46038c349
--- /dev/null
+++ b/nova/network/quantum/melange_ipam_lib.py
@@ -0,0 +1,135 @@
+# 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 flags
+from nova import log as logging
+from nova.network.quantum import melange_connection
+
+LOG = logging.getLogger("quantum_melange_ipam")
+
+FLAGS = flags.FLAGS
+
+
+def get_ipam_lib(net_man):
+ return QuantumMelangeIPAMLib()
+
+
+class QuantumMelangeIPAMLib:
+
+ def __init__(self):
+ self.m_conn = melange_connection.MelangeConnection()
+
+ def create_subnet(self, context, label, project_id,
+ quantum_net_id, cidr=None,
+ gateway_v6=None, cidr_v6=None,
+ dns1=None, dns2=None):
+ 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)
+
+ def allocate_fixed_ip(self, context, project_id, quantum_net_id, vif_ref):
+ 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):
+ 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']
+
+ def delete_subnets_by_net_id(self, context, net_id, project_id):
+ 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)
+
+ # get all networks with this project_id, as well as all networks
+ # where the project-id is not set (these are shared networks)
+ def get_project_and_global_net_ids(self, context, project_id):
+ id_proj_map = {}
+ if not project_id:
+ raise Exception("get_project_and_global_net_ids must be called" \
+ " with a non-null project_id")
+ tenant_id = project_id
+ all_tenant_blocks = self.m_conn.get_blocks(tenant_id)
+ for b in all_tenant_blocks['ip_blocks']:
+ id_proj_map[b['network_id']] = tenant_id
+ tenant_id = FLAGS.quantum_default_tenant_id
+ all_provider_blocks = self.m_conn.get_blocks(tenant_id)
+ for b in all_provider_blocks['ip_blocks']:
+ id_proj_map[b['network_id']] = tenant_id
+ return id_proj_map.items()
+
+ # FIXME: there must be a more efficient way to do this,
+ # talk to the melange folks
+ def get_subnets_by_net_id(self, context, project_id, net_id):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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..0fc74fa49
--- /dev/null
+++ b/nova/network/quantum/nova_ipam_lib.py
@@ -0,0 +1,152 @@
+# 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 math
+
+#from nova import context
+from nova import db
+from nova import exception
+from nova import flags
+from nova import ipv6
+from nova import log as logging
+from nova import utils
+from nova.network import manager
+from nova.network.quantum import melange_connection as melange
+
+LOG = logging.getLogger("quantum_nova_ipam_lib")
+
+FLAGS = flags.FLAGS
+
+
+def get_ipam_lib(net_man):
+ return QuantumNovaIPAMLib(net_man)
+
+
+class QuantumNovaIPAMLib:
+
+ def __init__(self, net_manager):
+ self.net_manager = net_manager
+
+ def create_subnet(self, context, label, tenant_id,
+ quantum_net_id, cidr=None,
+ gateway_v6=None, cidr_v6=None,
+ dns1=None, dns2=None):
+ print "creating subnet %s" % cidr
+ admin_context = context.elevated()
+ subnet_size = int(math.pow(2, (32 - int(cidr.split("/")[1]))))
+ 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)
+
+ # now grab the network and update project_id
+ network = db.network_get_by_bridge(admin_context, quantum_net_id)
+ net = {"project_id": tenant_id}
+ db.network_update(admin_context, network['id'], net)
+
+ def get_network_id_by_cidr(self, context, cidr, project_id):
+ 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['bridge']
+
+ def delete_subnets_by_net_id(self, context, net_id, project_id):
+ admin_context = context.elevated()
+ network = db.network_get_by_bridge(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):
+
+ # get all networks with this project_id, as well as all networks
+ # where the project-id is not set (these are shared networks)
+ admin_context = context.elevated()
+ networks = db.project_get_networks(admin_context, project_id, False)
+ networks.extend(db.project_get_networks(admin_context, None, False))
+ return [(n['bridge'], n['project_id']) for n in networks]
+
+ def allocate_fixed_ip(self, context, tenant_id, quantum_net_id, vif_rec):
+ admin_context = context.elevated()
+ network = db.network_get_by_bridge(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):
+ n = db.network_get_by_bridge(context.elevated(), net_id)
+ subnet_data_v4 = {
+ 'network_id': n['bridge'],
+ 'cidr': n['cidr'],
+ 'gateway': n['gateway'],
+ 'broadcast': n['broadcast'],
+ 'netmask': n['netmask'],
+ 'dns1': n['dns1'],
+ 'dns2': n['dns2']
+ }
+ subnet_data_v6 = {
+ 'network_id': n['bridge'],
+ '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):
+ 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 [f['address'] for f in fixed_ips]
+
+ def get_v6_ips_by_interface(self, context, net_id, vif_id, project_id):
+ admin_context = context.elevated()
+ network = db.network_get_by_bridge(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):
+ admin_context = context.elevated()
+ network = db.network_get_by_bridge(admin_context, quantum_net_id)
+
+ def deallocate_ips_by_vif(self, context, tenant_id, net_id, vif_ref):
+ try:
+ admin_context = context.elevated()
+ fixed_ips = db.fixed_ip_get_by_virtual_interface(admin_context,
+ vif_ref['id'])
+ for f in fixed_ips:
+ db.fixed_ip_update(admin_context, f['address'],
+ {'allocated': False,
+ 'virtual_interface_id': None})
+ except exception.FixedIpNotFoundForInstance:
+ LOG.error(_('Failed to deallocate fixed IP 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..3aa017bcd
--- /dev/null
+++ b/nova/network/quantum/quantum_connection.py
@@ -0,0 +1,97 @@
+# 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 import utils
+
+from nova.network.quantum.client import Client
+
+LOG = logging.getLogger("nova.network.quantum")
+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:
+
+ def __init__(self):
+ self.client = Client(FLAGS.quantum_connection_host,
+ FLAGS.quantum_connection_port,
+ format="json",
+ logger=LOG)
+
+ def create_network(self, tenant_id, network_name):
+ data = {'network': {'net-name': network_name}}
+ resdict = self.client.create_network(data, tenant=tenant_id)
+ return resdict["networks"]["network"]["id"]
+
+ def delete_network(self, tenant_id, net_id):
+ self.client.delete_network(net_id, tenant=tenant_id)
+
+ def network_exists(self, tenant_id, net_id):
+ try:
+ self.client.show_network_details(net_id, tenant=tenant_id)
+ except:
+ # FIXME: client lib should expose more granular exceptions
+ # so we can confirm we're getting a 404 and not some other error
+ return False
+ return True
+
+ def create_and_attach_port(self, tenant_id, net_id, interface_id):
+ LOG.debug("Connecting interface %s to net %s for %s" % \
+ (interface_id, net_id, tenant_id))
+ port_data = {'port': {'port-state': 'ACTIVE'}}
+ resdict = self.client.create_port(net_id, port_data, tenant=tenant_id)
+ port_id = resdict["ports"]["port"]["id"]
+
+ attach_data = {'port': {'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):
+ LOG.debug("Deleteing port %s on net %s for %s" % \
+ (port_id, net_id, tenant_id))
+
+ self.client.detach_resource(net_id, port_id, tenant=tenant_id)
+ self.client.delete_port(net_id, port_id, tenant=tenant_id)
+
+ # FIXME: this will be inefficient until API implements querying
+ def get_port_by_attachment(self, tenant_id, attachment_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"]:
+ return (net_id, port_id)
+ return (None, None)
diff --git a/nova/tests/test_quantum.py b/nova/tests/test_quantum.py
new file mode 100644
index 000000000..378beb6ed
--- /dev/null
+++ b/nova/tests/test_quantum.py
@@ -0,0 +1,261 @@
+# 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 import exception
+from nova import log as logging
+from nova import test
+from nova.network.quantum import manager as quantum_manager
+
+LOG = logging.getLogger('nova.tests.quantum_network')
+
+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'},
+ {'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',
+ 'vpn_public_address': '192.168.1.2'},
+ {'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,
+ 'vpn_public_address': None,
+ 'project_id': None},
+ {'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,
+ 'vpn_public_address': None,
+ 'project_id': "fake_project2"}]
+
+
+# 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'])
+
+ 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, {})
+ 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.") or \
+ nw_info[1][0]['cidr'].startswith("10."))
+ 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("10.") or \
+ nw_info[1][1]['ips'][0]['ip'].startswith("10."))
+ 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:1dba:") or \
+ nw_info[1][0]['cidr_v6'].startswith("2001:1dba:"))
+ self.assertTrue(nw_info[0][0]['cidr_v6'].startswith("2001:1db8:") or \
+ nw_info[1][0]['cidr_v6'].startswith("2001:1db8:"))
+
+ # v6 address
+ self.assertTrue(\
+ nw_info[0][1]['ip6s'][0]['ip'].startswith("2001:1dba:") or \
+ nw_info[1][1]['ip6s'][0]['ip'].startswith("2001:1dba:"))
+ self.assertTrue(\
+ nw_info[0][1]['ip6s'][0]['ip'].startswith("2001:1db8:") or \
+ 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, {})
+ 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 QuantumFakeIPAMTestCase(QuantumTestCaseBase, test.TestCase):
+
+ def setUp(self):
+ super(QuantumFakeIPAMTestCase, self).setUp()
+ self.net_man = quantum_manager.QuantumManager( \
+ ipam_lib="nova.network.quantum.fake")
+
+
+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")
+
+ # tests seem to create some networks by default, which
+ # 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'])
+
+# Cannot run this unit tests auotmatically for now, as it requires
+# melange to be running locally.
+#
+#class QuantumMelangeIPAMTestCase(QuantumTestCaseBase, test.TestCase):
+#
+# def setUp(self):
+# super(QuantumMelangeIPAMTestCase, self).setUp()
+# self.net_man = quantum_manager.QuantumManager( \
+# ipam_lib="nova.network.quantum.melange_ipam_lib")