Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 3d5906fe

History | View | Annotate | Download (16.4 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
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
70
from synnefo.lib.services import get_service_path
71
from synnefo.lib import join_urls
72

    
73
COMPUTE_URL = \
74
    join_urls(BASE_HOST,
75
              get_service_path(cyclades_services, "compute", version="v2.0"))
76
SERVERS_URL = join_urls(COMPUTE_URL, "servers/")
77
NETWORKS_URL = join_urls(COMPUTE_URL, "networks/")
78
FLAVORS_URL = join_urls(COMPUTE_URL, "flavors/")
79
IMAGES_URL = join_urls(COMPUTE_URL, "images/")
80
PLANKTON_URL = \
81
    join_urls(BASE_HOST,
82
              get_service_path(cyclades_services, "image", version="v1.0"))
83
IMAGES_PLANKTON_URL = join_urls(PLANKTON_URL, "images/")
84

    
85
PITHOSMAP_PREFIX = "pithosmap://"
86

    
87
log = getLogger('synnefo.api')
88

    
89

    
90
def random_password():
91
    """Generates a random password
92

93
    We generate a windows compliant password: it must contain at least
94
    one charachter from each of the groups: upper case, lower case, digits.
95
    """
96

    
97
    pool = lowercase + uppercase + digits
98
    lowerset = set(lowercase)
99
    upperset = set(uppercase)
100
    digitset = set(digits)
101
    length = 10
102

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

    
105
    # Make sure the password is compliant
106
    chars = set(password)
107
    if not chars & lowerset:
108
        password += choice(lowercase)
109
    if not chars & upperset:
110
        password += choice(uppercase)
111
    if not chars & digitset:
112
        password += choice(digits)
113

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

    
117
    return password
118

    
119

    
120
def zeropad(s):
121
    """Add zeros at the end of a string in order to make its length
122
       a multiple of 16."""
123

    
124
    npad = 16 - len(s) % 16
125
    return s + '\x00' * npad
126

    
127

    
128
def encrypt(plaintext):
129
    # Make sure key is 32 bytes long
130
    key = sha256(settings.SECRET_KEY).digest()
131

    
132
    aes = AES.new(key)
133
    enc = aes.encrypt(zeropad(plaintext))
134
    return b64encode(enc)
135

    
136

    
137
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
138
           non_suspended=False):
139
    """Find a VirtualMachine instance based on ID and owner."""
140

    
141
    try:
142
        server_id = int(server_id)
143
        servers = VirtualMachine.objects
144
        if for_update:
145
            servers = servers.select_for_update()
146
        vm = servers.get(id=server_id, userid=user_id)
147
        if non_deleted and vm.deleted:
148
            raise faults.BadRequest("Server has been deleted.")
149
        if non_suspended and vm.suspended:
150
            raise faults.Forbidden("Administratively Suspended VM")
151
        return vm
152
    except ValueError:
153
        raise faults.BadRequest('Invalid server ID.')
154
    except VirtualMachine.DoesNotExist:
155
        raise faults.ItemNotFound('Server not found.')
156

    
157

    
158
def get_vm_meta(vm, key):
159
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
160

    
161
    try:
162
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
163
    except VirtualMachineMetadata.DoesNotExist:
164
        raise faults.ItemNotFound('Metadata key not found.')
165

    
166

    
167
def get_image(image_id, user_id):
168
    """Return an Image instance or raise ItemNotFound."""
169

    
170
    with image_backend(user_id) as backend:
171
        return backend.get_image(image_id)
172

    
173

    
174
def get_image_dict(image_id, user_id):
175
    image = {}
176
    img = get_image(image_id, user_id)
177
    image["id"] = img["id"]
178
    image["name"] = img["name"]
179
    image["format"] = img["disk_format"]
180
    image["checksum"] = img["checksum"]
181
    image["location"] = img["location"]
182

    
183
    checksum = image["checksum"] = img["checksum"]
184
    size = image["size"] = img["size"]
185
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([checksum, str(size)])
186

    
187
    properties = img.get("properties", {})
188
    image["metadata"] = dict((key.upper(), val)
189
                             for key, val in properties.items())
190

    
191

    
192
    return image
193

    
194

    
195
def get_flavor(flavor_id, include_deleted=False):
196
    """Return a Flavor instance or raise ItemNotFound."""
197

    
198
    try:
199
        flavor_id = int(flavor_id)
200
        if include_deleted:
201
            return Flavor.objects.get(id=flavor_id)
202
        else:
203
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
204
    except (ValueError, Flavor.DoesNotExist):
205
        raise faults.ItemNotFound('Flavor not found.')
206

    
207

    
208
def get_flavor_provider(flavor):
209
    """Extract provider from disk template.
210

211
    Provider for `ext` disk_template is encoded in the disk template
212
    name, which is formed `ext_<provider_name>`. Provider is None
213
    for all other disk templates.
214

215
    """
216
    disk_template = flavor.disk_template
217
    provider = None
218
    if disk_template.startswith("ext"):
219
        disk_template, provider = disk_template.split("_", 1)
220
    return disk_template, provider
221

    
222

    
223
def get_network(network_id, user_id, for_update=False):
224
    """Return a Network instance or raise ItemNotFound."""
225

    
226
    try:
227
        network_id = int(network_id)
228
        objects = Network.objects
229
        if for_update:
230
            objects = objects.select_for_update()
231
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
232
    except (ValueError, Network.DoesNotExist):
233
        raise faults.ItemNotFound('Network not found.')
234

    
235

    
236
def validate_network_params(subnet, gateway=None, subnet6=None, gateway6=None):
237
    try:
238
        # Use strict option to not all subnets with host bits set
239
        network = ipaddr.IPv4Network(subnet, strict=True)
240
    except ValueError:
241
        raise faults.BadRequest("Invalid network IPv4 subnet")
242

    
243
    # Check that network size is allowed!
244
    if not validate_network_size(network.prefixlen):
245
        raise faults.OverLimit(message="Unsupported network size",
246
                        details="Network mask must be in range (%s, 29]" %
247
                                MAX_CIDR_BLOCK)
248

    
249
    # Check that gateway belongs to network
250
    if gateway:
251
        try:
252
            gateway = ipaddr.IPv4Address(gateway)
253
        except ValueError:
254
            raise faults.BadRequest("Invalid network IPv4 gateway")
255
        if not gateway in network:
256
            raise faults.BadRequest("Invalid network IPv4 gateway")
257

    
258
    if subnet6:
259
        try:
260
            # Use strict option to not all subnets with host bits set
261
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
262
        except ValueError:
263
            raise faults.BadRequest("Invalid network IPv6 subnet")
264
        if gateway6:
265
            try:
266
                gateway6 = ipaddr.IPv6Address(gateway6)
267
            except ValueError:
268
                raise faults.BadRequest("Invalid network IPv6 gateway")
269
            if not gateway6 in network6:
270
                raise faults.BadRequest("Invalid network IPv6 gateway")
271

    
272

    
273
def validate_network_size(cidr_block):
274
    """Return True if network size is allowed."""
275
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
276

    
277

    
278
def allocate_public_address(backend):
279
    """Allocate a public IP for a vm."""
280
    for network in backend_public_networks(backend):
281
        try:
282
            address = get_network_free_address(network)
283
            return (network, address)
284
        except EmptyPool:
285
            pass
286
    return (None, None)
287

    
288

    
289
def get_public_ip(backend):
290
    """Reserve an IP from a public network.
291

292
    This method should run inside a transaction.
293

294
    """
295

    
296
    # Guarantee exclusive access to backend, because accessing the IP pools of
297
    # the backend networks may result in a deadlock with backend allocator
298
    # which also checks that backend networks have a free IP.
299
    backend = Backend.objects.select_for_update().get(id=backend.id)
300

    
301
    address = None
302
    if settings.PUBLIC_USE_POOL:
303
        (network, address) = allocate_public_address(backend)
304
    else:
305
        for net in list(backend_public_networks(backend)):
306
            pool = net.get_pool()
307
            if not pool.empty():
308
                address = 'pool'
309
                network = net
310
                break
311
    if address is None:
312
        log.error("Public networks of backend %s are full", backend)
313
        raise faults.OverLimit("Can not allocate IP for new machine."
314
                        " Public networks are full.")
315
    return (network, address)
316

    
317

    
318
def backend_public_networks(backend):
319
    """Return available public networks of the backend.
320

321
    Iterator for non-deleted public networks that are available
322
    to the specified backend.
323

324
    """
325
    for network in Network.objects.filter(public=True, deleted=False,
326
                                          drained=False):
327
        if BackendNetwork.objects.filter(network=network,
328
                                         backend=backend).exists():
329
            yield network
330

    
331

    
332
def get_network_free_address(network):
333
    """Reserve an IP address from the IP Pool of the network.
334

335
    Raises EmptyPool
336

337
    """
338

    
339
    pool = network.get_pool()
340
    address = pool.get()
341
    pool.save()
342
    return address
343

    
344

    
345
def get_nic(machine, network):
346
    try:
347
        return NetworkInterface.objects.get(machine=machine, network=network)
348
    except NetworkInterface.DoesNotExist:
349
        raise faults.ItemNotFound('Server not connected to this network.')
350

    
351

    
352
def get_nic_from_index(vm, nic_index):
353
    """Returns the nic_index-th nic of a vm
354
       Error Response Codes: itemNotFound (404), badMediaType (415)
355
    """
356
    matching_nics = vm.nics.filter(index=nic_index)
357
    matching_nics_len = len(matching_nics)
358
    if matching_nics_len < 1:
359
        raise faults.ItemNotFound('NIC not found on VM')
360
    elif matching_nics_len > 1:
361
        raise faults.BadMediaType('NIC index conflict on VM')
362
    nic = matching_nics[0]
363
    return nic
364

    
365

    
366
def render_metadata(request, metadata, use_values=False, status=200):
367
    if request.serialization == 'xml':
368
        data = render_to_string('metadata.xml', {'metadata': metadata})
369
    else:
370
        if use_values:
371
            d = {'metadata': {'values': metadata}}
372
        else:
373
            d = {'metadata': metadata}
374
        data = json.dumps(d)
375
    return HttpResponse(data, status=status)
376

    
377

    
378
def render_meta(request, meta, status=200):
379
    if request.serialization == 'xml':
380
        key, val = meta.items()[0]
381
        data = render_to_string('meta.xml', dict(key=key, val=val))
382
    else:
383
        data = json.dumps(dict(meta=meta))
384
    return HttpResponse(data, status=status)
385

    
386

    
387
def construct_nic_id(nic):
388
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
389

    
390

    
391
def verify_personality(personality):
392
    """Verify that a a list of personalities is well formed"""
393
    if len(personality) > settings.MAX_PERSONALITY:
394
        raise faults.OverLimit("Maximum number of personalities"
395
                        " exceeded")
396
    for p in personality:
397
        # Verify that personalities are well-formed
398
        try:
399
            assert isinstance(p, dict)
400
            keys = set(p.keys())
401
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
402
            assert keys.issubset(allowed)
403
            contents = p['contents']
404
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
405
                # No need to decode if contents already exceed limit
406
                raise faults.OverLimit("Maximum size of personality exceeded")
407
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
408
                raise faults.OverLimit("Maximum size of personality exceeded")
409
        except AssertionError:
410
            raise faults.BadRequest("Malformed personality in request")
411

    
412

    
413
def values_from_flavor(flavor):
414
    """Get Ganeti connectivity info from flavor type.
415

416
    If link or mac_prefix equals to "pool", then the resources
417
    are allocated from the corresponding Pools.
418

419
    """
420
    try:
421
        flavor = Network.FLAVORS[flavor]
422
    except KeyError:
423
        raise faults.BadRequest("Unknown network flavor")
424

    
425
    mode = flavor.get("mode")
426

    
427
    link = flavor.get("link")
428
    if link == "pool":
429
        link = allocate_resource("bridge")
430

    
431
    mac_prefix = flavor.get("mac_prefix")
432
    if mac_prefix == "pool":
433
        mac_prefix = allocate_resource("mac_prefix")
434

    
435
    tags = flavor.get("tags")
436

    
437
    return mode, link, mac_prefix, tags
438

    
439

    
440
def allocate_resource(res_type):
441
    table = get_pool_table(res_type)
442
    pool = table.get_pool()
443
    value = pool.get()
444
    pool.save()
445
    return value
446

    
447

    
448
def release_resource(res_type, value):
449
    table = get_pool_table(res_type)
450
    pool = table.get_pool()
451
    pool.put(value)
452
    pool.save()
453

    
454

    
455
def get_pool_table(res_type):
456
    if res_type == "bridge":
457
        return BridgePoolTable
458
    elif res_type == "mac_prefix":
459
        return MacPrefixPoolTable
460
    else:
461
        raise Exception("Unknown resource type")
462

    
463

    
464
def get_existing_users():
465
    """
466
    Retrieve user ids stored in cyclades user agnostic models.
467
    """
468
    # also check PublicKeys a user with no servers/networks exist
469
    from synnefo.userdata.models import PublicKeyPair
470
    from synnefo.db.models import VirtualMachine, Network
471

    
472
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
473
                                                                  flat=True)
474
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
475
                                                                  flat=True)
476
    networkusernames = Network.objects.filter().values_list('userid',
477
                                                            flat=True)
478

    
479
    return set(list(keypairusernames) + list(serverusernames) +
480
               list(networkusernames))
481

    
482

    
483
def vm_to_links(vm_id):
484
    href = join_urls(SERVERS_URL, str(vm_id))
485
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
486

    
487

    
488
def network_to_links(network_id):
489
    href = join_urls(NETWORKS_URL, str(network_id))
490
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
491

    
492

    
493
def flavor_to_links(flavor_id):
494
    href = join_urls(FLAVORS_URL, str(flavor_id))
495
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
496

    
497

    
498
def image_to_links(image_id):
499
    href = join_urls(IMAGES_URL, str(image_id))
500
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
501
    links.append({"rel": "alternate",
502
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
503
    return links