merged trunk
This commit is contained in:
1
.mailmap
1
.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>
|
||||
|
||||
1
Authors
1
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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -944,6 +944,22 @@ def virtual_interface_get_by_address(context, address):
|
||||
return vif_ref
|
||||
|
||||
|
||||
@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.
|
||||
@@ -1857,6 +1873,19 @@ def network_get_by_bridge(context, bridge):
|
||||
return result
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -435,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.")
|
||||
|
||||
|
||||
@@ -549,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."""
|
||||
@@ -792,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."""
|
||||
|
||||
16
nova/network/quantum/__init__.py
Normal file
16
nova/network/quantum/__init__.py
Normal file
@@ -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.
|
||||
307
nova/network/quantum/client.py
Normal file
307
nova/network/quantum/client.py
Normal file
@@ -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))
|
||||
324
nova/network/quantum/manager.py
Normal file
324
nova/network/quantum/manager.py
Normal file
@@ -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)
|
||||
141
nova/network/quantum/melange_connection.py
Normal file
141
nova/network/quantum/melange_connection.py
Normal file
@@ -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)
|
||||
205
nova/network/quantum/melange_ipam_lib.py
Normal file
205
nova/network/quantum/melange_ipam_lib.py
Normal file
@@ -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)
|
||||
195
nova/network/quantum/nova_ipam_lib.py
Normal file
195
nova/network/quantum/nova_ipam_lib.py
Normal file
@@ -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']))
|
||||
118
nova/network/quantum/quantum_connection.py
Normal file
118
nova/network/quantum/quantum_connection.py
Normal file
@@ -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)
|
||||
@@ -55,5 +55,17 @@ class BaseScheduler(abstract_scheduler.AbstractScheduler):
|
||||
scheduling objectives
|
||||
"""
|
||||
# NOTE(sirp): The default logic is the same as the NoopCostFunction
|
||||
return [dict(weight=1, hostname=hostname, capabilities=capabilities)
|
||||
for hostname, capabilities in hosts]
|
||||
hosts = [dict(weight=1, hostname=hostname, capabilities=capabilities)
|
||||
for hostname, capabilities in hosts]
|
||||
|
||||
# NOTE(Vek): What we actually need to return is enough hosts
|
||||
# for all the instances!
|
||||
num_instances = request_spec.get('num_instances', 1)
|
||||
instances = []
|
||||
while num_instances > len(hosts):
|
||||
instances.extend(hosts)
|
||||
num_instances -= len(hosts)
|
||||
if num_instances > 0:
|
||||
instances.extend(hosts[:num_instances])
|
||||
|
||||
return instances
|
||||
|
||||
@@ -26,6 +26,7 @@ from nova import test
|
||||
from nova.compute import api as compute_api
|
||||
from nova.scheduler import driver
|
||||
from nova.scheduler import abstract_scheduler
|
||||
from nova.scheduler import base_scheduler
|
||||
from nova.scheduler import zone_manager
|
||||
|
||||
|
||||
@@ -65,6 +66,11 @@ class FakeAbstractScheduler(abstract_scheduler.AbstractScheduler):
|
||||
pass
|
||||
|
||||
|
||||
class FakeBaseScheduler(base_scheduler.BaseScheduler):
|
||||
# No need to stub anything at the moment
|
||||
pass
|
||||
|
||||
|
||||
class FakeZoneManager(zone_manager.ZoneManager):
|
||||
def __init__(self):
|
||||
self.service_states = {
|
||||
@@ -387,3 +393,30 @@ class AbstractSchedulerTestCase(test.TestCase):
|
||||
|
||||
# 0 from local zones, 12 from remotes
|
||||
self.assertEqual(12, len(build_plan))
|
||||
|
||||
|
||||
class BaseSchedulerTestCase(test.TestCase):
|
||||
"""Test case for Base Scheduler."""
|
||||
|
||||
def test_weigh_hosts(self):
|
||||
"""
|
||||
Try to weigh a short list of hosts and make sure enough
|
||||
entries for a larger number instances are returned.
|
||||
"""
|
||||
|
||||
sched = FakeBaseScheduler()
|
||||
|
||||
# Fake out a list of hosts
|
||||
zm = FakeZoneManager()
|
||||
hostlist = [(host, services['compute'])
|
||||
for host, services in zm.service_states.items()
|
||||
if 'compute' in services]
|
||||
|
||||
# Call weigh_hosts()
|
||||
num_instances = len(hostlist) * 2 + len(hostlist) / 2
|
||||
instlist = sched.weigh_hosts('compute',
|
||||
dict(num_instances=num_instances),
|
||||
hostlist)
|
||||
|
||||
# Should be enough entries to cover all instances
|
||||
self.assertEqual(len(instlist), num_instances)
|
||||
|
||||
323
nova/tests/test_quantum.py
Normal file
323
nova/tests/test_quantum.py
Normal file
@@ -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)
|
||||
@@ -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']
|
||||
|
||||
@@ -188,9 +188,16 @@ class VMOps(object):
|
||||
ramdisk = VMHelper.fetch_image(context, self._session,
|
||||
instance, instance.ramdisk_id, instance.user_id,
|
||||
instance.project_id, ImageType.RAMDISK)[0]
|
||||
# Create the VM ref and attach the first disk
|
||||
first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid',
|
||||
vdis[0]['vdi_uuid'])
|
||||
|
||||
# NOTE(jk0): Since vdi_type may contain either 'os' or 'swap', we
|
||||
# need to ensure that the 'swap' VDI is not chosen as the mount
|
||||
# point for file injection.
|
||||
first_vdi_ref = None
|
||||
for vdi in vdis:
|
||||
if vdi.get('vdi_type') != 'swap':
|
||||
# Create the VM ref and attach the first disk
|
||||
first_vdi_ref = self._session.call_xenapi(
|
||||
'VDI.get_by_uuid', vdi['vdi_uuid'])
|
||||
|
||||
vm_mode = instance.vm_mode and instance.vm_mode.lower()
|
||||
if vm_mode == 'pv':
|
||||
|
||||
Reference in New Issue
Block a user