Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 69dadbe4

History | View | Annotate | Download (19.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
from base64 import b64encode, b64decode
35
from hashlib import sha256
36
from logging import getLogger
37
from random import choice
38
from string import digits, lowercase, uppercase
39

    
40
from Crypto.Cipher import AES
41

    
42
from django.conf import settings
43
from django.http import HttpResponse
44
from django.template.loader import render_to_string
45
from django.utils import simplejson as json
46
from django.db.models import Q
47

    
48
from snf_django.lib.api import faults
49
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
50
                               Network, NetworkInterface, SecurityGroup,
51
                               BridgePoolTable, MacPrefixPoolTable, IPAddress,
52
                               IPPoolTable, Subnet)
53
from synnefo.db import pools
54

    
55
from synnefo.plankton.utils import image_backend
56

    
57
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
58
from synnefo.lib.services import get_service_path
59
from synnefo.lib import join_urls
60

    
61
COMPUTE_URL = \
62
    join_urls(BASE_HOST,
63
              get_service_path(cyclades_services, "compute", version="v2.0"))
64
SERVERS_URL = join_urls(COMPUTE_URL, "servers/")
65
NETWORKS_URL = join_urls(COMPUTE_URL, "networks/")
66
FLAVORS_URL = join_urls(COMPUTE_URL, "flavors/")
67
IMAGES_URL = join_urls(COMPUTE_URL, "images/")
68
PLANKTON_URL = \
69
    join_urls(BASE_HOST,
70
              get_service_path(cyclades_services, "image", version="v1.0"))
71
IMAGES_PLANKTON_URL = join_urls(PLANKTON_URL, "images/")
72

    
73
PITHOSMAP_PREFIX = "pithosmap://"
74

    
75
log = getLogger('synnefo.api')
76

    
77

    
78
def random_password():
79
    """Generates a random password
80

81
    We generate a windows compliant password: it must contain at least
82
    one charachter from each of the groups: upper case, lower case, digits.
83
    """
84

    
85
    pool = lowercase + uppercase + digits
86
    lowerset = set(lowercase)
87
    upperset = set(uppercase)
88
    digitset = set(digits)
89
    length = 10
90

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

    
93
    # Make sure the password is compliant
94
    chars = set(password)
95
    if not chars & lowerset:
96
        password += choice(lowercase)
97
    if not chars & upperset:
98
        password += choice(uppercase)
99
    if not chars & digitset:
100
        password += choice(digits)
101

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

    
105
    return password
106

    
107

    
108
def zeropad(s):
109
    """Add zeros at the end of a string in order to make its length
110
       a multiple of 16."""
111

    
112
    npad = 16 - len(s) % 16
113
    return s + '\x00' * npad
114

    
115

    
116
def encrypt(plaintext):
117
    # Make sure key is 32 bytes long
118
    key = sha256(settings.SECRET_KEY).digest()
119

    
120
    aes = AES.new(key)
121
    enc = aes.encrypt(zeropad(plaintext))
122
    return b64encode(enc)
123

    
124

    
125
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
126
           non_suspended=False, prefetch_related=None):
127
    """Find a VirtualMachine instance based on ID and owner."""
128

    
129
    try:
130
        server_id = int(server_id)
131
        servers = VirtualMachine.objects
132
        if for_update:
133
            servers = servers.select_for_update()
134
        if prefetch_related is not None:
135
            servers = servers.prefetch_related(prefetch_related)
136
        vm = servers.get(id=server_id, userid=user_id)
137
        if non_deleted and vm.deleted:
138
            raise faults.BadRequest("Server has been deleted.")
139
        if non_suspended and vm.suspended:
140
            raise faults.Forbidden("Administratively Suspended VM")
141
        return vm
142
    except ValueError:
143
        raise faults.BadRequest('Invalid server ID.')
144
    except VirtualMachine.DoesNotExist:
145
        raise faults.ItemNotFound('Server not found.')
146

    
147

    
148
def get_vm_meta(vm, key):
149
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
150

    
151
    try:
152
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
153
    except VirtualMachineMetadata.DoesNotExist:
154
        raise faults.ItemNotFound('Metadata key not found.')
155

    
156

    
157
def get_image(image_id, user_id):
158
    """Return an Image instance or raise ItemNotFound."""
159

    
160
    with image_backend(user_id) as backend:
161
        return backend.get_image(image_id)
162

    
163

    
164
def get_image_dict(image_id, user_id):
165
    image = {}
166
    img = get_image(image_id, user_id)
167
    image["id"] = img["id"]
168
    image["name"] = img["name"]
169
    image["format"] = img["disk_format"]
170
    image["checksum"] = img["checksum"]
171
    image["location"] = img["location"]
172

    
173
    checksum = image["checksum"] = img["checksum"]
174
    size = image["size"] = img["size"]
175
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([checksum, str(size)])
176

    
177
    properties = img.get("properties", {})
178
    image["metadata"] = dict((key.upper(), val)
179
                             for key, val in properties.items())
180

    
181
    return image
182

    
183

    
184
def get_flavor(flavor_id, include_deleted=False):
185
    """Return a Flavor instance or raise ItemNotFound."""
186

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

    
196

    
197
def get_flavor_provider(flavor):
198
    """Extract provider from disk template.
199

200
    Provider for `ext` disk_template is encoded in the disk template
201
    name, which is formed `ext_<provider_name>`. Provider is None
202
    for all other disk templates.
203

204
    """
205
    disk_template = flavor.disk_template
206
    provider = None
207
    if disk_template.startswith("ext"):
208
        disk_template, provider = disk_template.split("_", 1)
209
    return disk_template, provider
210

    
211

    
212
def get_network(network_id, user_id, for_update=False, non_deleted=False):
213
    """Return a Network instance or raise ItemNotFound."""
214

    
215
    try:
216
        network_id = int(network_id)
217
        objects = Network.objects.prefetch_related("subnets")
218
        if for_update:
219
            objects = objects.select_for_update()
220
        network = objects.get(Q(userid=user_id) | Q(public=True),
221
                              id=network_id)
222
        if non_deleted and network.deleted:
223
            raise faults.BadRequest("Network has been deleted.")
224
        return network
225
    except (ValueError, Network.DoesNotExist):
226
        raise faults.ItemNotFound('Network %s not found.' % network_id)
227

    
228

    
229
def get_subnet(subnet_id, user_id, for_update=False, public=True,
230
               non_deleted=False):
231
    """Return a Subnet instance or raise ItemNotFound."""
232

    
233
    try:
234
        subnet_id = int(subnet_id)
235
        objects = Subnet.objects
236
        if for_update:
237
            objects = objects.select_for_update()
238
        if public:
239
            subnet = objects.get(Q(network__userid=user_id) |
240
                                 Q(network__public=True), id=subnet_id)
241
        else:
242
            subnet = objects.get(network__userid=user_id, id=subnet_id)
243
        if non_deleted and subnet.deleted:
244
            raise faults.BadRequest("Subnet has been deleted.")
245
        return subnet
246
    except (ValueError, Subnet.DoesNotExist):
247
        raise faults.ItemNotFound('Subnet %s not found.' % subnet_id)
248

    
249

    
250
def get_port(port_id, user_id, for_update=False):
251
    """
252
    Return a NetworkInteface instance or raise ItemNotFound.
253
    """
254
    try:
255
        objects = NetworkInterface.objects
256
        if for_update:
257
            objects = objects.select_for_update()
258

    
259
        if not user_id:
260
            port = objects.get(id=port_id)
261
        else:
262
            port = objects.get(network__userid=user_id, id=port_id)
263

    
264
        if (port.device_owner != "vm") and for_update:
265
            raise faults.BadRequest('Can not update non vm port')
266

    
267
        return port
268
    except (ValueError, NetworkInterface.DoesNotExist):
269
        raise faults.ItemNotFound('Port not found.')
270

    
271

    
272
def get_security_group(sg_id):
273
    try:
274
        sg = SecurityGroup.objects.get(id=sg_id)
275
        return sg
276
    except (ValueError, SecurityGroup.DoesNotExist):
277
        raise faults.ItemNotFound("Not valid security group")
278

    
279

    
280
def get_floating_ip_by_address(userid, address, for_update=False):
281
    try:
282
        objects = IPAddress.objects
283
        if for_update:
284
            objects = objects.select_for_update()
285
        return objects.get(userid=userid, floating_ip=True,
286
                           address=address, deleted=False)
287
    except IPAddress.DoesNotExist:
288
        raise faults.ItemNotFound("Floating IP does not exist.")
289

    
290

    
291
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
292
    try:
293
        objects = IPAddress.objects
294
        if for_update:
295
            objects = objects.select_for_update()
296
        return objects.get(id=floating_ip_id, floating_ip=True,
297
                               userid=userid, deleted=False)
298
    except IPAddress.DoesNotExist:
299
        raise faults.ItemNotFound("Floating IP %s does not exist." %
300
                                  floating_ip_id)
301

    
302

    
303
def allocate_ip_from_pools(pool_rows, userid, address=None, floating_ip=False):
304
    """Try to allocate a value from a number of pools.
305

306
    This function takes as argument a number of PoolTable objects and tries to
307
    allocate a value from them. If all pools are empty EmptyPool is raised.
308

309
    """
310
    for pool_row in pool_rows:
311
        pool = pool_row.pool
312
        try:
313
            value = pool.get(value=address)
314
            pool.save()
315
            subnet = pool_row.subnet
316
            ipaddress = IPAddress.objects.create(subnet=subnet,
317
                                                 network=subnet.network,
318
                                                 userid=userid,
319
                                                 address=value,
320
                                                 floating_ip=floating_ip)
321
            return ipaddress
322
        except pools.EmptyPool:
323
            pass
324
    raise pools.EmptyPool("No more IP addresses available on pools %s" %
325
                          pool_rows)
326

    
327

    
328
def allocate_ip(network, userid, address=None, floating_ip=False):
329
    """Try to allocate an IP from networks IP pools."""
330
    ip_pools = IPPoolTable.objects.select_for_update()\
331
        .filter(subnet__network=network)
332
    try:
333
        return allocate_ip_from_pools(ip_pools, userid, address=address,
334
                                      floating_ip=floating_ip)
335
    except pools.EmptyPool:
336
        raise faults.Conflict("No more IP addresses available on network %s"
337
                              % network.id)
338
    except pools.ValueNotAvailable:
339
        raise faults.Conflict("IP address %s is already used." % address)
340
    except pools.InvalidValue:
341
        raise faults.BadRequest("Address %s does not belong to network %s" %
342
                                (address, network.id))
343

    
344

    
345
def allocate_public_ip(userid, floating_ip=False, backend=None):
346
    """Try to allocate a public or floating IP address.
347

348
    Try to allocate a a public IPv4 address from one of the available networks.
349
    If 'floating_ip' is set, only networks which are floating IP pools will be
350
    used and the IPAddress that will be created will be marked as a floating
351
    IP. If 'backend' is set, only the networks that exist in this backend will
352
    be used.
353

354
    """
355

    
356
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
357
        .prefetch_related("subnet__network")\
358
        .filter(subnet__deleted=False)\
359
        .filter(subnet__network__public=True)\
360
        .filter(subnet__network__drained=False)
361
    if floating_ip:
362
        ip_pool_rows = ip_pool_rows\
363
            .filter(subnet__network__floating_ip_pool=True)
364
    if backend is not None:
365
        ip_pool_rows = ip_pool_rows\
366
            .filter(subnet__network__backend_networks__backend=backend)
367

    
368
    try:
369
        return allocate_ip_from_pools(ip_pool_rows, userid,
370
                                      floating_ip=floating_ip)
371
    except pools.EmptyPool:
372
        ip_type = "floating" if floating_ip else "public"
373
        log_msg = "Failed to allocate a %s IP. Reason:" % ip_type
374
        if ip_pool_rows:
375
            log_msg += " No network exists."
376
        else:
377
            log_msg += " All network are full."
378
        if backend is not None:
379
            log_msg += " Backend: %s" % backend
380
        log.error(log_msg)
381
        exception_msg = "Can not allocate a %s IP address." % ip_type
382
        raise faults.ServiceUnavailable(exception_msg)
383

    
384

    
385
def backend_has_free_public_ip(backend):
386
    """Check if a backend has a free public IPv4 address."""
387
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
388
        .filter(subnet__network__public=True)\
389
        .filter(subnet__network__drained=False)\
390
        .filter(subnet__deleted=False)\
391
        .filter(subnet__network__backend_networks__backend=backend)
392
    for pool_row in ip_pool_rows:
393
        pool = pool_row.pool
394
        if pool.empty():
395
            continue
396
        else:
397
            return True
398

    
399

    
400
def backend_public_networks(backend):
401
    return Network.objects.filter(deleted=False, public=True,
402
                                  backend_networks__backend=backend)
403

    
404

    
405
def get_vm_nic(vm, nic_id):
406
    """Get a VMs NIC by its ID."""
407
    try:
408
        return vm.nics.get(id=nic_id)
409
    except NetworkInterface.DoesNotExist:
410
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
411

    
412

    
413
def get_nic(nic_id):
414
    try:
415
        return NetworkInterface.objects.get(id=nic_id)
416
    except NetworkInterface.DoesNotExist:
417
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
418

    
419

    
420
def render_metadata(request, metadata, use_values=False, status=200):
421
    if request.serialization == 'xml':
422
        data = render_to_string('metadata.xml', {'metadata': metadata})
423
    else:
424
        if use_values:
425
            d = {'metadata': {'values': metadata}}
426
        else:
427
            d = {'metadata': metadata}
428
        data = json.dumps(d)
429
    return HttpResponse(data, status=status)
430

    
431

    
432
def render_meta(request, meta, status=200):
433
    if request.serialization == 'xml':
434
        key, val = meta.items()[0]
435
        data = render_to_string('meta.xml', dict(key=key, val=val))
436
    else:
437
        data = json.dumps(dict(meta=meta))
438
    return HttpResponse(data, status=status)
439

    
440

    
441
def verify_personality(personality):
442
    """Verify that a a list of personalities is well formed"""
443
    if len(personality) > settings.MAX_PERSONALITY:
444
        raise faults.OverLimit("Maximum number of personalities"
445
                               " exceeded")
446
    for p in personality:
447
        # Verify that personalities are well-formed
448
        try:
449
            assert isinstance(p, dict)
450
            keys = set(p.keys())
451
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
452
            assert keys.issubset(allowed)
453
            contents = p['contents']
454
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
455
                # No need to decode if contents already exceed limit
456
                raise faults.OverLimit("Maximum size of personality exceeded")
457
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
458
                raise faults.OverLimit("Maximum size of personality exceeded")
459
        except AssertionError:
460
            raise faults.BadRequest("Malformed personality in request")
461

    
462

    
463
def values_from_flavor(flavor):
464
    """Get Ganeti connectivity info from flavor type.
465

466
    If link or mac_prefix equals to "pool", then the resources
467
    are allocated from the corresponding Pools.
468

469
    """
470
    try:
471
        flavor = Network.FLAVORS[flavor]
472
    except KeyError:
473
        raise faults.BadRequest("Unknown network flavor")
474

    
475
    mode = flavor.get("mode")
476

    
477
    link = flavor.get("link")
478
    if link == "pool":
479
        link = allocate_resource("bridge")
480

    
481
    mac_prefix = flavor.get("mac_prefix")
482
    if mac_prefix == "pool":
483
        mac_prefix = allocate_resource("mac_prefix")
484

    
485
    tags = flavor.get("tags")
486

    
487
    return mode, link, mac_prefix, tags
488

    
489

    
490
def allocate_resource(res_type):
491
    table = get_pool_table(res_type)
492
    pool = table.get_pool()
493
    value = pool.get()
494
    pool.save()
495
    return value
496

    
497

    
498
def release_resource(res_type, value):
499
    table = get_pool_table(res_type)
500
    pool = table.get_pool()
501
    pool.put(value)
502
    pool.save()
503

    
504

    
505
def get_pool_table(res_type):
506
    if res_type == "bridge":
507
        return BridgePoolTable
508
    elif res_type == "mac_prefix":
509
        return MacPrefixPoolTable
510
    else:
511
        raise Exception("Unknown resource type")
512

    
513

    
514
def get_existing_users():
515
    """
516
    Retrieve user ids stored in cyclades user agnostic models.
517
    """
518
    # also check PublicKeys a user with no servers/networks exist
519
    from synnefo.userdata.models import PublicKeyPair
520
    from synnefo.db.models import VirtualMachine, Network
521

    
522
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
523
                                                                  flat=True)
524
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
525
                                                                  flat=True)
526
    networkusernames = Network.objects.filter().values_list('userid',
527
                                                            flat=True)
528

    
529
    return set(list(keypairusernames) + list(serverusernames) +
530
               list(networkusernames))
531

    
532

    
533
def vm_to_links(vm_id):
534
    href = join_urls(SERVERS_URL, str(vm_id))
535
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
536

    
537

    
538
def network_to_links(network_id):
539
    href = join_urls(NETWORKS_URL, str(network_id))
540
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
541

    
542

    
543
def flavor_to_links(flavor_id):
544
    href = join_urls(FLAVORS_URL, str(flavor_id))
545
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
546

    
547

    
548
def image_to_links(image_id):
549
    href = join_urls(IMAGES_URL, str(image_id))
550
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
551
    links.append({"rel": "alternate",
552
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
553
    return links
554

    
555

    
556
def start_action(vm, action, jobId):
557
    vm.action = action
558
    vm.backendjobid = jobId
559
    vm.backendopcode = None
560
    vm.backendjobstatus = None
561
    vm.backendlogmsg = None
562
    vm.save()