Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / subnets.py @ 868e4ce0

History | View | Annotate | Download (14 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
    sub = Subnet.objects.create(name=name, network=network, cidr=cidr,
152
                                ipversion=ipversion, gateway=gateway,
153
                                dhcp=dhcp, host_routes=hosts,
154
                                dns_nameservers=dns)
155

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

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

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

    
182

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

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

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

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

    
201

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

    
210

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

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

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

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

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

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

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

    
238
    check_name_length(name)
239

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

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

    
251

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

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

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

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

    
283
    return dictionary
284

    
285

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

    
300

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

    
310

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

    
317

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

    
324

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

    
332

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

    
339

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

    
350

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

    
366

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

    
381

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

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

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