Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / subnets.py @ 883c1f94

History | View | Annotate | Download (10.9 kB)

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

    
34
import ipaddr
35
from logging import getLogger
36
from functools import wraps
37

    
38
from django.conf import settings
39
from django.db import transaction
40
from django.db.models import Q
41

    
42
from snf_django.lib import api
43
from snf_django.lib.api import faults
44
from synnefo.logic import utils
45

    
46
from synnefo.db.models import Subnet, Network, IPPoolTable
47

    
48
log = getLogger(__name__)
49

    
50

    
51
def subnet_command(action):
52
    def decorator(func):
53
        @wraps(func)
54
        @transaction.commit_on_success()
55
        def wrapper(subnet, *args, **kwargs):
56
            return func(subnet, *args, **kwargs)
57
        return wrapper
58
    return decorator
59

    
60

    
61
def list_subnets(user_id):
62
    """List all subnets of a user"""
63
    log.debug('list_subnets %s', user_id)
64

    
65
    query = (((Q(network__userid=user_id) & Q(network__public=False)) |
66
              Q(network__public=True)) & Q(deleted=False))
67
    user_subnets = Subnet.objects.filter(query)
68
    return user_subnets
69

    
70

    
71
@transaction.commit_on_success
72
def create_subnet(*args, **kwargs):
73
    return _create_subnet(*args, **kwargs)
74

    
75

    
76
def _create_subnet(network_id, user_id, cidr, name, ipversion=4, gateway=None,
77
                   dhcp=True, slaac=True, dns_nameservers=None,
78
                   allocation_pools=None, host_routes=None):
79
    """Create a subnet
80

81
    network_id and the desired cidr are mandatory, everything else is optional
82

83
    """
84
    try:
85
        network = Network.objects.get(id=network_id)
86
    except Network.DoesNotExist:
87
        raise api.faults.ItemNotFound("No network found with that id")
88

    
89
    if network.deleted:
90
        raise api.faults.BadRequest("Network has been deleted")
91

    
92
    if user_id != network.userid:
93
        raise api.faults.Unauthorized("Unauthorized operation")
94

    
95
    if ipversion not in [4, 6]:
96
        raise api.faults.BadRequest("Malformed IP version type")
97

    
98
    check_number_of_subnets(network, ipversion)
99

    
100
    if network.backend_networks.exists():
101
        raise api.faults.BadRequest("Cannot create subnet in network %s, VMs"
102
                                    " are already connected to this network" %
103
                                    network_id)
104

    
105
    try:
106
        cidr_ip = ipaddr.IPNetwork(cidr)
107
    except ValueError:
108
        raise api.faults.BadRequest("Malformed CIDR")
109

    
110
    if ipversion == 6:
111
        validate_subnet_params(subnet6=cidr, gateway6=gateway)
112
    else:
113
        validate_subnet_params(subnet=cidr, gateway=gateway)
114

    
115
    name = utils.check_name_length(name, Subnet.SUBNET_NAME_LENGTH, "Subnet "
116
                                   "name is too long")
117
    sub = Subnet.objects.create(name=name, network=network, cidr=cidr,
118
                                ipversion=ipversion, gateway=gateway,
119
                                dhcp=dhcp, host_routes=host_routes,
120
                                dns_nameservers=dns_nameservers)
121

    
122
    gateway_ip = ipaddr.IPAddress(gateway) if gateway else None
123

    
124
    if allocation_pools is not None:
125
        if ipversion == 6:
126
            raise api.faults.Conflict("Can't allocate an IP Pool in IPv6")
127
    elif ipversion == 4:
128
        # Check if the gateway is the first IP of the subnet, or the last. In
129
        # that case create a single ip pool.
130
        if gateway_ip:
131
            if int(gateway_ip) - int(cidr_ip) == 1:
132
                allocation_pools = [(gateway_ip + 1, cidr_ip.broadcast - 1)]
133
            elif int(cidr_ip.broadcast) - int(gateway_ip) == 1:
134
                allocation_pools = [(cidr_ip.network + 1, gateway_ip - 1)]
135
            else:
136
                # If the gateway isn't the first available ip, create two
137
                # different ip pools adjacent to said ip
138
                allocation_pools = [(cidr_ip.network + 1, gateway_ip - 1),
139
                                    (gateway_ip + 1, cidr_ip.broadcast - 1)]
140
        else:
141
            allocation_pools = [(cidr_ip.network + 1, cidr_ip.broadcast - 1)]
142

    
143
    if allocation_pools:
144
        # Validate the allocation pools
145
        validate_pools(allocation_pools, cidr_ip, gateway_ip)
146
        create_ip_pools(allocation_pools, cidr_ip, sub)
147

    
148
    return sub
149

    
150

    
151
def get_subnet(sub_id, prefetch_related=False):
152
    """Show info of a specific subnet"""
153
    log.debug('get_subnet %s', sub_id)
154
    try:
155
        subnets = Subnet.objects
156
        if prefetch_related:
157
            subnets = subnets.select_related("network")
158
            subnets = subnets.prefetch_related("ip_pools")
159
        return subnets.get(id=sub_id)
160
    except Subnet.DoesNotExist:
161
        raise api.faults.ItemNotFound("Subnet not found")
162

    
163

    
164
def delete_subnet():
165
    """Delete a subnet, raises BadRequest
166
    A subnet is deleted ONLY when the network that it belongs to is deleted
167

168
    """
169
    raise api.faults.BadRequest("Deletion of a subnet is not supported")
170

    
171

    
172
@transaction.commit_on_success
173
def update_subnet(sub_id, name, user_id):
174
    """Update the fields of a subnet
175
    Only the name can be updated
176

177
    """
178
    log.info('Update subnet %s, name %s' % (sub_id, name))
179

    
180
    try:
181
        subnet = Subnet.objects.get(id=sub_id)
182
    except:
183
        raise api.faults.ItemNotFound("Subnet not found")
184

    
185
    if user_id != subnet.network.userid:
186
        raise api.faults.Unauthorized("Unauthorized operation")
187

    
188
    utils.check_name_length(name, Subnet.SUBNET_NAME_LENGTH, "Subnet name is "
189
                            " too long")
190

    
191
    subnet.name = name
192
    subnet.save()
193

    
194
    return subnet
195

    
196

    
197
#Utility functions
198
def create_ip_pools(pools, cidr, subnet):
199
    """Create IP Pools in the database"""
200
    return [_create_ip_pool(pool, cidr, subnet) for pool in pools]
201

    
202

    
203
def _create_ip_pool(pool, cidr, subnet):
204
    size = int(pool[1]) - int(pool[0]) + 1
205
    base = str(cidr)
206
    offset = int(pool[0]) - int(cidr.network)
207
    return IPPoolTable.objects.create(size=size, offset=offset,
208
                                      base=base, subnet=subnet)
209

    
210

    
211
def check_number_of_subnets(network, version):
212
    """Check if a user can add a subnet in a network"""
213
    if network.subnets.filter(ipversion=version):
214
        raise api.faults.BadRequest("Only one subnet of IPv4/IPv6 per "
215
                                    "network is allowed")
216

    
217

    
218
def validate_pools(pool_list, cidr, gateway):
219
    """Validate IP Pools
220

221
    Validate the given IP pools are inside the cidr range
222
    Validate there are no overlaps in the given pools
223
    Finally, validate the gateway isn't in the given ip pools
224
    Input must be a list containing a sublist with start/end ranges as
225
    ipaddr.IPAddress items eg.,
226
    [[IPv4Address('192.168.42.11'), IPv4Address('192.168.42.15')],
227
     [IPv4Address('192.168.42.30'), IPv4Address('192.168.42.60')]]
228

229
    """
230
    if pool_list[0][0] <= cidr.network:
231
        raise api.faults.Conflict("IP Pool out of bounds")
232
    elif pool_list[-1][1] >= cidr.broadcast:
233
        raise api.faults.Conflict("IP Pool out of bounds")
234

    
235
    for start, end in pool_list:
236
        if start > end:
237
            raise api.faults.Conflict("Invalid IP pool range")
238
        # Raise BadRequest if gateway is inside the pool range
239
        if gateway:
240
            if not (gateway < start or gateway > end):
241
                raise api.faults.Conflict("Gateway cannot be in pool range")
242

    
243
    # Check if there is a conflict between the IP Pool ranges
244
    end = cidr.network
245
    for pool in pool_list:
246
        if end >= pool[0]:
247
            raise api.faults.Conflict("IP Pool range conflict")
248
        end = pool[1]
249

    
250

    
251
def validate_subnet_params(subnet=None, gateway=None, subnet6=None,
252
                           gateway6=None):
253
    if subnet:
254
        try:
255
            # Use strict option to not all subnets with host bits set
256
            network = ipaddr.IPv4Network(subnet, strict=True)
257
        except ValueError:
258
            raise faults.BadRequest("Invalid network IPv4 subnet")
259

    
260
        # Check that network size is allowed!
261
        prefixlen = network.prefixlen
262
        if prefixlen > 29 or prefixlen <= settings.MAX_CIDR_BLOCK:
263
            raise faults.OverLimit(
264
                message="Unsupported network size",
265
                details="Netmask must be in range: (%s, 29]" %
266
                settings.MAX_CIDR_BLOCK)
267
        if gateway:  # Check that gateway belongs to network
268
            try:
269
                gateway = ipaddr.IPv4Address(gateway)
270
            except ValueError:
271
                raise faults.BadRequest("Invalid network IPv4 gateway")
272
            if not gateway in network:
273
                raise faults.BadRequest("Invalid network IPv4 gateway")
274

    
275
    if subnet6:
276
        try:
277
            # Use strict option to not all subnets with host bits set
278
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
279
        except ValueError:
280
            raise faults.BadRequest("Invalid network IPv6 subnet")
281
        # Check that network6 is an /64 subnet, because this is imposed by
282
        # 'mac2eui64' utiity.
283
        if network6.prefixlen != 64:
284
            msg = ("Unsupported IPv6 subnet size. Network netmask must be"
285
                   " /64")
286
            raise faults.BadRequest(msg)
287
        if gateway6:
288
            try:
289
                gateway6 = ipaddr.IPv6Address(gateway6)
290
            except ValueError:
291
                raise faults.BadRequest("Invalid network IPv6 gateway")
292
            if not gateway6 in network6:
293
                raise faults.BadRequest("Invalid network IPv6 gateway")
294

    
295

    
296
def parse_allocation_pools(allocation_pools):
297
    alloc = list()
298
    for pool in allocation_pools:
299
        try:
300
            start, end = pool.split(',')
301
            alloc.append([ipaddr.IPv4Address(start),
302
                          ipaddr.IPv4Address(end)])
303
        except ValueError:
304
            raise faults.BadRequest("Malformed IPv4 address")
305

    
306
    return alloc