Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (11 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 snf_django.lib import api
41
from snf_django.lib.api import faults
42

    
43
from synnefo.db.models import Subnet, Network, IPPoolTable
44

    
45
log = getLogger(__name__)
46

    
47

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

    
57

    
58
def list_subnets(user_id):
59
    """List all subnets of a user"""
60
    log.debug('list_subnets')
61

    
62
    user_subnets = Subnet.objects.filter(network__userid=user_id)
63
    return user_subnets
64

    
65

    
66
@transaction.commit_on_success
67
def create_subnet(*args, **kwargs):
68
    return _create_subnet(*args, **kwargs)
69

    
70

    
71
def _create_subnet(network_id, user_id, cidr, name, ipversion=4, gateway=None,
72
                   dhcp=True, slac=True, dns_nameservers=None,
73
                   allocation_pools=None, host_routes=None):
74
    """Create a subnet
75

76
    network_id and the desired cidr are mandatory, everything else is optional
77

78
    """
79
    try:
80
        network = Network.objects.get(id=network_id)
81
    except Network.DoesNotExist:
82
        raise api.faults.ItemNotFound("No networks found with that id")
83

    
84
    if user_id != network.userid:
85
        raise api.faults.Unauthorized("Unauthorized operation")
86

    
87
    if ipversion not in [4, 6]:
88
        raise api.faults.BadRequest("Malformed IP version type")
89

    
90
    check_number_of_subnets(network, ipversion)
91

    
92
    # Returns the first available IP in the subnet
93
    try:
94
        cidr_ip = ipaddr.IPNetwork(cidr)
95
    except ValueError:
96
        raise api.faults.BadRequest("Malformed CIDR")
97
    potential_gateway = str(ipaddr.IPNetwork(cidr).network + 1)
98

    
99
    if gateway is "":
100
        gateway = potential_gateway
101

    
102
    if ipversion == 6:
103
        validate_subnet_params(None, None, cidr, gateway)
104
        if slac is not None:
105
            dhcp = check_boolean_value(slac, "enable_slac")
106
        else:
107
            dhcp = check_boolean_value(dhcp, "dhcp")
108
    else:
109
        validate_subnet_params(cidr, gateway)
110
        dhcp = check_boolean_value(dhcp, "dhcp")
111

    
112
    name = check_name_length(name)
113

    
114
    gateway_ip = ipaddr.IPAddress(gateway)
115

    
116
    sub = Subnet.objects.create(name=name, network=network, cidr=cidr,
117
                                ipversion=ipversion, gateway=gateway,
118
                                dhcp=dhcp, host_routes=host_routes,
119
                                dns_nameservers=dns_nameservers)
120

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

    
137
    if allocation_pools:
138
        create_ip_pools(allocation_pools, cidr_ip, sub)
139

    
140
    return sub
141

    
142

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

    
151
    return subnet
152

    
153

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

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

    
161

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

167
    """
168
    log.info('Update subnet %s, name %s' % (sub_id, name))
169

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

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

    
178
    check_name_length(name)
179

    
180
    subnet.name = name
181
    subnet.save()
182

    
183
    return subnet
184

    
185

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

    
199

    
200
def check_empty_lists(value):
201
    """Check if value is Null/None, in which case we return an empty list"""
202
    if value is None:
203
        return []
204
    return value
205

    
206

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

    
213

    
214
def check_boolean_value(value, key):
215
    """Check if dhcp value is in acceptable values"""
216
    if value not in [True, False]:
217
        raise api.faults.BadRequest("Malformed request, %s must "
218
                                    "be True or False" % key)
219
    return value
220

    
221

    
222
def check_name_length(name):
223
    """Check if the length of a name is within acceptable value"""
224
    if len(str(name)) > Subnet.SUBNET_NAME_LENGTH:
225
        raise api.faults.BadRequest("Subnet name too long")
226
    return name
227

    
228

    
229
def get_subnet_fromdb(subnet_id, user_id, for_update=False):
230
    """Return a Subnet instance or raise ItemNotFound.
231
    This is the same as util.get_network
232

233
    """
234
    try:
235
        subnet_id = int(subnet_id)
236
        if for_update:
237
            return Subnet.objects.select_for_update().get(id=subnet_id,
238
                                                          network__userid=
239
                                                          user_id)
240
        return Subnet.objects.get(id=subnet_id, network__userid=user_id)
241
    except (ValueError, Subnet.DoesNotExist):
242
        raise api.faults.ItemNotFound('Subnet not found')
243

    
244

    
245
def validate_subpools(pool_list, cidr, gateway):
246
    """Validate IP Pools
247

248
    Validate the given IP pools are inside the cidr range
249
    Validate there are no overlaps in the given pools
250
    Finally, validate the gateway isn't in the given ip pools
251
    Input must be a list containing a sublist with start/end ranges as
252
    ipaddr.IPAddress items eg.,
253
    [[IPv4Address('192.168.42.11'), IPv4Address('192.168.42.15')],
254
     [IPv4Address('192.168.42.30'), IPv4Address('192.168.42.60')]]
255

256
    """
257
    if pool_list[0][0] <= cidr.network:
258
        raise api.faults.Conflict("IP Pool out of bounds")
259
    elif pool_list[-1][1] >= cidr.broadcast:
260
        raise api.faults.Conflict("IP Pool out of bounds")
261

    
262
    for start, end in pool_list:
263
        if start > end:
264
            raise api.faults.Conflict("Invalid IP pool range")
265
        # Raise BadRequest if gateway is inside the pool range
266
        if not (gateway < start or gateway > end):
267
            raise api.faults.Conflict("Gateway cannot be in pool range")
268

    
269
    # Check if there is a conflict between the IP Poll ranges
270
    end = cidr.network
271
    for pool in pool_list:
272
        if end >= pool[0]:
273
            raise api.faults.Conflict("IP Pool range conflict")
274
        end = pool[1]
275

    
276

    
277
def validate_subnet_params(subnet=None, gateway=None, subnet6=None,
278
                           gateway6=None):
279
    if subnet:
280
        try:
281
            # Use strict option to not all subnets with host bits set
282
            network = ipaddr.IPv4Network(subnet, strict=True)
283
        except ValueError:
284
            raise faults.BadRequest("Invalid network IPv4 subnet")
285

    
286
        # Check that network size is allowed!
287
        prefixlen = network.prefixlen
288
        if prefixlen > 29 or prefixlen <= settings.MAX_CIDR_BLOCK:
289
            raise faults.OverLimit(
290
                message="Unsupported network size",
291
                details="Netmask must be in range: (%s, 29]" %
292
                settings.MAX_CIDR_BLOCK)
293
        if gateway:  # Check that gateway belongs to network
294
            try:
295
                gateway = ipaddr.IPv4Address(gateway)
296
            except ValueError:
297
                raise faults.BadRequest("Invalid network IPv4 gateway")
298
            if not gateway in network:
299
                raise faults.BadRequest("Invalid network IPv4 gateway")
300

    
301
    if subnet6:
302
        try:
303
            # Use strict option to not all subnets with host bits set
304
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
305
        except ValueError:
306
            raise faults.BadRequest("Invalid network IPv6 subnet")
307
        # Check that network6 is an /64 subnet, because this is imposed by
308
        # 'mac2eui64' utiity.
309
        if network6.prefixlen != 64:
310
            msg = ("Unsupported IPv6 subnet size. Network netmask must be"
311
                   " /64")
312
            raise faults.BadRequest(msg)
313
        if gateway6:
314
            try:
315
                gateway6 = ipaddr.IPv6Address(gateway6)
316
            except ValueError:
317
                raise faults.BadRequest("Invalid network IPv6 gateway")
318
            if not gateway6 in network6:
319
                raise faults.BadRequest("Invalid network IPv6 gateway")