Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 660b9f3b

History | View | Annotate | Download (16.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, Backend,
63
                               FloatingIP)
64
from synnefo.db.pools import EmptyPool
65

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

    
70
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
71
from synnefo.lib.services import get_service_path
72
from synnefo.lib import join_urls
73

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

    
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
    properties = img.get('properties', {})
178
    image["id"] = img["id"]
179
    image["name"] = img["name"]
180
    image['backend_id'] = img['location']
181
    image['format'] = img['disk_format']
182
    image['metadata'] = dict((key.upper(), val)
183
                             for key, val in properties.items())
184
    image['checksum'] = img['checksum']
185

    
186
    return image
187

    
188

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

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

    
201

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

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

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

    
216

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

    
220
    try:
221
        network_id = int(network_id)
222
        objects = Network.objects
223
        if for_update:
224
            objects = objects.select_for_update()
225
        network = objects.get(Q(userid=user_id) | Q(public=True),
226
                              id=network_id)
227
        if non_deleted and network.deleted:
228
            raise faults.BadRequest("Network has been deleted.")
229
        return network
230
    except (ValueError, Network.DoesNotExist):
231
        raise faults.ItemNotFound('Network not found.')
232

    
233

    
234
def get_floating_ip(user_id, ipv4, for_update=False):
235
    try:
236
        objects = FloatingIP.objects
237
        if for_update:
238
            objects = objects.select_for_update()
239
        return objects.get(userid=user_id, ipv4=ipv4, deleted=False)
240
    except FloatingIP.DoesNotExist:
241
        raise faults.ItemNotFound("Floating IP does not exist.")
242

    
243

    
244
def validate_network_params(subnet, gateway=None, subnet6=None, gateway6=None):
245
    try:
246
        # Use strict option to not all subnets with host bits set
247
        network = ipaddr.IPv4Network(subnet, strict=True)
248
    except ValueError:
249
        raise faults.BadRequest("Invalid network IPv4 subnet")
250

    
251
    # Check that network size is allowed!
252
    if not validate_network_size(network.prefixlen):
253
        raise faults.OverLimit(message="Unsupported network size",
254
                        details="Network mask must be in range (%s, 29]" %
255
                                MAX_CIDR_BLOCK)
256

    
257
    # Check that gateway belongs to network
258
    if gateway:
259
        try:
260
            gateway = ipaddr.IPv4Address(gateway)
261
        except ValueError:
262
            raise faults.BadRequest("Invalid network IPv4 gateway")
263
        if not gateway in network:
264
            raise faults.BadRequest("Invalid network IPv4 gateway")
265

    
266
    if subnet6:
267
        try:
268
            # Use strict option to not all subnets with host bits set
269
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
270
        except ValueError:
271
            raise faults.BadRequest("Invalid network IPv6 subnet")
272
        if gateway6:
273
            try:
274
                gateway6 = ipaddr.IPv6Address(gateway6)
275
            except ValueError:
276
                raise faults.BadRequest("Invalid network IPv6 gateway")
277
            if not gateway6 in network6:
278
                raise faults.BadRequest("Invalid network IPv6 gateway")
279

    
280

    
281
def validate_network_size(cidr_block):
282
    """Return True if network size is allowed."""
283
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
284

    
285

    
286
def allocate_public_address(backend):
287
    """Get a public IP for any available network of a backend."""
288
    # Guarantee exclusive access to backend, because accessing the IP pools of
289
    # the backend networks may result in a deadlock with backend allocator
290
    # which also checks that backend networks have a free IP.
291
    backend = Backend.objects.select_for_update().get(id=backend.id)
292
    public_networks = backend_public_networks(backend)
293
    return get_free_ip(public_networks)
294

    
295

    
296
def backend_public_networks(backend):
297
    """Return available public networks of the backend.
298

299
    Iterator for non-deleted public networks that are available
300
    to the specified backend.
301

302
    """
303
    bnets = BackendNetwork.objects.filter(backend=backend,
304
                                          network__public=True,
305
                                          network__deleted=False,
306
                                          network__drained=False)
307
    return [b.network for b in bnets]
308

    
309

    
310
def get_free_ip(networks):
311
    for network in networks:
312
        try:
313
            address = get_network_free_address(network)
314
            return network, address
315
        except faults.OverLimit:
316
            pass
317
    msg = "Can not allocate public IP. Public networks are full."
318
    log.error(msg)
319
    raise faults.OverLimit(msg)
320

    
321

    
322
def get_network_free_address(network):
323
    """Reserve an IP address from the IP Pool of the network."""
324

    
325
    pool = network.get_pool()
326
    try:
327
        address = pool.get()
328
    except EmptyPool:
329
        raise faults.OverLimit("Network %s is full." % network.backend_id)
330
    pool.save()
331
    return address
332

    
333

    
334
def get_nic(machine, network):
335
    try:
336
        return NetworkInterface.objects.get(machine=machine, network=network)
337
    except NetworkInterface.DoesNotExist:
338
        raise faults.ItemNotFound('Server not connected to this network.')
339

    
340

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

    
354

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

    
366

    
367
def render_meta(request, meta, status=200):
368
    if request.serialization == 'xml':
369
        data = render_to_string('meta.xml', dict(key=key, val=val))
370
    else:
371
        data = json.dumps(dict(meta=meta))
372
    return HttpResponse(data, status=status)
373

    
374

    
375
def construct_nic_id(nic):
376
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
377

    
378

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

    
400

    
401
def values_from_flavor(flavor):
402
    """Get Ganeti connectivity info from flavor type.
403

404
    If link or mac_prefix equals to "pool", then the resources
405
    are allocated from the corresponding Pools.
406

407
    """
408
    try:
409
        flavor = Network.FLAVORS[flavor]
410
    except KeyError:
411
        raise faults.BadRequest("Unknown network flavor")
412

    
413
    mode = flavor.get("mode")
414

    
415
    link = flavor.get("link")
416
    if link == "pool":
417
        link = allocate_resource("bridge")
418

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

    
423
    tags = flavor.get("tags")
424

    
425
    return mode, link, mac_prefix, tags
426

    
427

    
428
def allocate_resource(res_type):
429
    table = get_pool_table(res_type)
430
    pool = table.get_pool()
431
    value = pool.get()
432
    pool.save()
433
    return value
434

    
435

    
436
def release_resource(res_type, value):
437
    table = get_pool_table(res_type)
438
    pool = table.get_pool()
439
    pool.put(value)
440
    pool.save()
441

    
442

    
443
def get_pool_table(res_type):
444
    if res_type == "bridge":
445
        return BridgePoolTable
446
    elif res_type == "mac_prefix":
447
        return MacPrefixPoolTable
448
    else:
449
        raise Exception("Unknown resource type")
450

    
451

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

    
460
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
461
                                                                  flat=True)
462
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
463
                                                                  flat=True)
464
    networkusernames = Network.objects.filter().values_list('userid',
465
                                                            flat=True)
466

    
467
    return set(list(keypairusernames) + list(serverusernames) +
468
               list(networkusernames))
469

    
470

    
471
def vm_to_links(vm_id):
472
    href = join_urls(SERVERS_URL, str(vm_id))
473
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
474

    
475

    
476
def network_to_links(network_id):
477
    href = join_urls(NETWORKS_URL, str(network_id))
478
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
479

    
480

    
481
def flavor_to_links(flavor_id):
482
    href = join_urls(FLAVORS_URL, str(flavor_id))
483
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
484

    
485

    
486
def image_to_links(image_id):
487
    href = join_urls(IMAGES_URL, str(image_id))
488
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
489
    links.append({"rel": "alternate",
490
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
491
    return links
492

    
493
def start_action(vm, action, jobId):
494
    vm.action = action
495
    vm.backendjobid = jobId
496
    vm.backendopcode = None
497
    vm.backendjobstatus = None
498
    vm.backendlogmsg = None
499
    vm.save()