Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10.4 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, or the last. In
119
        # that case 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
            elif int(cidr_ip.broadcast) - int(gateway_ip) == 1:
124
                allocation_pools = [(cidr_ip.network + 1, gateway_ip - 1)]
125
            else:
126
                # If the gateway isn't the first available ip, create two
127
                # different ip pools adjacent to said ip
128
                allocation_pools = [(cidr_ip.network + 1, gateway_ip - 1),
129
                                    (gateway_ip + 1, cidr_ip.broadcast - 1)]
130
        else:
131
            allocation_pools = [(cidr_ip.network + 1, cidr_ip.broadcast - 1)]
132

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

    
138
    return sub
139

    
140

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

    
149
    return subnet
150

    
151

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

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

    
159

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

165
    """
166
    log.info('Update subnet %s, name %s' % (sub_id, name))
167

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

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

    
176
    check_name_length(name)
177

    
178
    subnet.name = name
179
    subnet.save()
180

    
181
    return subnet
182

    
183

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

    
189

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

    
197

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

    
204

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

    
211

    
212
def validate_pools(pool_list, cidr, gateway):
213
    """Validate IP Pools
214

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

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

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

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

    
244

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

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

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

    
289

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

    
300
    return alloc