ips.py 9.09 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
# Copyright 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

34 35 36 37 38 39
import logging

from snf_django.lib.api import faults
from django.db import transaction
from synnefo import quotas
from synnefo.db import pools
40
from synnefo.db.models import (IPPoolTable, IPAddress, Network)
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
log = logging.getLogger(__name__)


def allocate_ip_from_pools(pool_rows, userid, address=None, floating_ip=False):
    """Try to allocate a value from a number of pools.

    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.
    If an address is specified and does not belong to any of the pools,
    InvalidValue is raised.

    """
    for pool_row in pool_rows:
        pool = pool_row.pool
        try:
            value = pool.get(value=address)
            pool.save()
            subnet = pool_row.subnet
            ipaddress = IPAddress.objects.create(subnet=subnet,
                                                 network=subnet.network,
                                                 userid=userid,
                                                 address=value,
63 64
                                                 floating_ip=floating_ip,
                                                 ipversion=4)
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
            return ipaddress
        except pools.EmptyPool:
            pass
        except pools.InvalidValue:
            pass
    if address is None:
        raise pools.EmptyPool("No more IP addresses available on pools %s" %
                              pool_rows)
    else:
        raise pools.InvalidValue("Address %s does not belong to pools %s" %
                                 (address, pool_rows))


def allocate_ip(network, userid, address=None, floating_ip=False):
    """Try to allocate an IP from networks IP pools."""
    if network.action == "DESTROY":
81
        raise faults.Conflict("Cannot allocate IP. Network %s is being"
82
                              " deleted" % network.id)
83 84 85 86
    elif network.drained:
        raise faults.Conflict("Can not allocate IP while network '%s' is in"
                              " 'SNF:DRAINED' status" % network.id)

87
    ip_pools = IPPoolTable.objects.select_for_update()\
88
        .filter(subnet__network=network).order_by('id')
89 90 91 92 93 94 95 96 97 98 99 100 101
    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)
    except pools.InvalidValue:
        raise faults.BadRequest("Address %s does not belong to network %s" %
                                (address, network.id))


102
def allocate_public_ip(userid, floating_ip=False, backend=None, networks=None):
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
    """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.

    """

    ip_pool_rows = IPPoolTable.objects.select_for_update()\
        .prefetch_related("subnet__network")\
        .filter(subnet__deleted=False)\
        .filter(subnet__network__deleted=False)\
        .filter(subnet__network__public=True)\
        .filter(subnet__network__drained=False)
119 120
    if networks is not None:
        ip_pool_rows = ip_pool_rows.filter(subnet__network__in=networks)
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
    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 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)
141
        exception_msg = "Cannot allocate a %s IP address." % ip_type
142
        raise faults.Conflict(exception_msg)
143 144 145


@transaction.commit_on_success
146
def create_floating_ip(userid, network=None, address=None, project=None):
147 148 149 150
    if network is None:
        floating_ip = allocate_public_ip(userid, floating_ip=True)
    else:
        if not network.floating_ip_pool:
151
            msg = ("Cannot allocate floating IP. Network %s is"
152 153 154
                   " not a floating IP pool.")
            raise faults.Conflict(msg % network.id)
        if network.action == "DESTROY":
155
            msg = "Cannot allocate floating IP. Network %s is being deleted."
156 157 158 159 160 161
            raise faults.Conflict(msg % network.id)

        # Allocate the floating IP
        floating_ip = allocate_ip(network, userid, address=address,
                                  floating_ip=True)

162 163 164 165
    if project is None:
        project = userid
    floating_ip.project = project
    floating_ip.save()
166 167 168 169 170 171 172 173 174
    # Issue commission (quotas)
    quotas.issue_and_accept_commission(floating_ip)
    transaction.commit()

    log.info("Created floating IP '%s' for user IP '%s'", floating_ip, userid)

    return floating_ip


175 176 177 178 179 180 181 182 183
def get_free_floating_ip(userid, network=None):
    """Get one of the free available floating IPs of the user.

    Get one of the users floating IPs that is not connected to any port
    or server. If network is specified, the floating IP must be from
    that network.

    """
    floating_ips = IPAddress.objects\
184 185
                            .filter(userid=userid, deleted=False, nic=None,
                                    floating_ip=True)
186 187 188 189 190 191 192 193 194
    if network is not None:
        floating_ips = floating_ips.filter(network=network)

    for floating_ip in floating_ips:
        floating_ip = IPAddress.objects.select_for_update()\
                                       .get(id=floating_ip.id)
        if floating_ip.nic is None:
            return floating_ip

195
    msg = "Cannot find an unused floating IP to connect server to"
196 197 198 199
    if network is not None:
        msg += " network '%s'." % network.id
    else:
        msg += " a public network."
200
    msg += " Please create a floating IP."
201 202 203
    raise faults.Conflict(msg)


204 205 206 207 208
@transaction.commit_on_success
def delete_floating_ip(floating_ip):
    if floating_ip.nic:
        # This is safe, you also need for_update to attach floating IP to
        # instance.
209 210 211 212 213 214 215
        server = floating_ip.nic.machine
        if server is None:
            msg = ("Floating IP '%s' is used by port '%s'" %
                   (floating_ip.id, floating_ip.nic_id))
        else:
            msg = ("Floating IP '%s' is used by server '%s'" %
                   (floating_ip.id, floating_ip.nic.machine_id))
216 217
        raise faults.Conflict(msg)

218 219 220
    # Lock network to prevent deadlock
    Network.objects.select_for_update().get(id=floating_ip.network_id)

221 222 223 224 225 226
    # Return the address of the floating IP back to pool
    floating_ip.release_address()
    # And mark the floating IP as deleted
    floating_ip.deleted = True
    floating_ip.save()
    # Release quota for floating IP
227
    quotas.issue_and_accept_commission(floating_ip, action="DESTROY")
228 229 230 231 232
    transaction.commit()
    # Delete the floating IP from DB
    log.info("Deleted floating IP '%s' of user '%s", floating_ip,
             floating_ip.userid)
    floating_ip.delete()