Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10 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
    user_subnets = Subnet.objects.filter((Q(network__userid=user_id) &
65
                                          Q(network__public=False)) |
66
                                         Q(network__public=True))
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
    # Returns the first available IP in the subnet
97
    try:
98
        cidr_ip = ipaddr.IPNetwork(cidr)
99
    except ValueError:
100
        raise api.faults.BadRequest("Malformed CIDR")
101

    
102
    if ipversion == 6:
103
        validate_subnet_params(None, None, cidr, gateway)
104
    else:
105
        validate_subnet_params(cidr, gateway)
106

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

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

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

    
134
    if allocation_pools:
135
        create_ip_pools(allocation_pools, cidr_ip, sub)
136

    
137
    return sub
138

    
139

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

    
148
    return subnet
149

    
150

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

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

    
158

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

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

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

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

    
175
    check_name_length(name)
176

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

    
180
    return subnet
181

    
182

    
183
#Utility functions
184
def create_ip_pools(pools, cidr, subnet):
185
    """Create IP Pools in the database"""
186
    ip_pools = []
187
    for pool in pools:
188
        size = int(pool[1]) - int(pool[0]) + 1
189
        base = str(cidr)
190
        offset = int(pool[0]) - int(cidr.network)
191
        ip_pool = IPPoolTable.objects.create(size=size, offset=offset,
192
                                             base=base, subnet=subnet)
193
        ip_pools.append(ip_pool)
194
    return ip_pools
195

    
196

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

    
203

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

    
210

    
211
def validate_subpools(pool_list, cidr, gateway):
212
    """Validate IP Pools
213

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

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

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

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

    
243

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

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

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