Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (18 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, BridgePoolTable,
51
                               MacPrefixPoolTable, IPAddress, IPPoolTable)
52
from synnefo.db import pools
53

    
54
from synnefo.plankton.utils import image_backend
55

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

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

    
72
PITHOSMAP_PREFIX = "pithosmap://"
73

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

    
76

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

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

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

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

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

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

    
104
    return password
105

    
106

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

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

    
114

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

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

    
123

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

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

    
146

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

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

    
155

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

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

    
162

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

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

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

    
180
    return image
181

    
182

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

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

    
195

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

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

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

    
210

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

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

    
227

    
228
def get_port(port_id, user_id, for_update=False):
229
    """
230
    Return a NetworkInteface instance or raise ItemNotFound.
231
    """
232
    try:
233
        objects = NetworkInterface.objects
234
        if for_update:
235
            objects = objects.select_for_update()
236

    
237
        port = objects.get(network__userid=user_id, id=port_id)
238

    
239
        if (port.device_owner != "vm") and for_update:
240
            raise faults.BadRequest('Can not update non vm port')
241

    
242
        return port
243
    except (ValueError, NetworkInterface.DoesNotExist):
244
        raise faults.ItemNotFound('Port not found.')
245

    
246

    
247
def get_floating_ip_by_address(userid, address, for_update=False):
248
    try:
249
        objects = IPAddress.objects
250
        if for_update:
251
            objects = objects.select_for_update()
252
        return objects.get(userid=userid, floating_ip=True,
253
                           address=address, deleted=False)
254
    except IPAddress.DoesNotExist:
255
        raise faults.ItemNotFound("Floating IP does not exist.")
256

    
257

    
258
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
259
    try:
260
        objects = IPAddress.objects
261
        if for_update:
262
            objects = objects.select_for_update()
263
        return objects.get(id=floating_ip_id, floating_ip=True, userid=userid,
264
                           deleted=False)
265
    except IPAddress.DoesNotExist:
266
        raise faults.ItemNotFound("Floating IP %s does not exist." %
267
                                  floating_ip_id)
268

    
269

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

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

276
    """
277
    for pool_row in pool_rows:
278
        pool = pool_row.pool
279
        try:
280
            value = pool.get(value=address)
281
            pool.save()
282
            subnet = pool_row.subnet
283
            ipaddress = IPAddress.objects.create(subnet=subnet,
284
                                                 network=subnet.network,
285
                                                 userid=userid,
286
                                                 address=value,
287
                                                 floating_ip=floating_ip)
288
            return ipaddress
289
        except pools.EmptyPool:
290
            pass
291
    raise pools.EmptyPool("No more IP addresses available on pools %s" %
292
                          pool_rows)
293

    
294

    
295
def allocate_ip(network, userid, address=None, floating_ip=False):
296
    """Try to allocate an IP from networks IP pools."""
297
    ip_pools = IPPoolTable.objects.select_for_update()\
298
        .filter(subnet__network=network)
299
    try:
300
        return allocate_ip_from_pools(ip_pools, userid, address=address,
301
                                      floating_ip=floating_ip)
302
    except pools.EmptyPool:
303
        raise faults.Conflict("No more IP addresses available on network %s"
304
                              % network.id)
305
    except pools.ValueNotAvailable:
306
        raise faults.Conflict("IP address %s is already used." % address)
307
    except pools.InvalidValue:
308
        raise faults.BadRequest("Address %s does not belong to network %s" %
309
                                (address, network.id))
310

    
311

    
312
def allocate_public_ip(userid, floating_ip=False, backend=None):
313
    """Try to allocate a public or floating IP address.
314

315
    Try to allocate a a public IPv4 address from one of the available networks.
316
    If 'floating_ip' is set, only networks which are floating IP pools will be
317
    used and the IPAddress that will be created will be marked as a floating
318
    IP. If 'backend' is set, only the networks that exist in this backend will
319
    be used.
320

321
    """
322

    
323
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
324
        .prefetch_related("subnet__network")\
325
        .filter(subnet__deleted=False)\
326
        .filter(subnet__network__public=True)\
327
        .filter(subnet__network__drained=False)
328
    if floating_ip:
329
        ip_pool_rows = ip_pool_rows\
330
            .filter(subnet__network__floating_ip_pool=True)
331
    if backend is not None:
332
        ip_pool_rows = ip_pool_rows\
333
            .filter(subnet__network__backend_networks__backend=backend)
334

    
335
    try:
336
        return allocate_ip_from_pools(ip_pool_rows, userid,
337
                                      floating_ip=floating_ip)
338
    except pools.EmptyPool:
339
        ip_type = "floating" if floating_ip else "public"
340
        log_msg = "Failed to allocate a %s IP. Reason:" % ip_type
341
        if ip_pool_rows:
342
            log_msg += " No network exists."
343
        else:
344
            log_msg += " All network are full."
345
        if backend is not None:
346
            log_msg += " Backend: %s" % backend
347
        log.error(log_msg)
348
        exception_msg = "Can not allocate a %s IP address." % ip_type
349
        raise faults.ServiceUnavailable(exception_msg)
350

    
351

    
352
def backend_has_free_public_ip(backend):
353
    """Check if a backend has a free public IPv4 address."""
354
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
355
        .filter(subnet__network__public=True)\
356
        .filter(subnet__network__drained=False)\
357
        .filter(subnet__deleted=False)\
358
        .filter(subnet__network__backend_networks__backend=backend)
359
    for pool_row in ip_pool_rows:
360
        pool = pool_row.pool
361
        if pool.empty():
362
            continue
363
        else:
364
            return True
365

    
366

    
367
def backend_public_networks(backend):
368
    return Network.objects.filter(deleted=False, public=True,
369
                                  backend_networks__backend=backend)
370

    
371

    
372
def get_vm_nic(vm, nic_id):
373
    """Get a VMs NIC by its ID."""
374
    try:
375
        return vm.nics.get(id=nic_id)
376
    except NetworkInterface.DoesNotExist:
377
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
378

    
379

    
380
def get_nic(nic_id):
381
    try:
382
        return NetworkInterface.objects.get(id=nic_id)
383
    except NetworkInterface.DoesNotExist:
384
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
385

    
386

    
387
def render_metadata(request, metadata, use_values=False, status=200):
388
    if request.serialization == 'xml':
389
        data = render_to_string('metadata.xml', {'metadata': metadata})
390
    else:
391
        if use_values:
392
            d = {'metadata': {'values': metadata}}
393
        else:
394
            d = {'metadata': metadata}
395
        data = json.dumps(d)
396
    return HttpResponse(data, status=status)
397

    
398

    
399
def render_meta(request, meta, status=200):
400
    if request.serialization == 'xml':
401
        key, val = meta.items()[0]
402
        data = render_to_string('meta.xml', dict(key=key, val=val))
403
    else:
404
        data = json.dumps(dict(meta=meta))
405
    return HttpResponse(data, status=status)
406

    
407

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

    
429

    
430
def values_from_flavor(flavor):
431
    """Get Ganeti connectivity info from flavor type.
432

433
    If link or mac_prefix equals to "pool", then the resources
434
    are allocated from the corresponding Pools.
435

436
    """
437
    try:
438
        flavor = Network.FLAVORS[flavor]
439
    except KeyError:
440
        raise faults.BadRequest("Unknown network flavor")
441

    
442
    mode = flavor.get("mode")
443

    
444
    link = flavor.get("link")
445
    if link == "pool":
446
        link = allocate_resource("bridge")
447

    
448
    mac_prefix = flavor.get("mac_prefix")
449
    if mac_prefix == "pool":
450
        mac_prefix = allocate_resource("mac_prefix")
451

    
452
    tags = flavor.get("tags")
453

    
454
    return mode, link, mac_prefix, tags
455

    
456

    
457
def allocate_resource(res_type):
458
    table = get_pool_table(res_type)
459
    pool = table.get_pool()
460
    value = pool.get()
461
    pool.save()
462
    return value
463

    
464

    
465
def release_resource(res_type, value):
466
    table = get_pool_table(res_type)
467
    pool = table.get_pool()
468
    pool.put(value)
469
    pool.save()
470

    
471

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

    
480

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

    
489
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
490
                                                                  flat=True)
491
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
492
                                                                  flat=True)
493
    networkusernames = Network.objects.filter().values_list('userid',
494
                                                            flat=True)
495

    
496
    return set(list(keypairusernames) + list(serverusernames) +
497
               list(networkusernames))
498

    
499

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

    
504

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

    
509

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

    
514

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

    
522

    
523
def start_action(vm, action, jobId):
524
    vm.action = action
525
    vm.backendjobid = jobId
526
    vm.backendopcode = None
527
    vm.backendjobstatus = None
528
    vm.backendlogmsg = None
529
    vm.save()