Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 64cdd31b

History | View | Annotate | Download (17.2 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, non_deleted=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
        network = objects.get(Q(userid=user_id) | Q(public=True),
225
                              id=network_id)
226
        if non_deleted and network.deleted:
227
            raise faults.BadRequest("Networkhas been deleted.")
228
        return network
229
    except (ValueError, Network.DoesNotExist):
230
        raise faults.ItemNotFound('Network not found.')
231

    
232

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

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

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

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

    
269

    
270
def validate_network_size(cidr_block):
271
    """Return True if network size is allowed."""
272
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
273

    
274

    
275
def allocate_public_address(backend):
276
    """Allocate a public IP for a vm."""
277
    for network in backend_public_networks(backend):
278
        try:
279
            address = get_network_free_address(network)
280
        except faults.OverLimit:
281
            pass
282
        else:
283
            return (network, address)
284
    return (None, None)
285

    
286

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

290
    This method should run inside a transaction.
291

292
    """
293

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

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

    
315

    
316
def backend_public_networks(backend):
317
    """Return available public networks of the backend.
318

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

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

    
329

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

    
333
    pool = network.get_pool()
334
    try:
335
        address = pool.get()
336
    except EmptyPool:
337
        raise faults.OverLimit("Network %s is full." % network.backend_id)
338
        address = None
339
    pool.save()
340
    return address
341

    
342

    
343
def allocate_public_ip(networks=None):
344
    """Allocate an IP address from public networks."""
345
    if networks is None:
346
        networks = Network.objects.select_for_update().filter(public=True,
347
                                                              deleted=False)
348
    for network in networks:
349
        try:
350
            address = get_network_free_address(network)
351
        except:
352
            pass
353
        else:
354
            return network, address
355
    msg = "Can not allocate public IP. Public networks are full."
356
    log.error(msg)
357
    raise faults.OverLimit(msg)
358

    
359

    
360
def get_nic(machine, network):
361
    try:
362
        return NetworkInterface.objects.get(machine=machine, network=network)
363
    except NetworkInterface.DoesNotExist:
364
        raise faults.ItemNotFound('Server not connected to this network.')
365

    
366

    
367
def get_nic_from_index(vm, nic_index):
368
    """Returns the nic_index-th nic of a vm
369
       Error Response Codes: itemNotFound (404), badMediaType (415)
370
    """
371
    matching_nics = vm.nics.filter(index=nic_index)
372
    matching_nics_len = len(matching_nics)
373
    if matching_nics_len < 1:
374
        raise faults.ItemNotFound('NIC not found on VM')
375
    elif matching_nics_len > 1:
376
        raise faults.BadMediaType('NIC index conflict on VM')
377
    nic = matching_nics[0]
378
    return nic
379

    
380

    
381
def render_metadata(request, metadata, use_values=False, status=200):
382
    if request.serialization == 'xml':
383
        data = render_to_string('metadata.xml', {'metadata': metadata})
384
    else:
385
        if use_values:
386
            d = {'metadata': {'values': metadata}}
387
        else:
388
            d = {'metadata': metadata}
389
        data = json.dumps(d)
390
    return HttpResponse(data, status=status)
391

    
392

    
393
def render_meta(request, meta, status=200):
394
    if request.serialization == 'xml':
395
        data = render_to_string('meta.xml', dict(key=key, val=val))
396
    else:
397
        data = json.dumps(dict(meta=meta))
398
    return HttpResponse(data, status=status)
399

    
400

    
401
def construct_nic_id(nic):
402
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
403

    
404

    
405
def verify_personality(personality):
406
    """Verify that a a list of personalities is well formed"""
407
    if len(personality) > settings.MAX_PERSONALITY:
408
        raise faults.OverLimit("Maximum number of personalities"
409
                        " exceeded")
410
    for p in personality:
411
        # Verify that personalities are well-formed
412
        try:
413
            assert isinstance(p, dict)
414
            keys = set(p.keys())
415
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
416
            assert keys.issubset(allowed)
417
            contents = p['contents']
418
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
419
                # No need to decode if contents already exceed limit
420
                raise faults.OverLimit("Maximum size of personality exceeded")
421
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
422
                raise faults.OverLimit("Maximum size of personality exceeded")
423
        except AssertionError:
424
            raise faults.BadRequest("Malformed personality in request")
425

    
426

    
427
def values_from_flavor(flavor):
428
    """Get Ganeti connectivity info from flavor type.
429

430
    If link or mac_prefix equals to "pool", then the resources
431
    are allocated from the corresponding Pools.
432

433
    """
434
    try:
435
        flavor = Network.FLAVORS[flavor]
436
    except KeyError:
437
        raise faults.BadRequest("Unknown network flavor")
438

    
439
    mode = flavor.get("mode")
440

    
441
    link = flavor.get("link")
442
    if link == "pool":
443
        link = allocate_resource("bridge")
444

    
445
    mac_prefix = flavor.get("mac_prefix")
446
    if mac_prefix == "pool":
447
        mac_prefix = allocate_resource("mac_prefix")
448

    
449
    tags = flavor.get("tags")
450

    
451
    return mode, link, mac_prefix, tags
452

    
453

    
454
def allocate_resource(res_type):
455
    table = get_pool_table(res_type)
456
    pool = table.get_pool()
457
    value = pool.get()
458
    pool.save()
459
    return value
460

    
461

    
462
def release_resource(res_type, value):
463
    table = get_pool_table(res_type)
464
    pool = table.get_pool()
465
    pool.put(value)
466
    pool.save()
467

    
468

    
469
def get_pool_table(res_type):
470
    if res_type == "bridge":
471
        return BridgePoolTable
472
    elif res_type == "mac_prefix":
473
        return MacPrefixPoolTable
474
    else:
475
        raise Exception("Unknown resource type")
476

    
477

    
478
def get_existing_users():
479
    """
480
    Retrieve user ids stored in cyclades user agnostic models.
481
    """
482
    # also check PublicKeys a user with no servers/networks exist
483
    from synnefo.userdata.models import PublicKeyPair
484
    from synnefo.db.models import VirtualMachine, Network
485

    
486
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
487
                                                                  flat=True)
488
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
489
                                                                  flat=True)
490
    networkusernames = Network.objects.filter().values_list('userid',
491
                                                            flat=True)
492

    
493
    return set(list(keypairusernames) + list(serverusernames) +
494
               list(networkusernames))
495

    
496

    
497
def vm_to_links(vm_id):
498
    href = join_urls(SERVERS_URL, str(vm_id))
499
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
500

    
501

    
502
def network_to_links(network_id):
503
    href = join_urls(NETWORKS_URL, str(network_id))
504
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
505

    
506

    
507
def flavor_to_links(flavor_id):
508
    href = join_urls(FLAVORS_URL, str(flavor_id))
509
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
510

    
511

    
512
def image_to_links(image_id):
513
    href = join_urls(IMAGES_URL, str(image_id))
514
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
515
    links.append({"rel": "alternate",
516
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
517
    return links
518

    
519
def start_action(vm, action, jobId):
520
    vm.action = action
521
    vm.backendjobid = jobId
522
    vm.backendopcode = None
523
    vm.backendjobstatus = None
524
    vm.backendlogmsg = None
525
    vm.save()