Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / subnets.py @ 91430ce0

History | View | Annotate | Download (10.2 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

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

    
47
log = getLogger(__name__)
48

    
49

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

    
59

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

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

    
69

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

    
74

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

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

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

    
88
    if user_id != network.userid:
89
        raise api.faults.Unauthorized("Unauthorized operation")
90

    
91
    if ipversion not in [4, 6]:
92
        raise api.faults.BadRequest("Malformed IP version type")
93

    
94
    check_number_of_subnets(network, ipversion)
95

    
96
    try:
97
        cidr_ip = ipaddr.IPNetwork(cidr)
98
    except ValueError:
99
        raise api.faults.BadRequest("Malformed CIDR")
100

    
101
    if ipversion == 6:
102
        validate_subnet_params(subnet6=cidr, gateway6=gateway)
103
    else:
104
        validate_subnet_params(subnet=cidr, gateway=gateway)
105

    
106
    name = check_name_length(name)
107
    sub = Subnet.objects.create(name=name, network=network, cidr=cidr,
108
                                ipversion=ipversion, gateway=gateway,
109
                                dhcp=dhcp, host_routes=host_routes,
110
                                dns_nameservers=dns_nameservers)
111

    
112
    gateway_ip = ipaddr.IPAddress(gateway) if gateway else None
113

    
114
    if allocation_pools is not None:
115
        if ipversion == 6:
116
            raise api.faults.Conflict("Can't allocate an IP Pool in IPv6")
117
    elif ipversion == 4:
118
        # Check if the gateway is the first IP of the subnet, in this case
119
        # create a single ip pool
120
        if gateway_ip:
121
            if int(gateway_ip) - int(cidr_ip) == 1:
122
                allocation_pools = [(gateway_ip + 1, cidr_ip.broadcast - 1)]
123
            else:
124
                # If the gateway isn't the first available ip, create two
125
                # different ip pools adjacent to said ip
126
                allocation_pools = [(cidr_ip.network + 1, gateway_ip - 1),
127
                                    (gateway_ip + 1, cidr_ip.broadcast - 1)]
128
        else:
129
            allocation_pools = [(cidr_ip.network + 1, cidr_ip.broadcast - 1)]
130

    
131
    if allocation_pools:
132
        # Validate the allocation pools
133
        validate_pools(allocation_pools, cidr_ip, gateway_ip)
134
        create_ip_pools(allocation_pools, cidr_ip, sub)
135

    
136
    return sub
137

    
138

    
139
def get_subnet(sub_id):
140
    """Show info of a specific subnet"""
141
    log.debug('get_subnet %s', sub_id)
142
    try:
143
        subnet = Subnet.objects.get(id=sub_id)
144
    except Subnet.DoesNotExist:
145
        raise api.faults.ItemNotFound("Subnet not found")
146

    
147
    return subnet
148

    
149

    
150
def delete_subnet():
151
    """Delete a subnet, raises BadRequest
152
    A subnet is deleted ONLY when the network that it belongs to is deleted
153

154
    """
155
    raise api.faults.BadRequest("Deletion of a subnet is not supported")
156

    
157

    
158
@transaction.commit_on_success
159
def update_subnet(sub_id, name, user_id):
160
    """Update the fields of a subnet
161
    Only the name can be updated
162

163
    """
164
    log.info('Update subnet %s, name %s' % (sub_id, name))
165

    
166
    try:
167
        subnet = Subnet.objects.get(id=sub_id)
168
    except:
169
        raise api.faults.ItemNotFound("Subnet not found")
170

    
171
    if user_id != subnet.network.userid:
172
        raise api.faults.Unauthorized("Unauthorized operation")
173

    
174
    check_name_length(name)
175

    
176
    subnet.name = name
177
    subnet.save()
178

    
179
    return subnet
180

    
181

    
182
#Utility functions
183
def create_ip_pools(pools, cidr, subnet):
184
    """Create IP Pools in the database"""
185
    return [_create_ip_pool(pool, cidr, subnet) for pool in pools]
186

    
187

    
188
def _create_ip_pool(pool, cidr, subnet):
189
    size = int(pool[1]) - int(pool[0]) + 1
190
    base = str(cidr)
191
    offset = int(pool[0]) - int(cidr.network)
192
    return IPPoolTable.objects.create(size=size, offset=offset,
193
                                      base=base, subnet=subnet)
194

    
195

    
196
def check_number_of_subnets(network, version):
197
    """Check if a user can add a subnet in a network"""
198
    if network.subnets.filter(ipversion=version):
199
        raise api.faults.BadRequest("Only one subnet of IPv4/IPv6 per "
200
                                    "network is allowed")
201

    
202

    
203
def check_name_length(name):
204
    """Check if the length of a name is within acceptable value"""
205
    if len(str(name)) > Subnet.SUBNET_NAME_LENGTH:
206
        raise api.faults.BadRequest("Subnet name too long")
207
    return name
208

    
209

    
210
def validate_pools(pool_list, cidr, gateway):
211
    """Validate IP Pools
212

213
    Validate the given IP pools are inside the cidr range
214
    Validate there are no overlaps in the given pools
215
    Finally, validate the gateway isn't in the given ip pools
216
    Input must be a list containing a sublist with start/end ranges as
217
    ipaddr.IPAddress items eg.,
218
    [[IPv4Address('192.168.42.11'), IPv4Address('192.168.42.15')],
219
     [IPv4Address('192.168.42.30'), IPv4Address('192.168.42.60')]]
220

221
    """
222
    if pool_list[0][0] <= cidr.network:
223
        raise api.faults.Conflict("IP Pool out of bounds")
224
    elif pool_list[-1][1] >= cidr.broadcast:
225
        raise api.faults.Conflict("IP Pool out of bounds")
226

    
227
    for start, end in pool_list:
228
        if start > end:
229
            raise api.faults.Conflict("Invalid IP pool range")
230
        # Raise BadRequest if gateway is inside the pool range
231
        if gateway:
232
            if not (gateway < start or gateway > end):
233
                raise api.faults.Conflict("Gateway cannot be in pool range")
234

    
235
    # Check if there is a conflict between the IP Poll ranges
236
    end = cidr.network
237
    for pool in pool_list:
238
        if end >= pool[0]:
239
            raise api.faults.Conflict("IP Pool range conflict")
240
        end = pool[1]
241

    
242

    
243
def validate_subnet_params(subnet=None, gateway=None, subnet6=None,
244
                           gateway6=None):
245
    if subnet:
246
        try:
247
            # Use strict option to not all subnets with host bits set
248
            network = ipaddr.IPv4Network(subnet, strict=True)
249
        except ValueError:
250
            raise faults.BadRequest("Invalid network IPv4 subnet")
251

    
252
        # Check that network size is allowed!
253
        prefixlen = network.prefixlen
254
        if prefixlen > 29 or prefixlen <= settings.MAX_CIDR_BLOCK:
255
            raise faults.OverLimit(
256
                message="Unsupported network size",
257
                details="Netmask must be in range: (%s, 29]" %
258
                settings.MAX_CIDR_BLOCK)
259
        if gateway:  # Check that gateway belongs to network
260
            try:
261
                gateway = ipaddr.IPv4Address(gateway)
262
            except ValueError:
263
                raise faults.BadRequest("Invalid network IPv4 gateway")
264
            if not gateway in network:
265
                raise faults.BadRequest("Invalid network IPv4 gateway")
266

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

    
287

    
288
def parse_allocation_pools(allocation_pools):
289
    alloc = list()
290
    for pool in allocation_pools:
291
        try:
292
            start, end = pool.split(',')
293
            alloc.append([ipaddr.IPv4Address(start),
294
                          ipaddr.IPv4Address(end)])
295
        except ValueError:
296
            raise faults.BadRequest("Malformed IPv4 address")
297

    
298
    return alloc