Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / subnets.py @ 911a1bc1

History | View | Annotate | Download (12.3 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
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
    if allocation_pools:
152
        if ipversion == 6:
153
            raise api.faults.Conflict("Can't allocate an IP Pool in IPv6")
154
        pools = parse_ip_pools(allocation_pools)
155
        validate_subpools(pools, cidr_ip, gateway_ip)
156
    else:
157
        # FIX ME
158
        pass
159

    
160
    # FIX ME
161
    try:
162
        sub = Subnet.objects.create(name=name, network=network, cidr=cidr,
163
                                    ipversion=ipversion, gateway=gateway,
164
                                    dhcp=dhcp, host_routes=hosts,
165
                                    dns_nameservers=dns)
166
    except:
167
        raise
168
        return "Error"
169

    
170
    subnet_dict = subnet_to_dict(sub)
171
    data = json.dumps({'subnet': subnet_dict})
172
    return HttpResponse(data, status=200)
173

    
174

    
175
@api.api_method(http_method='GET', user_required=True, logger=log)
176
def get_subnet(request, sub_id):
177
    """Show info of a specific subnet"""
178
    log.debug('get_subnet %s', sub_id)
179
    user_id = request.user_uniq
180

    
181
    try:
182
        subnet = Subnet.objects.get(id=sub_id)
183
    except Subnet.DoesNotExist:
184
        raise api.faults.ItemNotFound("Subnet not found")
185

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

    
189
    subnet_dict = subnet_to_dict(subnet)
190
    data = json.dumps({'subnet': subnet_dict})
191
    return HttpResponse(data, status=200)
192

    
193

    
194
@api.api_method(http_method='DELETE', user_required=True, logger=log)
195
def delete_subnet(request, sub_id):
196
    """
197
    Delete a subnet, raises BadRequest
198
    A subnet is deleted ONLY when the network that it belongs to is deleted
199
    """
200
    raise api.faults.BadRequest("Deletion of a subnet is not supported")
201

    
202

    
203
@api.api_method(http_method='PUT', user_required=True, logger=log)
204
def update_subnet(request, sub_id):
205
    """
206
    Update the fields of a subnet
207
    Only the name can be updated, everything else returns BadRequest
208
    """
209

    
210
    dictionary = utils.get_request_dict(request)
211
    log.info('Update subnet %s', dictionary)
212
    user_id = request.user_uniq
213

    
214
    try:
215
        subnet = dictionary['subnet']
216
    except KeyError:
217
        raise api.faults.BadRequest("Malformed request")
218

    
219
    original_subnet = get_subnet_fromdb(sub_id, user_id)
220
    original_dict = subnet_to_dict(original_subnet)
221

    
222
    if len(subnet) != 1:
223
        raise api.faults.BadRequest("Only the name of subnet can be updated")
224

    
225
    name = subnet.get("name", None)
226

    
227
    if not name:
228
        raise api.faults.BadRequest("Only the name of subnet can be updated")
229

    
230
    check_name_length(name)
231

    
232
    try:
233
        original_subnet.name = name
234
        original_subnet.save()
235
    except:
236
        #Fix me
237
        return "Unknown Error"
238

    
239
    subnet_dict = subnet_to_dict(original_subnet)
240
    data = json.dumps({'subnet': subnet_dict})
241
    return HttpResponse(data, status=200)
242

    
243

    
244
#Utility functions
245
def subnet_to_dict(subnet):
246
    """Returns a dictionary containing the info of a subnet"""
247
    dns = check_empty_lists(subnet.dns_nameservers)
248
    hosts = check_empty_lists(subnet.host_routes)
249
    #allocation_pools =
250

    
251
    dictionary = dict({'id': str(subnet.id),
252
                       'network_id': str(subnet.network.id),
253
                       'name': subnet.name if subnet.name is not None else "",
254
                       'tenant_id': subnet.network.userid,
255
                       'user_id': subnet.network.userid,
256
                       'gateway_ip': subnet.gateway,
257
                       'ip_version': subnet.ipversion,
258
                       'cidr': subnet.cidr,
259
                       'enable_dhcp': subnet.dhcp,
260
                       'dns_nameservers': dns,
261
                       'host_routes': hosts,
262
                       'allocation_pools': []})
263

    
264
    if subnet.ipversion == 6:
265
        dictionary['slac'] = subnet.dhcp
266

    
267
    return dictionary
268

    
269

    
270
def check_empty_lists(value):
271
    """Check if value is Null/None, in which case we return an empty list"""
272
    if value is None:
273
        return []
274
    return value
275

    
276

    
277
def check_number_of_subnets(network, version):
278
    """Check if a user can add a subnet in a network"""
279
    if network.subnets.filter(ipversion=version):
280
        raise api.faults.BadRequest("Only one subnet of IPv4/IPv6 per "
281
                                    "network is allowed")
282

    
283

    
284
def check_boolean_value(value, key):
285
    """Check if dhcp value is in acceptable values"""
286
    if value not in [True, False]:
287
        raise api.faults.BadRequest("Malformed request, %s must "
288
                                    "be True or False" % key)
289
    return value
290

    
291

    
292
def check_name_length(name):
293
    """Check if the length of a name is within acceptable value"""
294
    if len(str(name)) > Subnet.SUBNET_NAME_LENGTH:
295
        raise api.faults.BadRequest("Subnet name too long")
296
    return name
297

    
298

    
299
def check_for_hosts_dns(subnet):
300
    """
301
    Check if a request contains host_routes or dns_nameservers options
302
    Expects the request in a dictionary format
303
    """
304
    if subnet.get('host_routes', None):
305
        raise api.faults.BadRequest("Setting host routes isn't supported")
306
    if subnet.get('dns_nameservers', None):
307
        raise api.faults.BadRequest("Setting dns nameservers isn't supported")
308

    
309

    
310
def get_subnet_fromdb(subnet_id, user_id, for_update=False):
311
    """
312
    Return a Subnet instance or raise ItemNotFound.
313
    This is the same as util.get_network
314
    """
315
    try:
316
        subnet_id = int(subnet_id)
317
        if for_update:
318
            return Subnet.objects.select_for_update().get(id=subnet_id,
319
                                                          network__userid=
320
                                                          user_id)
321
        return Subnet.objects.get(id=subnet_id, network__userid=user_id)
322
    except (ValueError, Subnet.DoesNotExist):
323
        raise api.faults.ItemNotFound('Subnet not found')
324

    
325

    
326
def parse_ip_pools(pools):
327
    """
328
    Convert [{'start': '192.168.42.1', 'end': '192.168.42.15'},
329
             {'start': '192.168.42.30', 'end': '192.168.42.60'}]
330
    to
331
            [["192.168.42.1", "192.168.42.15"],
332
             ["192.168.42.30", "192.168.42.60"]]
333
    """
334
    pool_list = list()
335
    for pool in pools:
336
        parse = [pool["start"], pool["end"]]
337
        pool_list.append(parse)
338
    return pool_list
339

    
340

    
341
def validate_subpools(pools, cidr, gateway):
342
    """
343
    Validate the given IP pools are inside the cidr range
344
    Validate there are no overlaps in the given pools
345
    Finally, validate the gateway isn't in the given ip pools
346
    Input must be a list containing a sublist with start/end ranges as strings
347
    [["192.168.42.1", "192.168.42.15"], ["192.168.42.30", "192.168.42.60"]]
348
    """
349
    pool_list = [(map(lambda ip_str: IPAddress(ip_str), pool))
350
                 for pool in pools]
351
    pool_list.sort()
352

    
353
    if pool_list[0][0] <= cidr.network:
354
        raise api.faults.Conflict("IP Pool out of bounds")
355
    elif pool_list[-1][1] >= cidr.broadcast:
356
        raise api.faults.Conflict("IP Pool out of bounds")
357

    
358
    for start, end in pool_list:
359
        if start > end:
360
            raise api.faults.Conflict("Invalid IP pool range")
361
        # Raise BadRequest if gateway is inside the pool range
362
        if not (gateway < start or gateway > end):
363
            raise api.faults.Conflict("Gateway cannot be in pool range")
364

    
365
    # Check if there is a conflict between the IP Poll ranges
366
    end = cidr.network
367
    for pool in pool_list:
368
        if end >= pool[0]:
369
            raise api.faults.Conflict("IP Pool range conflict")
370
        end = pool[1]