Commit 1fcb59b8 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

cyclades: Refactor code for allocating IPs

Refactor code that was used for allocating IP addresses and creating
NetworkInterfaces. The new code is more suitable to the new DB schema
where each network may have many IPv4 subnets and each subnet may
have many IP pools. Also, the new functions are using prefetch related
to minimize the needed DB queries.
parent e15d8a29
......@@ -409,50 +409,41 @@ class ServerCreateAPITest(ComputeAPITest):
def test_create_network_settings(self, mrapi):
mrapi().CreateInstance.return_value = 12
# Create public network and backend
subnet1 = mfactory.IPv4SubnetFactory()
subnet1 = mfactory.IPv4SubnetFactory(network__userid="test_user")
bnet1 = mfactory.BackendNetworkFactory(network=subnet1.network,
backend=self.backend,
operstate="ACTIVE")
subnet2 = mfactory.IPv4SubnetFactory()
subnet2 = mfactory.IPv4SubnetFactory(network__userid="test_user")
bnet2 = mfactory.BackendNetworkFactory(network=subnet2.network,
backend=self.backend,
operstate="ACTIVE")
subnet3 = mfactory.IPv4SubnetFactory(network__userid="test_user")
bnet3 = mfactory.BackendNetworkFactory(network=subnet3.network,
backend=self.backend,
operstate="ACTIVE")
subnet4 = mfactory.IPv4SubnetFactory(network__userid="test_user")
bnet4 = mfactory.BackendNetworkFactory(network=subnet4.network,
backend=self.backend,
operstate="ACTIVE")
# User requested private networks
request = deepcopy(self.request)
request["server"]["networks"] = [bnet3.network.id, bnet4.network.id]
request["server"]["networks"] = [bnet1.network.id, bnet2.network.id]
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=[
"SNF:ANY_PUBLIC",
bnet1.network.id,
bnet2.network.id]):
"SNF:ANY_PUBLIC"]):
with mocked_quotaholder():
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
self.assertEqual(response.status_code, 202)
name, args, kwargs = mrapi().CreateInstance.mock_calls[0]
self.assertEqual(len(kwargs["nics"]), 5)
self.assertEqual(len(kwargs["nics"]), 3)
self.assertEqual(kwargs["nics"][0]["network"],
self.network.backend_id)
self.assertEqual(kwargs["nics"][1]["network"],
bnet1.network.backend_id)
self.assertEqual(kwargs["nics"][2]["network"],
bnet2.network.backend_id)
self.assertEqual(kwargs["nics"][3]["network"],
bnet3.network.backend_id)
self.assertEqual(kwargs["nics"][4]["network"],
bnet4.network.backend_id)
subnet3 = mfactory.IPv4SubnetFactory(network__public=True,
network__floating_ip_pool=True)
bnet3 = mfactory.BackendNetworkFactory(network=subnet3.network,
backend=self.backend,
operstate="ACTIVE")
request["server"]["floating_ips"] = []
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=[bnet2.network.id]):
DEFAULT_INSTANCE_NETWORKS=[bnet3.network.id]):
with mocked_quotaholder():
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
......@@ -460,11 +451,11 @@ class ServerCreateAPITest(ComputeAPITest):
name, args, kwargs = mrapi().CreateInstance.mock_calls[1]
self.assertEqual(len(kwargs["nics"]), 3)
self.assertEqual(kwargs["nics"][0]["network"],
bnet2.network.backend_id)
self.assertEqual(kwargs["nics"][1]["network"],
bnet3.network.backend_id)
self.assertEqual(kwargs["nics"][1]["network"],
bnet1.network.backend_id)
self.assertEqual(kwargs["nics"][2]["network"],
bnet4.network.backend_id)
bnet2.network.backend_id)
# test invalid network in DEFAULT_INSTANCE_NETWORKS
with override_settings(settings, DEFAULT_INSTANCE_NETWORKS=[42]):
......@@ -480,9 +471,10 @@ class ServerCreateAPITest(ComputeAPITest):
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
self.assertFault(response, 403, "forbidden")
# test wrong user
request = deepcopy(self.request)
request["server"]["networks"] = [bnet3.network.id]
request["server"]["networks"] = [bnet1.network.id]
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=["SNF:ANY_PUBLIC"]):
with mocked_quotaholder():
......@@ -492,12 +484,14 @@ class ServerCreateAPITest(ComputeAPITest):
# Test floating IPs
request = deepcopy(self.request)
request["server"]["networks"] = [bnet4.network.id]
request["server"]["networks"] = [bnet1.network.id]
fp1 = mfactory.FloatingIPFactory(address="10.0.0.2",
userid="test_user",
network=self.network,
nic=None)
fp2 = mfactory.FloatingIPFactory(address="10.0.0.3",
userid="test_user",
network=self.network,
nic=None)
request["server"]["floating_ips"] = [fp1.address, fp2.address]
with override_settings(settings,
......@@ -521,7 +515,7 @@ class ServerCreateAPITest(ComputeAPITest):
self.assertEqual(kwargs["nics"][2]["network"], fp2.network.backend_id)
self.assertEqual(kwargs["nics"][2]["ip"], fp2.address)
self.assertEqual(kwargs["nics"][3]["network"],
bnet4.network.backend_id)
bnet1.network.backend_id)
def test_create_server_no_flavor(self, mrapi):
request = deepcopy(self.request)
......
......@@ -47,10 +47,9 @@ from django.db.models import Q
from snf_django.lib.api import faults
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
Network, BackendNetwork, NetworkInterface,
BridgePoolTable, MacPrefixPoolTable, Backend,
IPAddress)
from synnefo.db.pools import EmptyPool
Network, NetworkInterface, BridgePoolTable,
MacPrefixPoolTable, IPAddress, IPPoolTable)
from synnefo.db import pools
from synnefo.plankton.utils import image_backend
......@@ -223,7 +222,7 @@ def get_network(network_id, user_id, for_update=False, non_deleted=False):
raise faults.BadRequest("Network has been deleted.")
return network
except (ValueError, Network.DoesNotExist):
raise faults.ItemNotFound('Network not found.')
raise faults.ItemNotFound('Network %s not found.' % network_id)
def get_port(port_id, user_id, for_update=False):
......@@ -245,59 +244,126 @@ def get_port(port_id, user_id, for_update=False):
raise faults.ItemNotFound('Port not found.')
def get_floating_ip(user_id, ipv4, for_update=False):
def get_floating_ip_by_address(userid, address, for_update=False):
try:
objects = IPAddress.objects
if for_update:
objects = objects.select_for_update()
return objects.get(userid=user_id, ipv4=ipv4, deleted=False)
return objects.get(userid=userid, floating_ip=True,
address=address, deleted=False)
except IPAddress.DoesNotExist:
raise faults.ItemNotFound("Floating IP does not exist.")
def allocate_public_address(backend, userid):
"""Get a public IP for any available network of a backend."""
# Guarantee exclusive access to backend, because accessing the IP pools of
# the backend networks may result in a deadlock with backend allocator
# which also checks that backend networks have a free IP.
backend = Backend.objects.select_for_update().get(id=backend.id)
public_networks = backend_public_networks(backend)
return get_free_ip(public_networks, userid)
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
try:
objects = IPAddress.objects
if for_update:
objects = objects.select_for_update()
return objects.get(id=floating_ip_id, floating_ip=True, userid=userid,
deleted=False)
except IPAddress.DoesNotExist:
raise faults.ItemNotFound("Floating IP %s does not exist." %
floating_ip_id)
def backend_public_networks(backend):
"""Return available public networks of the backend.
def allocate_ip_from_pools(pool_rows, userid, address=None, floating_ip=False):
"""Try to allocate a value from a number of pools.
Iterator for non-deleted public networks that are available
to the specified backend.
This function takes as argument a number of PoolTable objects and tries to
allocate a value from them. If all pools are empty EmptyPool is raised.
"""
bnets = BackendNetwork.objects.filter(backend=backend,
network__public=True,
network__deleted=False,
network__floating_ip_pool=False,
network__drained=False)
return [b.network for b in bnets]
def get_free_ip(networks, userid):
for network in networks:
for pool_row in pool_rows:
pool = pool_row.pool
try:
return network.allocate_address(userid=userid)
except faults.OverLimit:
value = pool.get(value=address)
pool.save()
subnet = pool_row.subnet
ipaddress = IPAddress.objects.create(subnet=subnet,
network=subnet.network,
userid=userid,
address=value,
floating_ip=floating_ip)
return ipaddress
except pools.EmptyPool:
pass
msg = "Can not allocate public IP. Public networks are full."
log.error(msg)
raise faults.OverLimit(msg)
raise pools.EmptyPool("No more IP addresses available on pools %s" %
pool_rows)
def allocate_ip(network, userid, address=None, floating_ip=False):
"""Try to allocate an IP from networks IP pools."""
ip_pools = IPPoolTable.objects.select_for_update()\
.filter(subnet__network=network)
try:
return allocate_ip_from_pools(ip_pools, userid, address=address,
floating_ip=floating_ip)
except pools.EmptyPool:
raise faults.Conflict("No more IP addresses available on network %s"
% network.id)
except pools.ValueNotAvailable:
raise faults.Conflict("IP address %s is already used." % address)
def allocate_public_ip(userid, floating_ip=False, backend=None):
"""Try to allocate a public or floating IP address.
Try to allocate a a public IPv4 address from one of the available networks.
If 'floating_ip' is set, only networks which are floating IP pools will be
used and the IPAddress that will be created will be marked as a floating
IP. If 'backend' is set, only the networks that exist in this backend will
be used.
"""
def get_network_free_address(network, userid):
"""Reserve an IP address from the IP Pool of the network."""
ip_pool_rows = IPPoolTable.objects.select_for_update()\
.prefetch_related("subnet__network")\
.filter(subnet__deleted=False)\
.filter(subnet__network__public=True)\
.filter(subnet__network__drained=False)
if floating_ip:
ip_pool_rows = ip_pool_rows\
.filter(subnet__network__floating_ip_pool=True)
if backend is not None:
ip_pool_rows = ip_pool_rows\
.filter(subnet__network__backend_networks__backend=backend)
try:
return network.allocate_address(userid=userid)
except EmptyPool:
raise faults.OverLimit("Network %s is full." % network.backend_id)
return allocate_ip_from_pools(ip_pool_rows, userid,
floating_ip=floating_ip)
except pools.EmptyPool:
ip_type = "floating" if floating_ip else "public"
log_msg = "Failed to allocate a %s IP. Reason:" % ip_type
if ip_pool_rows:
log_msg += " No network exists."
else:
log_msg += " All network are full."
if backend is not None:
log_msg += " Backend: %s" % backend
log.error(log_msg)
exception_msg = "Can not allocate a %s IP address." % ip_type
raise faults.ServiceUnavailable(exception_msg)
def backend_has_free_public_ip(backend):
"""Check if a backend has a free public IPv4 address."""
ip_pool_rows = IPPoolTable.objects.select_for_update()\
.filter(subnet__network__public=True)\
.filter(subnet__network__drained=False)\
.filter(subnet__deleted=False)\
.filter(subnet__network__backend_networks__backend=backend)
for pool_row in ip_pool_rows:
pool = pool_row.pool
if pool.empty():
continue
else:
return True
def backend_public_networks(backend):
return Network.objects.filter(deleted=False, public=True,
backend_networks__backend=backend)
def get_vm_nic(vm, nic_id):
......
......@@ -31,12 +31,10 @@ import logging
import datetime
from django.utils import importlib
from synnefo.settings import (BACKEND_ALLOCATOR_MODULE, BACKEND_REFRESH_MIN,
BACKEND_PER_USER,
DEFAULT_INSTANCE_NETWORKS)
from django.conf import settings
from synnefo.db.models import Backend
from synnefo.logic.backend import update_backend_resources
from synnefo.api.util import backend_public_networks
from synnefo.logic import backend as backend_mod
from synnefo.api import util
log = logging.getLogger(__name__)
......@@ -47,7 +45,7 @@ class BackendAllocator():
"""
def __init__(self):
self.strategy_mod =\
importlib.import_module(BACKEND_ALLOCATOR_MODULE)
importlib.import_module(settings.BACKEND_ALLOCATOR_MODULE)
def allocate(self, userid, flavor):
"""Allocate a vm of the specified flavor to a backend.
......@@ -114,20 +112,12 @@ def get_available_backends(flavor):
backends = backends.filter(offline=False, drained=False,
disk_templates__contains=disk_template)
backends = list(backends)
if "SNF:ANY_PUBLIC" in DEFAULT_INSTANCE_NETWORKS:
backends = filter(lambda x: has_free_ip(x), backends)
if "SNF:ANY_PUBLIC" in settings.DEFAULT_INSTANCE_NETWORKS:
backends = filter(lambda x: util.backend_has_free_public_ip(x),
backends)
return backends
def has_free_ip(backend):
"""Find if Backend has any free public IP."""
for network in backend_public_networks(backend):
if not network.get_pool().empty():
return True
log.warning("No available network in backend %r", backend)
return False
def flavor_disk(flavor):
""" Get flavor's 'real' disk size
......@@ -164,18 +154,18 @@ def refresh_backends_stats(backends):
"""
now = datetime.datetime.now()
delta = datetime.timedelta(minutes=BACKEND_REFRESH_MIN)
delta = datetime.timedelta(minutes=settings.BACKEND_REFRESH_MIN)
for b in backends:
if now > b.updated + delta:
log.debug("Updating resources of backend %r. Last Updated %r",
b, b.updated)
update_backend_resources(b)
backend_mod.update_backend_resources(b)
def get_backend_for_user(userid):
"""Find fixed Backend for user based on BACKEND_PER_USER setting."""
backend = BACKEND_PER_USER.get(userid)
backend = settings.BACKEND_PER_USER.get(userid)
if not backend:
return None
......
......@@ -12,9 +12,9 @@ from synnefo import quotas
from synnefo.api import util
from synnefo.logic import backend
from synnefo.logic.backend_allocator import BackendAllocator
from synnefo.db.models import (NetworkInterface, VirtualMachine, Network,
VirtualMachineMetadata, IPAddress, Subnet)
from synnefo.db import query as db_query
from synnefo.db.models import (NetworkInterface, VirtualMachine,
VirtualMachineMetadata, IPAddress)
from synnefo.db import query as db_query, pools
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
......@@ -241,53 +241,79 @@ def create_instance_nics(vm, userid, private_networks=[], floating_ips=[]):
networks defined by the user.
"""
attachments = []
nics = []
for network_id in settings.DEFAULT_INSTANCE_NETWORKS:
network, ipaddress = None, None
if network_id == "SNF:ANY_PUBLIC":
ipaddress = util.allocate_public_address(backend=vm.backend,
userid=userid)
network = ipaddress.network
ipaddress = util.allocate_public_ip(userid=userid,
backend=vm.backend)
nic, ipaddress = create_nic(vm, ipaddress=ipaddress)
else:
try:
network = Network.objects.get(id=network_id, deleted=False)
except Network.DoesNotExist:
network = util.get_network(network_id, userid,
non_deleted=True)
except faults.ItemNotFound:
msg = "Invalid configuration. Setting"\
" 'DEFAULT_INSTANCE_NETWORKS' contains invalid"\
" network '%s'" % network_id
log.error(msg)
raise Exception(msg)
try:
subnet = network.subnets.get(ipversion=4, dhcp=True)
ipaddress = util.get_network_free_address(subnet, userid)
except Subnet.DoesNotExist:
ipaddress = None
attachments.append((network, ipaddress))
raise faults.InternalServerError(msg)
nic, ipaddress = create_nic(vm, network=network)
nics.append(nic)
for address in floating_ips:
floating_ip = get_floating_ip(userid=vm.userid, address=address)
attachments.append((floating_ip.network, floating_ip))
floating_ip = util.get_floating_ip_by_address(vm.userid, address,
for_update=True)
nic, ipaddress = create_nic(vm, ipaddress=floating_ip)
nics.append(nic)
for network_id in private_networks:
network = util.get_network(network_id, userid, non_deleted=True)
if network.public:
raise faults.Forbidden("Can not connect to public network")
attachments.append((network, ipaddress))
nics = []
for index, (network, ipaddress) in enumerate(attachments):
# Create VM's public NIC. Do not wait notification form ganeti
# hooks to create this NIC, because if the hooks never run (e.g.
# building error) the VM's public IP address will never be
# released!
nic = NetworkInterface.objects.create(userid=userid, machine=vm,
network=network, index=index,
state="BUILDING")
if ipaddress is not None:
ipaddress.nic = nic
ipaddress.save()
nic, ipaddress = create_nic(vm, network=network)
nics.append(nic)
for index, nic in enumerate(nics):
nic.index = index
nic.save()
return nics
def create_nic(vm, network=None, ipaddress=None, address=None):
"""Helper functions for create NIC objects.
Create a NetworkInterface connecting a VirtualMachine to a network with the
IPAddress specified. If no 'ipaddress' is passed and the network has an
IPv4 subnet, then an IPv4 address will be automatically be allocated.
"""
userid = vm.userid
if ipaddress is None:
if network.subnets.filter(ipversion=4).exists():
try:
ipaddress = util.allocate_ip(network, userid=userid,
address=address)
except pools.ValueNotAvailable:
raise faults.badRequest("Address '%s' is not available." %
address)
if ipaddress is not None and ipaddress.nic is not None:
raise faults.Conflict("IP address '%s' already in use" %
ipaddress.address)
if network is None:
network = ipaddress.network
elif network.state != 'ACTIVE':
# TODO: What if is in settings ?
raise faults.BuildInProgress('Network not active yet')
nic = NetworkInterface.objects.create(machine=vm, network=network,
state="BUILDING")
if ipaddress is not None:
ipaddress.nic = nic
ipaddress.save()
return nic, ipaddress
@server_command("DESTROY")
def destroy(vm):
log.info("Deleting VM %s", vm)
......@@ -353,22 +379,9 @@ def set_firewall_profile(vm, profile, nic):
@server_command("CONNECT")
def connect(vm, network):
if network.state != 'ACTIVE':
raise faults.BuildInProgress('Network not active yet')
nic, ipaddress = create_nic(vm, network)
address = None
try:
subnet = network.subnets.get(ipversion=4, dhcp=True)
address = util.get_network_free_address(subnet, userid=vm.userid)
except Subnet.DoesNotExist:
subnet = None
nic = NetworkInterface.objects.create(machine=vm, network=network,
state="BUILDING")
if address is not None:
address.nic = nic
address.save()
log.info("Connecting VM %s to Network %s. NIC: %s", vm, network, nic)
log.info("Creating NIC %s with IPAddress %s", nic, ipaddress)
return backend.connect_to_network(vm, nic)
......@@ -437,41 +450,15 @@ def console(vm, console_type):
@server_command("CONNECT")
def add_floating_ip(vm, address):
floating_ip = get_floating_ip(userid=vm.userid, address=address)
nic = NetworkInterface.objects.create(machine=vm,
network=floating_ip.network,
ipv4=floating_ip.ipv4,
ip_type="FLOATING",
state="BUILDING")
log.info("Connecting VM %s to floating IP %s. NIC: %s", vm, floating_ip,
nic)
# Use for_update, to guarantee that floating IP will only by assigned once
# and that it can not be released will it is being attached!
floating_ip = util.get_floating_ip_by_address(vm.userid, address,
for_update=True)
nic, floating_ip = create_nic(vm, ipaddress=floating_ip)
log.info("Created NIC %s with floating IP %s", nic, floating_ip)
return backend.connect_to_network(vm, nic)
def get_floating_ip(userid, address):
"""Get a floating IP by it's address.
Helper function for looking up a IPAddress by it's address. This function
also checks if the floating IP is currently used by any instance.
"""
try:
# Get lock in VM, to guarantee that floating IP will only by assigned
# once
floating_ip = db_query.get_user_floating_ip(userid=userid,
address=address,
for_update=True)
except IPAddress.DoesNotExist:
raise faults.ItemNotFound("Floating IP with address '%s' does not"
" exist" % address)
if floating_ip.nic is not None:
raise faults.Conflict("Floating IP '%s' already in use" %
floating_ip.id)
return floating_ip
@server_command("DISCONNECT")
def remove_floating_ip(vm, address):
try:
......
......@@ -61,7 +61,8 @@ class ServerCreationTest(TransactionTestCase):
self.assertEqual(models.VirtualMachine.objects.count(), 0)
subnet = mfactory.IPv4SubnetFactory(network__public=True)
mfactory.BackendNetworkFactory(network=subnet.network)
bn = mfactory.BackendNetworkFactory(network=subnet.network)
backend = bn.backend
# error in nics
req = deepcopy(kwargs)
......@@ -94,9 +95,10 @@ class ServerCreationTest(TransactionTestCase):
self.assertEqual(vm.task, "BUILD")
# test connect in IPv6 only network
subnet = mfactory.IPv6SubnetFactory()
subnet = mfactory.IPv6SubnetFactory(network__public=True)
net = subnet.network
mfactory.BackendNetworkFactory(network=net)
mfactory.BackendNetworkFactory(network=net, backend=backend,
operstate="ACTIVE")
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=[str(net.id)]):
with mocked_quotaholder():
......@@ -114,42 +116,25 @@ class ServerCreationTest(TransactionTestCase):
class ServerTest(TransactionTestCase):
def test_connect_network(self, mrapi):
# Common connect
subnet = mfactory.IPv4SubnetFactory(network__flavor="CUSTOM",
cidr="192.168.2.0/24",
gateway="192.168.2.1",
dhcp=True)