Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 04a1b675

History | View | Annotate | Download (14.5 kB)

1
# Copyright 2011-2012 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 datetime
35
import ipaddr
36

    
37
from base64 import b64encode, b64decode
38
from datetime import timedelta, tzinfo
39
from functools import wraps
40
from hashlib import sha256
41
from logging import getLogger
42
from random import choice
43
from string import digits, lowercase, uppercase
44
from time import time
45
from traceback import format_exc
46
from wsgiref.handlers import format_date_time
47

    
48
import dateutil.parser
49

    
50
from Crypto.Cipher import AES
51

    
52
from django.conf import settings
53
from django.http import HttpResponse
54
from django.template.loader import render_to_string
55
from django.utils import simplejson as json
56
from django.utils.cache import add_never_cache_headers
57
from django.db.models import Q
58

    
59
from snf_django.lib.api import faults
60
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
61
                               Network, BackendNetwork, NetworkInterface,
62
                               BridgePoolTable, MacPrefixPoolTable)
63
from synnefo.db.pools import EmptyPool
64

    
65
from snf_django.lib.astakos import get_user
66
from synnefo.plankton.utils import image_backend
67
from synnefo.settings import MAX_CIDR_BLOCK
68

    
69

    
70
log = getLogger('synnefo.api')
71

    
72

    
73
def random_password():
74
    """Generates a random password
75

76
    We generate a windows compliant password: it must contain at least
77
    one charachter from each of the groups: upper case, lower case, digits.
78
    """
79

    
80
    pool = lowercase + uppercase + digits
81
    lowerset = set(lowercase)
82
    upperset = set(uppercase)
83
    digitset = set(digits)
84
    length = 10
85

    
86
    password = ''.join(choice(pool) for i in range(length - 2))
87

    
88
    # Make sure the password is compliant
89
    chars = set(password)
90
    if not chars & lowerset:
91
        password += choice(lowercase)
92
    if not chars & upperset:
93
        password += choice(uppercase)
94
    if not chars & digitset:
95
        password += choice(digits)
96

    
97
    # Pad if necessary to reach required length
98
    password += ''.join(choice(pool) for i in range(length - len(password)))
99

    
100
    return password
101

    
102

    
103
def zeropad(s):
104
    """Add zeros at the end of a string in order to make its length
105
       a multiple of 16."""
106

    
107
    npad = 16 - len(s) % 16
108
    return s + '\x00' * npad
109

    
110

    
111
def encrypt(plaintext):
112
    # Make sure key is 32 bytes long
113
    key = sha256(settings.SECRET_KEY).digest()
114

    
115
    aes = AES.new(key)
116
    enc = aes.encrypt(zeropad(plaintext))
117
    return b64encode(enc)
118

    
119

    
120
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
121
           non_suspended=False):
122
    """Find a VirtualMachine instance based on ID and owner."""
123

    
124
    try:
125
        server_id = int(server_id)
126
        servers = VirtualMachine.objects
127
        if for_update:
128
            servers = servers.select_for_update()
129
        vm = servers.get(id=server_id, userid=user_id)
130
        if non_deleted and vm.deleted:
131
            raise VirtualMachine.DeletedError
132
        if non_suspended and vm.suspended:
133
            raise faults.Forbidden("Administratively Suspended VM")
134
        return vm
135
    except ValueError:
136
        raise faults.BadRequest('Invalid server ID.')
137
    except VirtualMachine.DoesNotExist:
138
        raise faults.ItemNotFound('Server not found.')
139

    
140

    
141
def get_vm_meta(vm, key):
142
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
143

    
144
    try:
145
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
146
    except VirtualMachineMetadata.DoesNotExist:
147
        raise faults.ItemNotFound('Metadata key not found.')
148

    
149

    
150
def get_image(image_id, user_id):
151
    """Return an Image instance or raise ItemNotFound."""
152

    
153
    with image_backend(user_id) as backend:
154
        image = backend.get_image(image_id)
155
        if not image:
156
            raise faults.ItemNotFound('Image not found.')
157
        return image
158

    
159

    
160
def get_image_dict(image_id, user_id):
161
    image = {}
162
    img = get_image(image_id, user_id)
163
    properties = img.get('properties', {})
164
    image['backend_id'] = img['location']
165
    image['format'] = img['disk_format']
166
    image['metadata'] = dict((key.upper(), val)
167
                             for key, val in properties.items())
168
    image['checksum'] = img['checksum']
169

    
170
    return image
171

    
172

    
173
def get_flavor(flavor_id, include_deleted=False):
174
    """Return a Flavor instance or raise ItemNotFound."""
175

    
176
    try:
177
        flavor_id = int(flavor_id)
178
        if include_deleted:
179
            return Flavor.objects.get(id=flavor_id)
180
        else:
181
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
182
    except (ValueError, Flavor.DoesNotExist):
183
        raise faults.ItemNotFound('Flavor not found.')
184

    
185

    
186
def get_flavor_provider(flavor):
187
    """Extract provider from disk template.
188

189
    Provider for `ext` disk_template is encoded in the disk template
190
    name, which is formed `ext_<provider_name>`. Provider is None
191
    for all other disk templates.
192

193
    """
194
    disk_template = flavor.disk_template
195
    provider = None
196
    if disk_template.startswith("ext"):
197
        disk_template, provider = disk_template.split("_", 1)
198
    return disk_template, provider
199

    
200

    
201
def get_network(network_id, user_id, for_update=False):
202
    """Return a Network instance or raise ItemNotFound."""
203

    
204
    try:
205
        network_id = int(network_id)
206
        objects = Network.objects
207
        if for_update:
208
            objects = objects.select_for_update()
209
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
210
    except (ValueError, Network.DoesNotExist):
211
        raise faults.ItemNotFound('Network not found.')
212

    
213

    
214
def validate_network_params(subnet, gateway=None, subnet6=None, gateway6=None):
215
    try:
216
        # Use strict option to not all subnets with host bits set
217
        network = ipaddr.IPv4Network(subnet, strict=True)
218
    except ValueError:
219
        raise faults.BadRequest("Invalid network IPv4 subnet")
220

    
221
    # Check that network size is allowed!
222
    if not validate_network_size(network.prefixlen):
223
        raise faults.OverLimit(message="Unsupported network size",
224
                        details="Network mask must be in range (%s, 29]" %
225
                                MAX_CIDR_BLOCK)
226

    
227
    # Check that gateway belongs to network
228
    if gateway:
229
        try:
230
            gateway = ipaddr.IPv4Address(gateway)
231
        except ValueError:
232
            raise faults.BadRequest("Invalid network IPv4 gateway")
233
        if not gateway in network:
234
            raise faults.BadRequest("Invalid network IPv4 gateway")
235

    
236
    if subnet6:
237
        try:
238
            # Use strict option to not all subnets with host bits set
239
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
240
        except ValueError:
241
            raise faults.BadRequest("Invalid network IPv6 subnet")
242
        if gateway6:
243
            try:
244
                gateway6 = ipaddr.IPv6Address(gateway6)
245
            except ValueError:
246
                raise faults.BadRequest("Invalid network IPv6 gateway")
247
            if not gateway6 in network6:
248
                raise faults.BadRequest("Invalid network IPv6 gateway")
249

    
250

    
251
def validate_network_size(cidr_block):
252
    """Return True if network size is allowed."""
253
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
254

    
255

    
256
def allocate_public_address(backend):
257
    """Allocate a public IP for a vm."""
258
    for network in backend_public_networks(backend):
259
        try:
260
            address = get_network_free_address(network)
261
            return (network, address)
262
        except EmptyPool:
263
            pass
264
    return (None, None)
265

    
266

    
267
def get_public_ip(backend):
268
    """Reserve an IP from a public network.
269

270
    This method should run inside a transaction.
271

272
    """
273
    address = None
274
    if settings.PUBLIC_USE_POOL:
275
        (network, address) = allocate_public_address(backend)
276
    else:
277
        for net in list(backend_public_networks(backend)):
278
            pool = net.get_pool()
279
            if not pool.empty():
280
                address = 'pool'
281
                network = net
282
                break
283
    if address is None:
284
        log.error("Public networks of backend %s are full", backend)
285
        raise faults.OverLimit("Can not allocate IP for new machine."
286
                        " Public networks are full.")
287
    return (network, address)
288

    
289

    
290
def backend_public_networks(backend):
291
    """Return available public networks of the backend.
292

293
    Iterator for non-deleted public networks that are available
294
    to the specified backend.
295

296
    """
297
    for network in Network.objects.filter(public=True, deleted=False):
298
        if BackendNetwork.objects.filter(network=network,
299
                                         backend=backend).exists():
300
            yield network
301

    
302

    
303
def get_network_free_address(network):
304
    """Reserve an IP address from the IP Pool of the network.
305

306
    Raises EmptyPool
307

308
    """
309

    
310
    pool = network.get_pool()
311
    address = pool.get()
312
    pool.save()
313
    return address
314

    
315

    
316
def get_nic(machine, network):
317
    try:
318
        return NetworkInterface.objects.get(machine=machine, network=network)
319
    except NetworkInterface.DoesNotExist:
320
        raise faults.ItemNotFound('Server not connected to this network.')
321

    
322

    
323
def get_nic_from_index(vm, nic_index):
324
    """Returns the nic_index-th nic of a vm
325
       Error Response Codes: itemNotFound (404), badMediaType (415)
326
    """
327
    matching_nics = vm.nics.filter(index=nic_index)
328
    matching_nics_len = len(matching_nics)
329
    if matching_nics_len < 1:
330
        raise faults.ItemNotFound('NIC not found on VM')
331
    elif matching_nics_len > 1:
332
        raise faults.BadMediaType('NIC index conflict on VM')
333
    nic = matching_nics[0]
334
    return nic
335

    
336

    
337
def render_metadata(request, metadata, use_values=False, status=200):
338
    if request.serialization == 'xml':
339
        data = render_to_string('metadata.xml', {'metadata': metadata})
340
    else:
341
        if use_values:
342
            d = {'metadata': {'values': metadata}}
343
        else:
344
            d = {'metadata': metadata}
345
        data = json.dumps(d)
346
    return HttpResponse(data, status=status)
347

    
348

    
349
def render_meta(request, meta, status=200):
350
    if request.serialization == 'xml':
351
        data = render_to_string('meta.xml', dict(key=key, val=val))
352
    else:
353
        data = json.dumps(dict(meta=meta))
354
    return HttpResponse(data, status=status)
355

    
356

    
357
def construct_nic_id(nic):
358
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
359

    
360

    
361
def verify_personality(personality):
362
    """Verify that a a list of personalities is well formed"""
363
    if len(personality) > settings.MAX_PERSONALITY:
364
        raise faults.OverLimit("Maximum number of personalities"
365
                        " exceeded")
366
    for p in personality:
367
        # Verify that personalities are well-formed
368
        try:
369
            assert isinstance(p, dict)
370
            keys = set(p.keys())
371
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
372
            assert keys.issubset(allowed)
373
            contents = p['contents']
374
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
375
                # No need to decode if contents already exceed limit
376
                raise faults.OverLimit("Maximum size of personality exceeded")
377
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
378
                raise faults.OverLimit("Maximum size of personality exceeded")
379
        except AssertionError:
380
            raise faults.BadRequest("Malformed personality in request")
381

    
382

    
383
def values_from_flavor(flavor):
384
    """Get Ganeti connectivity info from flavor type.
385

386
    If link or mac_prefix equals to "pool", then the resources
387
    are allocated from the corresponding Pools.
388

389
    """
390
    try:
391
        flavor = Network.FLAVORS[flavor]
392
    except KeyError:
393
        raise faults.BadRequest("Unknown network flavor")
394

    
395
    mode = flavor.get("mode")
396

    
397
    link = flavor.get("link")
398
    if link == "pool":
399
        link = allocate_resource("bridge")
400

    
401
    mac_prefix = flavor.get("mac_prefix")
402
    if mac_prefix == "pool":
403
        mac_prefix = allocate_resource("mac_prefix")
404

    
405
    tags = flavor.get("tags")
406

    
407
    return mode, link, mac_prefix, tags
408

    
409

    
410
def allocate_resource(res_type):
411
    table = get_pool_table(res_type)
412
    pool = table.get_pool()
413
    value = pool.get()
414
    pool.save()
415
    return value
416

    
417

    
418
def release_resource(res_type, value):
419
    table = get_pool_table(res_type)
420
    pool = table.get_pool()
421
    pool.put(value)
422
    pool.save()
423

    
424

    
425
def get_pool_table(res_type):
426
    if res_type == "bridge":
427
        return BridgePoolTable
428
    elif res_type == "mac_prefix":
429
        return MacPrefixPoolTable
430
    else:
431
        raise Exception("Unknown resource type")
432

    
433

    
434
def get_existing_users():
435
    """
436
    Retrieve user ids stored in cyclades user agnostic models.
437
    """
438
    # also check PublicKeys a user with no servers/networks exist
439
    from synnefo.ui.userdata.models import PublicKeyPair
440
    from synnefo.db.models import VirtualMachine, Network
441

    
442
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
443
                                                                  flat=True)
444
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
445
                                                                  flat=True)
446
    networkusernames = Network.objects.filter().values_list('userid',
447
                                                            flat=True)
448

    
449
    return set(list(keypairusernames) + list(serverusernames) +
450
               list(networkusernames))