Statistics
| Branch: | Tag: | Revision:

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

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 django.db.models import Q
41

    
42
from snf_django.lib import api
43
from snf_django.lib.api import faults
44
from synnefo.logic import utils
45
from synnefo.api import util
46

    
47
from synnefo.db.models import Subnet, Network, IPPoolTable
48

    
49
log = getLogger(__name__)
50

    
51

    
52
def subnet_command(action):
53
    def decorator(func):
54
        @wraps(func)
55
        @transaction.commit_on_success()
56
        def wrapper(subnet, *args, **kwargs):
57
            return func(subnet, *args, **kwargs)
58
        return wrapper
59
    return decorator
60

    
61

    
62
def list_subnets(user_id):
63
    """List all subnets of a user"""
64
    log.debug('list_subnets %s', user_id)
65

    
66
    query = (((Q(network__userid=user_id) & Q(network__public=False)) |
67
              Q(network__public=True)) & Q(deleted=False))
68
    user_subnets = Subnet.objects.filter(query)
69
    return user_subnets
70

    
71

    
72
@transaction.commit_on_success
73
def create_subnet(*args, **kwargs):
74
    return _create_subnet(*args, **kwargs)
75

    
76

    
77
def _create_subnet(network_id, user_id, cidr, name, ipversion=4, gateway=None,
78
                   dhcp=True, slaac=True, dns_nameservers=None,
79
                   allocation_pools=None, host_routes=None):
80
    """Create a subnet
81

82
    network_id and the desired cidr are mandatory, everything else is optional
83

84
    """
85
    try:
86
        network = Network.objects.get(id=network_id)
87
    except Network.DoesNotExist:
88
        raise api.faults.ItemNotFound("No network found with that id")
89

    
90
    if network.deleted:
91
        raise api.faults.BadRequest("Network has been deleted")
92

    
93
    if user_id != network.userid:
94
        raise api.faults.Unauthorized("Unauthorized operation")
95

    
96
    if ipversion not in [4, 6]:
97
        raise api.faults.BadRequest("Malformed IP version type")
98

    
99
    check_number_of_subnets(network, ipversion)
100

    
101
    if network.backend_networks.exists():
102
        raise api.faults.BadRequest("Cannot create subnet in network %s, VMs"
103
                                    " are already connected to this network" %
104
                                    network_id)
105

    
106
    try:
107
        cidr_ip = ipaddr.IPNetwork(cidr)
108
    except ValueError:
109
        raise api.faults.BadRequest("Malformed CIDR")
110

    
111
    if ipversion == 6:
112
        validate_subnet_params(subnet6=cidr, gateway6=gateway)
113
    else:
114
        validate_subnet_params(subnet=cidr, gateway=gateway)
115

    
116
    utils.check_name_length(name, Subnet.SUBNET_NAME_LENGTH, "Subnet "
117
                            "name is too long")
118
    sub = Subnet.objects.create(name=name, network=network, cidr=cidr,
119
                                ipversion=ipversion, gateway=gateway,
120
                                userid=network.userid, public=network.public,
121
                                dhcp=dhcp, host_routes=host_routes,
122
                                dns_nameservers=dns_nameservers)
123

    
124
    gateway_ip = ipaddr.IPAddress(gateway) if gateway else None
125

    
126
    if allocation_pools is not None:
127
        if ipversion == 6:
128
            raise api.faults.Conflict("Can't allocate an IP Pool in IPv6")
129
    elif ipversion == 4:
130
        # Check if the gateway is the first IP of the subnet, or the last. In
131
        # that case create a single ip pool.
132
        if gateway_ip:
133
            if int(gateway_ip) - int(cidr_ip) == 1:
134
                allocation_pools = [(gateway_ip + 1, cidr_ip.broadcast - 1)]
135
            elif int(cidr_ip.broadcast) - int(gateway_ip) == 1:
136
                allocation_pools = [(cidr_ip.network + 1, gateway_ip - 1)]
137
            else:
138
                # If the gateway isn't the first available ip, create two
139
                # different ip pools adjacent to said ip
140
                allocation_pools = [(cidr_ip.network + 1, gateway_ip - 1),
141
                                    (gateway_ip + 1, cidr_ip.broadcast - 1)]
142
        else:
143
            allocation_pools = [(cidr_ip.network + 1, cidr_ip.broadcast - 1)]
144

    
145
    if allocation_pools:
146
        # Validate the allocation pools
147
        validate_pools(allocation_pools, cidr_ip, gateway_ip)
148
        create_ip_pools(allocation_pools, cidr_ip, sub)
149

    
150
    return sub
151

    
152

    
153
def get_subnet(subnet_id, user_id, for_update=False):
154
    """Return a Subnet instance or raise ItemNotFound."""
155

    
156
    try:
157
        objects = Subnet.objects
158
        subnet_id = int(subnet_id)
159
        return objects.get(Q(userid=user_id) | Q(public=True),
160
                           id=subnet_id)
161
    except (ValueError, TypeError):
162
        raise faults.BadRequest("Invalid subnet ID '%s'" % subnet_id)
163
    except Subnet.DoesNotExist:
164
        raise faults.ItemNotFound("Subnet '%s' not found." % subnet_id)
165

    
166

    
167
def delete_subnet():
168
    """Delete a subnet, raises BadRequest
169
    A subnet is deleted ONLY when the network that it belongs to is deleted
170

171
    """
172
    raise api.faults.BadRequest("Deletion of a subnet is not supported")
173

    
174

    
175
@transaction.commit_on_success
176
def update_subnet(sub_id, name, user_id):
177
    """Update the fields of a subnet
178
    Only the name can be updated
179

180
    """
181
    log.info('Update subnet %s, name %s' % (sub_id, name))
182

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

    
188
    if user_id != subnet.network.userid:
189
        raise api.faults.Unauthorized("Unauthorized operation")
190

    
191
    utils.check_name_length(name, Subnet.SUBNET_NAME_LENGTH, "Subnet name is "
192
                            " too long")
193

    
194
    subnet.name = name
195
    subnet.save()
196

    
197
    return subnet
198

    
199

    
200
#Utility functions
201
def create_ip_pools(pools, cidr, subnet):
202
    """Create IP Pools in the database"""
203
    return [_create_ip_pool(pool, cidr, subnet) for pool in pools]
204

    
205

    
206
def _create_ip_pool(pool, cidr, subnet):
207
    size = int(pool[1]) - int(pool[0]) + 1
208
    base = str(cidr)
209
    offset = int(pool[0]) - int(cidr.network)
210
    return IPPoolTable.objects.create(size=size, offset=offset,
211
                                      base=base, subnet=subnet)
212

    
213

    
214
def check_number_of_subnets(network, version):
215
    """Check if a user can add a subnet in a network"""
216
    if network.subnets.filter(ipversion=version):
217
        raise api.faults.BadRequest("Only one subnet of IPv4/IPv6 per "
218
                                    "network is allowed")
219

    
220

    
221
def validate_pools(pool_list, cidr, gateway):
222
    """Validate IP Pools
223

224
    Validate the given IP pools are inside the cidr range
225
    Validate there are no overlaps in the given pools
226
    Finally, validate the gateway isn't in the given ip pools
227
    Input must be a list containing a sublist with start/end ranges as
228
    ipaddr.IPAddress items eg.,
229
    [[IPv4Address('192.168.42.11'), IPv4Address('192.168.42.15')],
230
     [IPv4Address('192.168.42.30'), IPv4Address('192.168.42.60')]]
231

232
    """
233
    if pool_list[0][0] <= cidr.network:
234
        raise api.faults.Conflict("IP Pool out of bounds")
235
    elif pool_list[-1][1] >= cidr.broadcast:
236
        raise api.faults.Conflict("IP Pool out of bounds")
237

    
238
    for start, end in pool_list:
239
        if start > end:
240
            raise api.faults.Conflict("Invalid IP pool range")
241
        # Raise BadRequest if gateway is inside the pool range
242
        if gateway:
243
            if not (gateway < start or gateway > end):
244
                raise api.faults.Conflict("Gateway cannot be in pool range")
245

    
246
    # Check if there is a conflict between the IP Pool ranges
247
    end = cidr.network
248
    for pool in pool_list:
249
        if end >= pool[0]:
250
            raise api.faults.Conflict("IP Pool range conflict")
251
        end = pool[1]
252

    
253

    
254
def validate_subnet_params(subnet=None, gateway=None, subnet6=None,
255
                           gateway6=None):
256
    if subnet:
257
        try:
258
            # Use strict option to not all subnets with host bits set
259
            network = ipaddr.IPv4Network(subnet, strict=True)
260
        except ValueError:
261
            raise faults.BadRequest("Invalid network IPv4 subnet")
262

    
263
        # Check that network size is allowed!
264
        prefixlen = network.prefixlen
265
        if prefixlen > 29 or prefixlen <= settings.MAX_CIDR_BLOCK:
266
            raise faults.OverLimit(
267
                message="Unsupported network size",
268
                details="Netmask must be in range: (%s, 29]" %
269
                settings.MAX_CIDR_BLOCK)
270
        if gateway:  # Check that gateway belongs to network
271
            try:
272
                gateway = ipaddr.IPv4Address(gateway)
273
            except ValueError:
274
                raise faults.BadRequest("Invalid network IPv4 gateway")
275
            if not gateway in network:
276
                raise faults.BadRequest("Invalid network IPv4 gateway")
277

    
278
    if subnet6:
279
        try:
280
            # Use strict option to not all subnets with host bits set
281
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
282
        except ValueError:
283
            raise faults.BadRequest("Invalid network IPv6 subnet")
284
        # Check that network6 is an /64 subnet, because this is imposed by
285
        # 'mac2eui64' utiity.
286
        if network6.prefixlen != 64:
287
            msg = ("Unsupported IPv6 subnet size. Network netmask must be"
288
                   " /64")
289
            raise faults.BadRequest(msg)
290
        if gateway6:
291
            try:
292
                gateway6 = ipaddr.IPv6Address(gateway6)
293
            except ValueError:
294
                raise faults.BadRequest("Invalid network IPv6 gateway")
295
            if not gateway6 in network6:
296
                raise faults.BadRequest("Invalid network IPv6 gateway")
297

    
298

    
299
def parse_allocation_pools(allocation_pools):
300
    alloc = list()
301
    for pool in allocation_pools:
302
        try:
303
            start, end = pool.split(',')
304
            alloc.append([ipaddr.IPv4Address(start),
305
                          ipaddr.IPv4Address(end)])
306
        except ValueError:
307
            raise faults.BadRequest("Malformed IPv4 address")
308

    
309
    return alloc