Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (18.9 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)
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_port(port_id, user_id, for_update=False):
230
    """
231
    Return a NetworkInteface instance or raise ItemNotFound.
232
    """
233
    try:
234
        objects = NetworkInterface.objects.filter(userid=user_id)
235
        if for_update:
236
            objects = objects.select_for_update()
237
        # if (port.device_owner != "vm") and for_update:
238
        #     raise faults.BadRequest('Can not update non vm port')
239
        return objects.get(id=port_id)
240
    except (ValueError, NetworkInterface.DoesNotExist):
241
        raise faults.ItemNotFound("Port '%s' not found." % port_id)
242

    
243

    
244
def get_security_group(sg_id):
245
    try:
246
        sg = SecurityGroup.objects.get(id=sg_id)
247
        return sg
248
    except (ValueError, SecurityGroup.DoesNotExist):
249
        raise faults.ItemNotFound("Not valid security group")
250

    
251

    
252
def get_floating_ip_by_address(userid, address, for_update=False):
253
    try:
254
        objects = IPAddress.objects
255
        if for_update:
256
            objects = objects.select_for_update()
257
        return objects.get(userid=userid, floating_ip=True,
258
                           address=address, deleted=False)
259
    except IPAddress.DoesNotExist:
260
        raise faults.ItemNotFound("Floating IP does not exist.")
261

    
262

    
263
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
264
    try:
265
        objects = IPAddress.objects
266
        if for_update:
267
            objects = objects.select_for_update()
268
        return objects.get(id=floating_ip_id, floating_ip=True,
269
                           userid=userid, deleted=False)
270
    except IPAddress.DoesNotExist:
271
        raise faults.ItemNotFound("Floating IP with ID %s does not exist." %
272
                                  floating_ip_id)
273

    
274

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

278
    This function takes as argument a number of PoolTable objects and tries to
279
    allocate a value from them. If all pools are empty EmptyPool is raised.
280
    If an address is specified and does not belong to any of the pools,
281
    InvalidValue is raised.
282

283
    """
284
    for pool_row in pool_rows:
285
        pool = pool_row.pool
286
        try:
287
            value = pool.get(value=address)
288
            pool.save()
289
            subnet = pool_row.subnet
290
            ipaddress = IPAddress.objects.create(subnet=subnet,
291
                                                 network=subnet.network,
292
                                                 userid=userid,
293
                                                 address=value,
294
                                                 floating_ip=floating_ip)
295
            return ipaddress
296
        except pools.EmptyPool:
297
            pass
298
        except pools.InvalidValue:
299
            pass
300
    if address is None:
301
        raise pools.EmptyPool("No more IP addresses available on pools %s" %
302
                              pool_rows)
303
    else:
304
        raise pools.InvalidValue("Address %s does not belong to pools %s" %
305
                                 (address, pool_rows))
306

    
307

    
308
def allocate_ip(network, userid, address=None, floating_ip=False):
309
    """Try to allocate an IP from networks IP pools."""
310
    if network.action == "DESTROY":
311
        raise faults.Conflict("Can not allocate IP. Network %s is being"
312
                              " deleted" % network.id)
313
    ip_pools = IPPoolTable.objects.select_for_update()\
314
        .filter(subnet__network=network)
315
    try:
316
        return allocate_ip_from_pools(ip_pools, userid, address=address,
317
                                      floating_ip=floating_ip)
318
    except pools.EmptyPool:
319
        raise faults.Conflict("No more IP addresses available on network %s"
320
                              % network.id)
321
    except pools.ValueNotAvailable:
322
        raise faults.Conflict("IP address %s is already used." % address)
323
    except pools.InvalidValue:
324
        raise faults.BadRequest("Address %s does not belong to network %s" %
325
                                (address, network.id))
326

    
327

    
328
def allocate_public_ip(userid, floating_ip=False, backend=None):
329
    """Try to allocate a public or floating IP address.
330

331
    Try to allocate a a public IPv4 address from one of the available networks.
332
    If 'floating_ip' is set, only networks which are floating IP pools will be
333
    used and the IPAddress that will be created will be marked as a floating
334
    IP. If 'backend' is set, only the networks that exist in this backend will
335
    be used.
336

337
    """
338

    
339
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
340
        .prefetch_related("subnet__network")\
341
        .filter(subnet__deleted=False)\
342
        .filter(subnet__network__deleted=False)\
343
        .filter(subnet__network__public=True)\
344
        .filter(subnet__network__drained=False)
345
    if floating_ip:
346
        ip_pool_rows = ip_pool_rows\
347
            .filter(subnet__network__floating_ip_pool=True)
348
    if backend is not None:
349
        ip_pool_rows = ip_pool_rows\
350
            .filter(subnet__network__backend_networks__backend=backend)
351

    
352
    try:
353
        return allocate_ip_from_pools(ip_pool_rows, userid,
354
                                      floating_ip=floating_ip)
355
    except pools.EmptyPool:
356
        ip_type = "floating" if floating_ip else "public"
357
        log_msg = "Failed to allocate a %s IP. Reason:" % ip_type
358
        if ip_pool_rows:
359
            log_msg += " No network exists."
360
        else:
361
            log_msg += " All network are full."
362
        if backend is not None:
363
            log_msg += " Backend: %s" % backend
364
        log.error(log_msg)
365
        exception_msg = "Can not allocate a %s IP address." % ip_type
366
        if floating_ip:
367
            raise faults.Conflict(exception_msg)
368
        else:
369
            raise faults.ServiceUnavailable(exception_msg)
370

    
371

    
372
def backend_has_free_public_ip(backend):
373
    """Check if a backend has a free public IPv4 address."""
374
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
375
        .filter(subnet__network__public=True)\
376
        .filter(subnet__network__drained=False)\
377
        .filter(subnet__deleted=False)\
378
        .filter(subnet__network__backend_networks__backend=backend)
379
    for pool_row in ip_pool_rows:
380
        pool = pool_row.pool
381
        if pool.empty():
382
            continue
383
        else:
384
            return True
385

    
386

    
387
def backend_public_networks(backend):
388
    return Network.objects.filter(deleted=False, public=True,
389
                                  backend_networks__backend=backend)
390

    
391

    
392
def get_vm_nic(vm, nic_id):
393
    """Get a VMs NIC by its ID."""
394
    try:
395
        return vm.nics.get(id=nic_id)
396
    except NetworkInterface.DoesNotExist:
397
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
398

    
399

    
400
def get_nic(nic_id):
401
    try:
402
        return NetworkInterface.objects.get(id=nic_id)
403
    except NetworkInterface.DoesNotExist:
404
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
405

    
406

    
407
def render_metadata(request, metadata, use_values=False, status=200):
408
    if request.serialization == 'xml':
409
        data = render_to_string('metadata.xml', {'metadata': metadata})
410
    else:
411
        if use_values:
412
            d = {'metadata': {'values': metadata}}
413
        else:
414
            d = {'metadata': metadata}
415
        data = json.dumps(d)
416
    return HttpResponse(data, status=status)
417

    
418

    
419
def render_meta(request, meta, status=200):
420
    if request.serialization == 'xml':
421
        key, val = meta.items()[0]
422
        data = render_to_string('meta.xml', dict(key=key, val=val))
423
    else:
424
        data = json.dumps(dict(meta=meta))
425
    return HttpResponse(data, status=status)
426

    
427

    
428
def verify_personality(personality):
429
    """Verify that a a list of personalities is well formed"""
430
    if len(personality) > settings.MAX_PERSONALITY:
431
        raise faults.OverLimit("Maximum number of personalities"
432
                               " exceeded")
433
    for p in personality:
434
        # Verify that personalities are well-formed
435
        try:
436
            assert isinstance(p, dict)
437
            keys = set(p.keys())
438
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
439
            assert keys.issubset(allowed)
440
            contents = p['contents']
441
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
442
                # No need to decode if contents already exceed limit
443
                raise faults.OverLimit("Maximum size of personality exceeded")
444
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
445
                raise faults.OverLimit("Maximum size of personality exceeded")
446
        except AssertionError:
447
            raise faults.BadRequest("Malformed personality in request")
448

    
449

    
450
def values_from_flavor(flavor):
451
    """Get Ganeti connectivity info from flavor type.
452

453
    If link or mac_prefix equals to "pool", then the resources
454
    are allocated from the corresponding Pools.
455

456
    """
457
    try:
458
        flavor = Network.FLAVORS[flavor]
459
    except KeyError:
460
        raise faults.BadRequest("Unknown network flavor")
461

    
462
    mode = flavor.get("mode")
463

    
464
    link = flavor.get("link")
465
    if link == "pool":
466
        link = allocate_resource("bridge")
467

    
468
    mac_prefix = flavor.get("mac_prefix")
469
    if mac_prefix == "pool":
470
        mac_prefix = allocate_resource("mac_prefix")
471

    
472
    tags = flavor.get("tags")
473

    
474
    return mode, link, mac_prefix, tags
475

    
476

    
477
def allocate_resource(res_type):
478
    table = get_pool_table(res_type)
479
    pool = table.get_pool()
480
    value = pool.get()
481
    pool.save()
482
    return value
483

    
484

    
485
def release_resource(res_type, value):
486
    table = get_pool_table(res_type)
487
    pool = table.get_pool()
488
    pool.put(value)
489
    pool.save()
490

    
491

    
492
def get_pool_table(res_type):
493
    if res_type == "bridge":
494
        return BridgePoolTable
495
    elif res_type == "mac_prefix":
496
        return MacPrefixPoolTable
497
    else:
498
        raise Exception("Unknown resource type")
499

    
500

    
501
def get_existing_users():
502
    """
503
    Retrieve user ids stored in cyclades user agnostic models.
504
    """
505
    # also check PublicKeys a user with no servers/networks exist
506
    from synnefo.userdata.models import PublicKeyPair
507
    from synnefo.db.models import VirtualMachine, Network
508

    
509
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
510
                                                                  flat=True)
511
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
512
                                                                  flat=True)
513
    networkusernames = Network.objects.filter().values_list('userid',
514
                                                            flat=True)
515

    
516
    return set(list(keypairusernames) + list(serverusernames) +
517
               list(networkusernames))
518

    
519

    
520
def vm_to_links(vm_id):
521
    href = join_urls(SERVERS_URL, str(vm_id))
522
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
523

    
524

    
525
def network_to_links(network_id):
526
    href = join_urls(NETWORKS_URL, str(network_id))
527
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
528

    
529

    
530
def flavor_to_links(flavor_id):
531
    href = join_urls(FLAVORS_URL, str(flavor_id))
532
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
533

    
534

    
535
def image_to_links(image_id):
536
    href = join_urls(IMAGES_URL, str(image_id))
537
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
538
    links.append({"rel": "alternate",
539
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
540
    return links
541

    
542

    
543
def start_action(vm, action, jobId):
544
    vm.action = action
545
    vm.backendjobid = jobId
546
    vm.backendopcode = None
547
    vm.backendjobstatus = None
548
    vm.backendlogmsg = None
549
    vm.save()