Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 85919a0f

History | View | Annotate | Download (14.6 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, Backend)
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
        return backend.get_image(image_id)
155

    
156

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

    
167
    return image
168

    
169

    
170
def get_flavor(flavor_id, include_deleted=False):
171
    """Return a Flavor instance or raise ItemNotFound."""
172

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

    
182

    
183
def get_flavor_provider(flavor):
184
    """Extract provider from disk template.
185

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

190
    """
191
    disk_template = flavor.disk_template
192
    provider = None
193
    if disk_template.startswith("ext"):
194
        disk_template, provider = disk_template.split("_", 1)
195
    return disk_template, provider
196

    
197

    
198
def get_network(network_id, user_id, for_update=False):
199
    """Return a Network instance or raise ItemNotFound."""
200

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

    
210

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

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

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

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

    
247

    
248
def validate_network_size(cidr_block):
249
    """Return True if network size is allowed."""
250
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
251

    
252

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

    
263

    
264
def get_public_ip(backend):
265
    """Reserve an IP from a public network.
266

267
    This method should run inside a transaction.
268

269
    """
270

    
271
    # Guarantee exclusive access to backend, because accessing the IP pools of
272
    # the backend networks may result in a deadlock with backend allocator
273
    # which also checks that backend networks have a free IP.
274
    backend = Backend.objects.select_for_update().get(id=backend.id)
275

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

    
292

    
293
def backend_public_networks(backend):
294
    """Return available public networks of the backend.
295

296
    Iterator for non-deleted public networks that are available
297
    to the specified backend.
298

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

    
305

    
306
def get_network_free_address(network):
307
    """Reserve an IP address from the IP Pool of the network.
308

309
    Raises EmptyPool
310

311
    """
312

    
313
    pool = network.get_pool()
314
    address = pool.get()
315
    pool.save()
316
    return address
317

    
318

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

    
325

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

    
339

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

    
351

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

    
359

    
360
def construct_nic_id(nic):
361
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
362

    
363

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

    
385

    
386
def values_from_flavor(flavor):
387
    """Get Ganeti connectivity info from flavor type.
388

389
    If link or mac_prefix equals to "pool", then the resources
390
    are allocated from the corresponding Pools.
391

392
    """
393
    try:
394
        flavor = Network.FLAVORS[flavor]
395
    except KeyError:
396
        raise faults.BadRequest("Unknown network flavor")
397

    
398
    mode = flavor.get("mode")
399

    
400
    link = flavor.get("link")
401
    if link == "pool":
402
        link = allocate_resource("bridge")
403

    
404
    mac_prefix = flavor.get("mac_prefix")
405
    if mac_prefix == "pool":
406
        mac_prefix = allocate_resource("mac_prefix")
407

    
408
    tags = flavor.get("tags")
409

    
410
    return mode, link, mac_prefix, tags
411

    
412

    
413
def allocate_resource(res_type):
414
    table = get_pool_table(res_type)
415
    pool = table.get_pool()
416
    value = pool.get()
417
    pool.save()
418
    return value
419

    
420

    
421
def release_resource(res_type, value):
422
    table = get_pool_table(res_type)
423
    pool = table.get_pool()
424
    pool.put(value)
425
    pool.save()
426

    
427

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

    
436

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

    
445
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
446
                                                                  flat=True)
447
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
448
                                                                  flat=True)
449
    networkusernames = Network.objects.filter().values_list('userid',
450
                                                            flat=True)
451

    
452
    return set(list(keypairusernames) + list(serverusernames) +
453
               list(networkusernames))