Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / subnets.py @ 4e3789fd

History | View | Annotate | Download (11.1 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
from synnefo.api import util
46

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

    
49
log = getLogger(__name__)
50

    
51

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

    
61

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

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

    
71

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

    
76

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

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

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

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

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

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

    
99
    check_number_of_subnets(network, ipversion)
100

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

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

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

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

    
124
    network.subnet_ids.append(sub.id)
125
    network.save()
126

    
127
    gateway_ip = ipaddr.IPAddress(gateway) if gateway else None
128

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

    
148
    if allocation_pools:
149
        # Validate the allocation pools
150
        validate_pools(allocation_pools, cidr_ip, gateway_ip)
151
        create_ip_pools(allocation_pools, cidr_ip, sub)
152

    
153
    return sub
154

    
155

    
156
def get_subnet(subnet_id, user_id, for_update=False):
157
    """Return a Subnet instance or raise ItemNotFound."""
158

    
159
    try:
160
        objects = Subnet.objects
161
        subnet_id = int(subnet_id)
162
        return objects.get(Q(userid=user_id) | Q(public=True),
163
                           id=subnet_id)
164
    except (ValueError, TypeError):
165
        raise faults.BadRequest("Invalid subnet ID '%s'" % subnet_id)
166
    except Subnet.DoesNotExist:
167
        raise faults.ItemNotFound("Subnet '%s' not found." % subnet_id)
168

    
169

    
170
def delete_subnet():
171
    """Delete a subnet, raises BadRequest
172
    A subnet is deleted ONLY when the network that it belongs to is deleted
173

174
    """
175
    raise api.faults.BadRequest("Deletion of a subnet is not supported")
176

    
177

    
178
@transaction.commit_on_success
179
def update_subnet(sub_id, name, user_id):
180
    """Update the fields of a subnet
181
    Only the name can be updated
182

183
    """
184
    log.info('Update subnet %s, name %s' % (sub_id, name))
185

    
186
    try:
187
        subnet = Subnet.objects.get(id=sub_id)
188
    except:
189
        raise api.faults.ItemNotFound("Subnet not found")
190

    
191
    if user_id != subnet.network.userid:
192
        raise api.faults.Unauthorized("Unauthorized operation")
193

    
194
    utils.check_name_length(name, Subnet.SUBNET_NAME_LENGTH, "Subnet name is "
195
                            " too long")
196

    
197
    subnet.name = name
198
    subnet.save()
199

    
200
    return subnet
201

    
202

    
203
#Utility functions
204
def create_ip_pools(pools, cidr, subnet):
205
    """Create IP Pools in the database"""
206
    return [_create_ip_pool(pool, cidr, subnet) for pool in pools]
207

    
208

    
209
def _create_ip_pool(pool, cidr, subnet):
210
    size = int(pool[1]) - int(pool[0]) + 1
211
    base = str(cidr)
212
    offset = int(pool[0]) - int(cidr.network)
213
    return IPPoolTable.objects.create(size=size, offset=offset,
214
                                      base=base, subnet=subnet)
215

    
216

    
217
def check_number_of_subnets(network, version):
218
    """Check if a user can add a subnet in a network"""
219
    if network.subnets.filter(ipversion=version):
220
        raise api.faults.BadRequest("Only one subnet of IPv4/IPv6 per "
221
                                    "network is allowed")
222

    
223

    
224
def validate_pools(pool_list, cidr, gateway):
225
    """Validate IP Pools
226

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

235
    """
236
    if pool_list[0][0] <= cidr.network:
237
        raise api.faults.Conflict("IP Pool out of bounds")
238
    elif pool_list[-1][1] >= cidr.broadcast:
239
        raise api.faults.Conflict("IP Pool out of bounds")
240

    
241
    for start, end in pool_list:
242
        if start > end:
243
            raise api.faults.Conflict("Invalid IP pool range")
244
        # Raise BadRequest if gateway is inside the pool range
245
        if gateway:
246
            if not (gateway < start or gateway > end):
247
                raise api.faults.Conflict("Gateway cannot be in pool range")
248

    
249
    # Check if there is a conflict between the IP Pool ranges
250
    end = cidr.network
251
    for pool in pool_list:
252
        if end >= pool[0]:
253
            raise api.faults.Conflict("IP Pool range conflict")
254
        end = pool[1]
255

    
256

    
257
def validate_subnet_params(subnet=None, gateway=None, subnet6=None,
258
                           gateway6=None):
259
    if subnet:
260
        try:
261
            # Use strict option to not all subnets with host bits set
262
            network = ipaddr.IPv4Network(subnet, strict=True)
263
        except ValueError:
264
            raise faults.BadRequest("Invalid network IPv4 subnet")
265

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

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

    
301

    
302
def parse_allocation_pools(allocation_pools):
303
    alloc = list()
304
    for pool in allocation_pools:
305
        try:
306
            start, end = pool.split(',')
307
            alloc.append([ipaddr.IPv4Address(start),
308
                          ipaddr.IPv4Address(end)])
309
        except ValueError:
310
            raise faults.BadRequest("Malformed IPv4 address")
311

    
312
    return alloc