Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 1b696c26

History | View | Annotate | Download (16.1 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

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

    
88

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

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

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

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

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

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

    
116
    return password
117

    
118

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

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

    
126

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

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

    
135

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

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

    
156

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

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

    
165

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

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

    
172

    
173
def get_image_dict(image_id, user_id):
174
    image = {}
175
    img = get_image(image_id, user_id)
176
    properties = img.get('properties', {})
177
    image["id"] = img["id"]
178
    image["name"] = img["name"]
179
    image['backend_id'] = img['location']
180
    image['format'] = img['disk_format']
181
    image['metadata'] = dict((key.upper(), val)
182
                             for key, val in properties.items())
183
    image['checksum'] = img['checksum']
184

    
185
    return image
186

    
187

    
188
def get_flavor(flavor_id, include_deleted=False):
189
    """Return a Flavor instance or raise ItemNotFound."""
190

    
191
    try:
192
        flavor_id = int(flavor_id)
193
        if include_deleted:
194
            return Flavor.objects.get(id=flavor_id)
195
        else:
196
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
197
    except (ValueError, Flavor.DoesNotExist):
198
        raise faults.ItemNotFound('Flavor not found.')
199

    
200

    
201
def get_flavor_provider(flavor):
202
    """Extract provider from disk template.
203

204
    Provider for `ext` disk_template is encoded in the disk template
205
    name, which is formed `ext_<provider_name>`. Provider is None
206
    for all other disk templates.
207

208
    """
209
    disk_template = flavor.disk_template
210
    provider = None
211
    if disk_template.startswith("ext"):
212
        disk_template, provider = disk_template.split("_", 1)
213
    return disk_template, provider
214

    
215

    
216
def get_network(network_id, user_id, for_update=False):
217
    """Return a Network instance or raise ItemNotFound."""
218

    
219
    try:
220
        network_id = int(network_id)
221
        objects = Network.objects
222
        if for_update:
223
            objects = objects.select_for_update()
224
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
225
    except (ValueError, Network.DoesNotExist):
226
        raise faults.ItemNotFound('Network not found.')
227

    
228

    
229
def validate_network_params(subnet, gateway=None, subnet6=None, gateway6=None):
230
    try:
231
        # Use strict option to not all subnets with host bits set
232
        network = ipaddr.IPv4Network(subnet, strict=True)
233
    except ValueError:
234
        raise faults.BadRequest("Invalid network IPv4 subnet")
235

    
236
    # Check that network size is allowed!
237
    if not validate_network_size(network.prefixlen):
238
        raise faults.OverLimit(message="Unsupported network size",
239
                        details="Network mask must be in range (%s, 29]" %
240
                                MAX_CIDR_BLOCK)
241

    
242
    # Check that gateway belongs to network
243
    if gateway:
244
        try:
245
            gateway = ipaddr.IPv4Address(gateway)
246
        except ValueError:
247
            raise faults.BadRequest("Invalid network IPv4 gateway")
248
        if not gateway in network:
249
            raise faults.BadRequest("Invalid network IPv4 gateway")
250

    
251
    if subnet6:
252
        try:
253
            # Use strict option to not all subnets with host bits set
254
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
255
        except ValueError:
256
            raise faults.BadRequest("Invalid network IPv6 subnet")
257
        if gateway6:
258
            try:
259
                gateway6 = ipaddr.IPv6Address(gateway6)
260
            except ValueError:
261
                raise faults.BadRequest("Invalid network IPv6 gateway")
262
            if not gateway6 in network6:
263
                raise faults.BadRequest("Invalid network IPv6 gateway")
264

    
265

    
266
def validate_network_size(cidr_block):
267
    """Return True if network size is allowed."""
268
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
269

    
270

    
271
def allocate_public_address(backend):
272
    """Allocate a public IP for a vm."""
273
    for network in backend_public_networks(backend):
274
        try:
275
            address = get_network_free_address(network)
276
            return (network, address)
277
        except EmptyPool:
278
            pass
279
    return (None, None)
280

    
281

    
282
def get_public_ip(backend):
283
    """Reserve an IP from a public network.
284

285
    This method should run inside a transaction.
286

287
    """
288

    
289
    # Guarantee exclusive access to backend, because accessing the IP pools of
290
    # the backend networks may result in a deadlock with backend allocator
291
    # which also checks that backend networks have a free IP.
292
    backend = Backend.objects.select_for_update().get(id=backend.id)
293

    
294
    address = None
295
    if settings.PUBLIC_USE_POOL:
296
        (network, address) = allocate_public_address(backend)
297
    else:
298
        for net in list(backend_public_networks(backend)):
299
            pool = net.get_pool()
300
            if not pool.empty():
301
                address = 'pool'
302
                network = net
303
                break
304
    if address is None:
305
        log.error("Public networks of backend %s are full", backend)
306
        raise faults.OverLimit("Can not allocate IP for new machine."
307
                        " Public networks are full.")
308
    return (network, address)
309

    
310

    
311
def backend_public_networks(backend):
312
    """Return available public networks of the backend.
313

314
    Iterator for non-deleted public networks that are available
315
    to the specified backend.
316

317
    """
318
    for network in Network.objects.filter(public=True, deleted=False,
319
                                          drained=False):
320
        if BackendNetwork.objects.filter(network=network,
321
                                         backend=backend).exists():
322
            yield network
323

    
324

    
325
def get_network_free_address(network):
326
    """Reserve an IP address from the IP Pool of the network.
327

328
    Raises EmptyPool
329

330
    """
331

    
332
    pool = network.get_pool()
333
    address = pool.get()
334
    pool.save()
335
    return address
336

    
337

    
338
def get_nic(machine, network):
339
    try:
340
        return NetworkInterface.objects.get(machine=machine, network=network)
341
    except NetworkInterface.DoesNotExist:
342
        raise faults.ItemNotFound('Server not connected to this network.')
343

    
344

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

    
358

    
359
def render_metadata(request, metadata, use_values=False, status=200):
360
    if request.serialization == 'xml':
361
        data = render_to_string('metadata.xml', {'metadata': metadata})
362
    else:
363
        if use_values:
364
            d = {'metadata': {'values': metadata}}
365
        else:
366
            d = {'metadata': metadata}
367
        data = json.dumps(d)
368
    return HttpResponse(data, status=status)
369

    
370

    
371
def render_meta(request, meta, status=200):
372
    if request.serialization == 'xml':
373
        data = render_to_string('meta.xml', dict(key=key, val=val))
374
    else:
375
        data = json.dumps(dict(meta=meta))
376
    return HttpResponse(data, status=status)
377

    
378

    
379
def construct_nic_id(nic):
380
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
381

    
382

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

    
404

    
405
def values_from_flavor(flavor):
406
    """Get Ganeti connectivity info from flavor type.
407

408
    If link or mac_prefix equals to "pool", then the resources
409
    are allocated from the corresponding Pools.
410

411
    """
412
    try:
413
        flavor = Network.FLAVORS[flavor]
414
    except KeyError:
415
        raise faults.BadRequest("Unknown network flavor")
416

    
417
    mode = flavor.get("mode")
418

    
419
    link = flavor.get("link")
420
    if link == "pool":
421
        link = allocate_resource("bridge")
422

    
423
    mac_prefix = flavor.get("mac_prefix")
424
    if mac_prefix == "pool":
425
        mac_prefix = allocate_resource("mac_prefix")
426

    
427
    tags = flavor.get("tags")
428

    
429
    return mode, link, mac_prefix, tags
430

    
431

    
432
def allocate_resource(res_type):
433
    table = get_pool_table(res_type)
434
    pool = table.get_pool()
435
    value = pool.get()
436
    pool.save()
437
    return value
438

    
439

    
440
def release_resource(res_type, value):
441
    table = get_pool_table(res_type)
442
    pool = table.get_pool()
443
    pool.put(value)
444
    pool.save()
445

    
446

    
447
def get_pool_table(res_type):
448
    if res_type == "bridge":
449
        return BridgePoolTable
450
    elif res_type == "mac_prefix":
451
        return MacPrefixPoolTable
452
    else:
453
        raise Exception("Unknown resource type")
454

    
455

    
456
def get_existing_users():
457
    """
458
    Retrieve user ids stored in cyclades user agnostic models.
459
    """
460
    # also check PublicKeys a user with no servers/networks exist
461
    from synnefo.userdata.models import PublicKeyPair
462
    from synnefo.db.models import VirtualMachine, Network
463

    
464
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
465
                                                                  flat=True)
466
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
467
                                                                  flat=True)
468
    networkusernames = Network.objects.filter().values_list('userid',
469
                                                            flat=True)
470

    
471
    return set(list(keypairusernames) + list(serverusernames) +
472
               list(networkusernames))
473

    
474

    
475
def vm_to_links(vm_id):
476
    link = join_urls(SERVERS_URL, str(vm_id))
477
    return [{"ref": rel, "link": link} for rel in ("self", "bookmark")]
478

    
479

    
480
def network_to_links(network_id):
481
    link = join_urls(NETWORKS_URL, str(network_id))
482
    return [{"ref": rel, "link": link} for rel in ("self", "bookmark")]
483

    
484

    
485
def flavor_to_links(flavor_id):
486
    link = join_urls(FLAVORS_URL, str(flavor_id))
487
    return [{"ref": rel, "link": link} for rel in ("self", "bookmark")]
488

    
489

    
490
def image_to_links(image_id):
491
    link = join_urls(IMAGES_URL, str(image_id))
492
    links = [{"ref": rel, "link": link} for rel in ("self", "bookmark")]
493
    links.append({"rel": "alternate",
494
                  "link": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
495
    return links