Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / subnets.py @ 4445f97a

History | View | Annotate | Download (14.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
from logging import getLogger
35
from snf_django.lib import api
36
from snf_django.lib.api import faults
37

    
38
from django.conf.urls import patterns
39
from django.http import HttpResponse
40
from django.utils import simplejson as json
41

    
42
from snf_django.lib.api import utils
43
from synnefo.db.models import Subnet, Network, IPPoolTable
44
from synnefo.logic import networks
45

    
46
from ipaddr import IPv4Network, IPv6Network, IPv4Address, IPAddress, IPNetwork
47

    
48
log = getLogger(__name__)
49

    
50

    
51
urlpatterns = patterns(
52
    'synnefo.api.subnets',
53
    (r'^(?:/|.json|.xml)?$', 'demux'),
54
    (r'^/([-\w]+)(?:/|.json|.xml)?$', 'subnet_demux'))
55

    
56

    
57
def demux(request):
58
    if request.method == 'GET':
59
        return list_subnets(request)
60
    elif request.method == 'POST':
61
        return create_subnet(request)
62
    else:
63
        return api.api_method_not_allowed(request)
64

    
65

    
66
def subnet_demux(request, sub_id):
67
    if request.method == 'GET':
68
        return get_subnet(request, sub_id)
69
    elif request.method == 'DELETE':
70
        return delete_subnet(request, sub_id)
71
    elif request.method == 'PUT':
72
        return update_subnet(request, sub_id)
73
    else:
74
        return api.api_method_not_allowed(request)
75

    
76

    
77
@api.api_method(http_method='GET', user_required=True, logger=log)
78
def list_subnets(request):
79
    """List all subnets of a user"""
80
    log.debug('list_subnets')
81

    
82
    user_subnets = Subnet.objects.filter(network__userid=request.user_uniq)
83
    subnets_dict = [subnet_to_dict(sub)
84
                    for sub in user_subnets.order_by('id')]
85
    data = json.dumps({'subnets': subnets_dict})
86

    
87
    return HttpResponse(data, status=200)
88

    
89

    
90
@api.api_method(http_method='POST', user_required=True, logger=log)
91
def create_subnet(request):
92
    """
93
    Create a subnet
94
    network_id and the desired cidr are mandatory, everything else is optional
95
    """
96

    
97
    dictionary = utils.get_request_dict(request)
98
    log.info('create subnet %s', dictionary)
99
    user_id = request.user_uniq
100

    
101
    try:
102
        subnet = dictionary['subnet']
103
        network_id = subnet['network_id']
104
        cidr = subnet['cidr']
105
    except KeyError:
106
        raise api.faults.BadRequest("Malformed request")
107

    
108
    try:
109
        network = Network.objects.get(id=network_id)
110
    except Network.DoesNotExist:
111
        raise api.faults.ItemNotFound("No networks found with that id")
112

    
113
    if user_id != network.userid:
114
        raise api.faults.Unauthorized("Unauthorized operation")
115

    
116
    ipversion = subnet.get('ip_version', 4)
117
    if ipversion not in [4, 6]:
118
        raise api.faults.BadRequest("Malformed IP version type")
119

    
120
    # Returns the first available IP in the subnet
121
    if ipversion == 6:
122
        potential_gateway = str(IPv6Network(cidr).network + 1)
123
        check_number_of_subnets(network, 6)
124
    else:
125
        potential_gateway = str(IPv4Network(cidr).network + 1)
126
        check_number_of_subnets(network, 4)
127

    
128
    gateway = subnet.get('gateway_ip', potential_gateway)
129

    
130
    if ipversion == 6:
131
        networks.validate_network_params(None, None, cidr, gateway)
132
        slac = subnet.get('slac', None)
133
        if slac is not None:
134
            dhcp = check_boolean_value(slac, "slac")
135
        else:
136
            dhcp = check_boolean_value(subnet.get('enable_dhcp', True), "dhcp")
137
    else:
138
        networks.validate_network_params(cidr, gateway)
139
        dhcp = check_boolean_value(subnet.get('enable_dhcp', True), "dhcp")
140

    
141
    name = check_name_length(subnet.get('name', None))
142

    
143
    dns = subnet.get('dns_nameservers', None)
144
    hosts = subnet.get('host_routes', None)
145

    
146
    gateway_ip = IPAddress(gateway)
147
    cidr_ip = IPNetwork(cidr)
148

    
149
    allocation_pools = subnet.get('allocation_pools', None)
150

    
151
    # FIX ME
152
    sub = Subnet.objects.create(name=name, network=network, cidr=cidr,
153
                                ipversion=ipversion, gateway=gateway,
154
                                dhcp=dhcp, host_routes=hosts,
155
                                dns_nameservers=dns)
156

    
157
    pool_list = list()
158
    if allocation_pools is not None:
159
        # If the user specified IP allocation pools, validate them and use them
160
        if ipversion == 6:
161
            raise api.faults.Conflict("Can't allocate an IP Pool in IPv6")
162
        pools = parse_ip_pools(allocation_pools)
163
        pool_list = string_to_ipaddr(pools)
164
        validate_subpools(pool_list, cidr_ip, gateway_ip)
165
    if allocation_pools is None and ipversion == 4:
166
        # Check if the gateway is the first IP of the subnet, in this case
167
        # create a single ip pool
168
        if int(gateway_ip) - int(cidr_ip) == 1:
169
            pool_list = [[gateway_ip + 1, cidr_ip.broadcast - 1]]
170
        else:
171
            # If the gateway isn't the first available ip, create two different
172
            # ip pools adjacent to said ip
173
            pool_list.append([cidr_ip.network + 1, gateway_ip - 1])
174
            pool_list.append([gateway_ip + 1, cidr_ip.broadcast - 1])
175

    
176
    if pool_list:
177
        create_ip_pools(pool_list, cidr_ip, sub)
178

    
179
    subnet_dict = subnet_to_dict(sub)
180
    data = json.dumps({'subnet': subnet_dict})
181
    return HttpResponse(data, status=200)
182

    
183

    
184
@api.api_method(http_method='GET', user_required=True, logger=log)
185
def get_subnet(request, sub_id):
186
    """Show info of a specific subnet"""
187
    log.debug('get_subnet %s', sub_id)
188
    user_id = request.user_uniq
189

    
190
    try:
191
        subnet = Subnet.objects.get(id=sub_id)
192
    except Subnet.DoesNotExist:
193
        raise api.faults.ItemNotFound("Subnet not found")
194

    
195
    if subnet.network.userid != user_id:
196
        raise api.failts.Unauthorized("You're not allowed to view this subnet")
197

    
198
    subnet_dict = subnet_to_dict(subnet)
199
    data = json.dumps({'subnet': subnet_dict})
200
    return HttpResponse(data, status=200)
201

    
202

    
203
@api.api_method(http_method='DELETE', user_required=True, logger=log)
204
def delete_subnet(request, sub_id):
205
    """
206
    Delete a subnet, raises BadRequest
207
    A subnet is deleted ONLY when the network that it belongs to is deleted
208
    """
209
    raise api.faults.BadRequest("Deletion of a subnet is not supported")
210

    
211

    
212
@api.api_method(http_method='PUT', user_required=True, logger=log)
213
def update_subnet(request, sub_id):
214
    """
215
    Update the fields of a subnet
216
    Only the name can be updated, everything else returns BadRequest
217
    """
218

    
219
    dictionary = utils.get_request_dict(request)
220
    log.info('Update subnet %s', dictionary)
221
    user_id = request.user_uniq
222

    
223
    try:
224
        subnet = dictionary['subnet']
225
    except KeyError:
226
        raise api.faults.BadRequest("Malformed request")
227

    
228
    original_subnet = get_subnet_fromdb(sub_id, user_id)
229
    original_dict = subnet_to_dict(original_subnet)
230

    
231
    if len(subnet) != 1:
232
        raise api.faults.BadRequest("Only the name of subnet can be updated")
233

    
234
    name = subnet.get("name", None)
235

    
236
    if not name:
237
        raise api.faults.BadRequest("Only the name of subnet can be updated")
238

    
239
    check_name_length(name)
240

    
241
    try:
242
        original_subnet.name = name
243
        original_subnet.save()
244
    except:
245
        #Fix me
246
        return "Unknown Error"
247

    
248
    subnet_dict = subnet_to_dict(original_subnet)
249
    data = json.dumps({'subnet': subnet_dict})
250
    return HttpResponse(data, status=200)
251

    
252

    
253
#Utility functions
254
def subnet_to_dict(subnet):
255
    """Returns a dictionary containing the info of a subnet"""
256
    dns = check_empty_lists(subnet.dns_nameservers)
257
    hosts = check_empty_lists(subnet.host_routes)
258
    allocation_pools = subnet.ip_pools.all()
259
    pools = list()
260

    
261
    if allocation_pools:
262
        for pool in allocation_pools:
263
            cidr = IPNetwork(pool.base)
264
            start = str(cidr.network + pool.offset)
265
            end = str(cidr.network + pool.offset + pool.size - 1)
266
            pools.append([{"start": start, "end": end}])
267

    
268
    dictionary = dict({'id': str(subnet.id),
269
                       'network_id': str(subnet.network.id),
270
                       'name': subnet.name if subnet.name is not None else "",
271
                       'tenant_id': subnet.network.userid,
272
                       'user_id': subnet.network.userid,
273
                       'gateway_ip': subnet.gateway,
274
                       'ip_version': subnet.ipversion,
275
                       'cidr': subnet.cidr,
276
                       'enable_dhcp': subnet.dhcp,
277
                       'dns_nameservers': dns,
278
                       'host_routes': hosts,
279
                       'allocation_pools': pools if pools is not None else []})
280

    
281
    if subnet.ipversion == 6:
282
        dictionary['slac'] = subnet.dhcp
283

    
284
    return dictionary
285

    
286

    
287
def string_to_ipaddr(pools):
288
    """
289
    Convert [["192.168.42.1", "192.168.42.15"],
290
            ["192.168.42.30", "192.168.42.60"]]
291
    to
292
            [[IPv4Address('192.168.42.1'), IPv4Address('192.168.42.15')],
293
            [IPv4Address('192.168.42.30'), IPv4Address('192.168.42.60')]]
294
    and sort the output
295
    """
296
    pool_list = [(map(lambda ip_str: IPAddress(ip_str), pool))
297
                 for pool in pools]
298
    pool_list.sort()
299
    return pool_list
300

    
301

    
302
def create_ip_pools(pools, cidr, subnet):
303
    """Placeholder"""
304
    for pool in pools:
305
        size = int(pool[1]) - int(pool[0]) + 1
306
        base = str(cidr)
307
        offset = int(pool[0]) - int(cidr.network)
308
        ip_pool = IPPoolTable.objects.create(size=size, offset=offset,
309
                                             base=base, subnet=subnet)
310

    
311

    
312
def check_empty_lists(value):
313
    """Check if value is Null/None, in which case we return an empty list"""
314
    if value is None:
315
        return []
316
    return value
317

    
318

    
319
def check_number_of_subnets(network, version):
320
    """Check if a user can add a subnet in a network"""
321
    if network.subnets.filter(ipversion=version):
322
        raise api.faults.BadRequest("Only one subnet of IPv4/IPv6 per "
323
                                    "network is allowed")
324

    
325

    
326
def check_boolean_value(value, key):
327
    """Check if dhcp value is in acceptable values"""
328
    if value not in [True, False]:
329
        raise api.faults.BadRequest("Malformed request, %s must "
330
                                    "be True or False" % key)
331
    return value
332

    
333

    
334
def check_name_length(name):
335
    """Check if the length of a name is within acceptable value"""
336
    if len(str(name)) > Subnet.SUBNET_NAME_LENGTH:
337
        raise api.faults.BadRequest("Subnet name too long")
338
    return name
339

    
340

    
341
def check_for_hosts_dns(subnet):
342
    """
343
    Check if a request contains host_routes or dns_nameservers options
344
    Expects the request in a dictionary format
345
    """
346
    if subnet.get('host_routes', None):
347
        raise api.faults.BadRequest("Setting host routes isn't supported")
348
    if subnet.get('dns_nameservers', None):
349
        raise api.faults.BadRequest("Setting dns nameservers isn't supported")
350

    
351

    
352
def get_subnet_fromdb(subnet_id, user_id, for_update=False):
353
    """
354
    Return a Subnet instance or raise ItemNotFound.
355
    This is the same as util.get_network
356
    """
357
    try:
358
        subnet_id = int(subnet_id)
359
        if for_update:
360
            return Subnet.objects.select_for_update().get(id=subnet_id,
361
                                                          network__userid=
362
                                                          user_id)
363
        return Subnet.objects.get(id=subnet_id, network__userid=user_id)
364
    except (ValueError, Subnet.DoesNotExist):
365
        raise api.faults.ItemNotFound('Subnet not found')
366

    
367

    
368
def parse_ip_pools(pools):
369
    """
370
    Convert [{'start': '192.168.42.1', 'end': '192.168.42.15'},
371
             {'start': '192.168.42.30', 'end': '192.168.42.60'}]
372
    to
373
            [["192.168.42.1", "192.168.42.15"],
374
             ["192.168.42.30", "192.168.42.60"]]
375
    """
376
    pool_list = list()
377
    for pool in pools:
378
        parse = [pool["start"], pool["end"]]
379
        pool_list.append(parse)
380
    return pool_list
381

    
382

    
383
def validate_subpools(pool_list, cidr, gateway):
384
    """
385
    Validate the given IP pools are inside the cidr range
386
    Validate there are no overlaps in the given pools
387
    Finally, validate the gateway isn't in the given ip pools
388
    Input must be a list containing a sublist with start/end ranges as
389
    ipaddr.IPAddress items eg.,
390
    [[IPv4Address('192.168.42.11'), IPv4Address('192.168.42.15')],
391
     [IPv4Address('192.168.42.30'), IPv4Address('192.168.42.60')]]
392
    """
393
    if pool_list[0][0] <= cidr.network:
394
        raise api.faults.Conflict("IP Pool out of bounds")
395
    elif pool_list[-1][1] >= cidr.broadcast:
396
        raise api.faults.Conflict("IP Pool out of bounds")
397

    
398
    for start, end in pool_list:
399
        if start > end:
400
            raise api.faults.Conflict("Invalid IP pool range")
401
        # Raise BadRequest if gateway is inside the pool range
402
        if not (gateway < start or gateway > end):
403
            raise api.faults.Conflict("Gateway cannot be in pool range")
404

    
405
    # Check if there is a conflict between the IP Poll ranges
406
    end = cidr.network
407
    for pool in pool_list:
408
        if end >= pool[0]:
409
            raise api.faults.Conflict("IP Pool range conflict")
410
        end = pool[1]