Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (9.8 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, slaac=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 network 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

    
98
    if ipversion == 6:
99
        validate_subnet_params(None, None, cidr, gateway)
100
    else:
101
        validate_subnet_params(cidr, gateway)
102

    
103
    name = check_name_length(name)
104
    sub = Subnet.objects.create(name=name, network=network, cidr=cidr,
105
                                ipversion=ipversion, gateway=gateway,
106
                                dhcp=dhcp, host_routes=host_routes,
107
                                dns_nameservers=dns_nameservers)
108

    
109
    gateway_ip = ipaddr.IPAddress(gateway) if gateway else None
110

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

    
130
    if allocation_pools:
131
        create_ip_pools(allocation_pools, cidr_ip, sub)
132

    
133
    return sub
134

    
135

    
136
def get_subnet(sub_id):
137
    """Show info of a specific subnet"""
138
    log.debug('get_subnet %s', sub_id)
139
    try:
140
        subnet = Subnet.objects.get(id=sub_id)
141
    except Subnet.DoesNotExist:
142
        raise api.faults.ItemNotFound("Subnet not found")
143

    
144
    return subnet
145

    
146

    
147
def delete_subnet():
148
    """Delete a subnet, raises BadRequest
149
    A subnet is deleted ONLY when the network that it belongs to is deleted
150

151
    """
152
    raise api.faults.BadRequest("Deletion of a subnet is not supported")
153

    
154

    
155
@transaction.commit_on_success
156
def update_subnet(sub_id, name, user_id):
157
    """Update the fields of a subnet
158
    Only the name can be updated
159

160
    """
161
    log.info('Update subnet %s, name %s' % (sub_id, name))
162

    
163
    try:
164
        subnet = Subnet.objects.get(id=sub_id)
165
    except:
166
        raise api.faults.ItemNotFound("Subnet not found")
167

    
168
    if user_id != subnet.network.userid:
169
        raise api.faults.Unauthorized("Unauthorized operation")
170

    
171
    check_name_length(name)
172

    
173
    subnet.name = name
174
    subnet.save()
175

    
176
    return subnet
177

    
178

    
179
#Utility functions
180
def create_ip_pools(pools, cidr, subnet):
181
    """Create IP Pools in the database"""
182
    ip_pools = []
183
    for pool in pools:
184
        size = int(pool[1]) - int(pool[0]) + 1
185
        base = str(cidr)
186
        offset = int(pool[0]) - int(cidr.network)
187
        ip_pool = IPPoolTable.objects.create(size=size, offset=offset,
188
                                             base=base, subnet=subnet)
189
        ip_pools.append(ip_pool)
190
    return ip_pools
191

    
192

    
193
def check_number_of_subnets(network, version):
194
    """Check if a user can add a subnet in a network"""
195
    if network.subnets.filter(ipversion=version):
196
        raise api.faults.BadRequest("Only one subnet of IPv4/IPv6 per "
197
                                    "network is allowed")
198

    
199

    
200
def check_name_length(name):
201
    """Check if the length of a name is within acceptable value"""
202
    if len(str(name)) > Subnet.SUBNET_NAME_LENGTH:
203
        raise api.faults.BadRequest("Subnet name too long")
204
    return name
205

    
206

    
207
def validate_subpools(pool_list, cidr, gateway):
208
    """Validate IP Pools
209

210
    Validate the given IP pools are inside the cidr range
211
    Validate there are no overlaps in the given pools
212
    Finally, validate the gateway isn't in the given ip pools
213
    Input must be a list containing a sublist with start/end ranges as
214
    ipaddr.IPAddress items eg.,
215
    [[IPv4Address('192.168.42.11'), IPv4Address('192.168.42.15')],
216
     [IPv4Address('192.168.42.30'), IPv4Address('192.168.42.60')]]
217

218
    """
219
    if pool_list[0][0] <= cidr.network:
220
        raise api.faults.Conflict("IP Pool out of bounds")
221
    elif pool_list[-1][1] >= cidr.broadcast:
222
        raise api.faults.Conflict("IP Pool out of bounds")
223

    
224
    for start, end in pool_list:
225
        if start > end:
226
            raise api.faults.Conflict("Invalid IP pool range")
227
        # Raise BadRequest if gateway is inside the pool range
228
        if gateway:
229
            if not (gateway < start or gateway > end):
230
                raise api.faults.Conflict("Gateway cannot be in pool range")
231

    
232
    # Check if there is a conflict between the IP Poll ranges
233
    end = cidr.network
234
    for pool in pool_list:
235
        if end >= pool[0]:
236
            raise api.faults.Conflict("IP Pool range conflict")
237
        end = pool[1]
238

    
239

    
240
def validate_subnet_params(subnet=None, gateway=None, subnet6=None,
241
                           gateway6=None):
242
    if subnet:
243
        try:
244
            # Use strict option to not all subnets with host bits set
245
            network = ipaddr.IPv4Network(subnet, strict=True)
246
        except ValueError:
247
            raise faults.BadRequest("Invalid network IPv4 subnet")
248

    
249
        # Check that network size is allowed!
250
        prefixlen = network.prefixlen
251
        if prefixlen > 29 or prefixlen <= settings.MAX_CIDR_BLOCK:
252
            raise faults.OverLimit(
253
                message="Unsupported network size",
254
                details="Netmask must be in range: (%s, 29]" %
255
                settings.MAX_CIDR_BLOCK)
256
        if gateway:  # Check that gateway belongs to network
257
            try:
258
                gateway = ipaddr.IPv4Address(gateway)
259
            except ValueError:
260
                raise faults.BadRequest("Invalid network IPv4 gateway")
261
            if not gateway in network:
262
                raise faults.BadRequest("Invalid network IPv4 gateway")
263

    
264
    if subnet6:
265
        try:
266
            # Use strict option to not all subnets with host bits set
267
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
268
        except ValueError:
269
            raise faults.BadRequest("Invalid network IPv6 subnet")
270
        # Check that network6 is an /64 subnet, because this is imposed by
271
        # 'mac2eui64' utiity.
272
        if network6.prefixlen != 64:
273
            msg = ("Unsupported IPv6 subnet size. Network netmask must be"
274
                   " /64")
275
            raise faults.BadRequest(msg)
276
        if gateway6:
277
            try:
278
                gateway6 = ipaddr.IPv6Address(gateway6)
279
            except ValueError:
280
                raise faults.BadRequest("Invalid network IPv6 gateway")
281
            if not gateway6 in network6:
282
                raise faults.BadRequest("Invalid network IPv6 gateway")