Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (17.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
    """Allocate a public IP for a vm."""
288
    for network in backend_public_networks(backend):
289
        try:
290
            address = get_network_free_address(network)
291
        except faults.OverLimit:
292
            pass
293
        else:
294
            return (network, address)
295
    return (None, None)
296

    
297

    
298
def get_public_ip(backend):
299
    """Reserve an IP from a public network.
300

301
    This method should run inside a transaction.
302

303
    """
304

    
305
    # Guarantee exclusive access to backend, because accessing the IP pools of
306
    # the backend networks may result in a deadlock with backend allocator
307
    # which also checks that backend networks have a free IP.
308
    backend = Backend.objects.select_for_update().get(id=backend.id)
309

    
310
    address = None
311
    if settings.PUBLIC_USE_POOL:
312
        (network, address) = allocate_public_address(backend)
313
    else:
314
        for net in list(backend_public_networks(backend)):
315
            pool = net.get_pool()
316
            if not pool.empty():
317
                address = 'pool'
318
                network = net
319
                break
320
    if address is None:
321
        log.error("Public networks of backend %s are full", backend)
322
        raise faults.OverLimit("Can not allocate IP for new machine."
323
                        " Public networks are full.")
324
    return (network, address)
325

    
326

    
327
def backend_public_networks(backend):
328
    """Return available public networks of the backend.
329

330
    Iterator for non-deleted public networks that are available
331
    to the specified backend.
332

333
    """
334
    for network in Network.objects.filter(public=True, deleted=False,
335
                                          drained=False):
336
        if BackendNetwork.objects.filter(network=network,
337
                                         backend=backend).exists():
338
            yield network
339

    
340

    
341
def get_network_free_address(network):
342
    """Reserve an IP address from the IP Pool of the network."""
343

    
344
    pool = network.get_pool()
345
    try:
346
        address = pool.get()
347
    except EmptyPool:
348
        raise faults.OverLimit("Network %s is full." % network.backend_id)
349
        address = None
350
    pool.save()
351
    return address
352

    
353

    
354
def allocate_public_ip(networks=None):
355
    """Allocate an IP address from public networks."""
356
    if networks is None:
357
        networks = Network.objects.select_for_update().filter(public=True,
358
                                                              deleted=False)
359
    for network in networks:
360
        try:
361
            address = get_network_free_address(network)
362
        except:
363
            pass
364
        else:
365
            return network, address
366
    msg = "Can not allocate public IP. Public networks are full."
367
    log.error(msg)
368
    raise faults.OverLimit(msg)
369

    
370

    
371
def get_nic(machine, network):
372
    try:
373
        return NetworkInterface.objects.get(machine=machine, network=network)
374
    except NetworkInterface.DoesNotExist:
375
        raise faults.ItemNotFound('Server not connected to this network.')
376

    
377

    
378
def get_nic_from_index(vm, nic_index):
379
    """Returns the nic_index-th nic of a vm
380
       Error Response Codes: itemNotFound (404), badMediaType (415)
381
    """
382
    matching_nics = vm.nics.filter(index=nic_index)
383
    matching_nics_len = len(matching_nics)
384
    if matching_nics_len < 1:
385
        raise faults.ItemNotFound('NIC not found on VM')
386
    elif matching_nics_len > 1:
387
        raise faults.BadMediaType('NIC index conflict on VM')
388
    nic = matching_nics[0]
389
    return nic
390

    
391

    
392
def render_metadata(request, metadata, use_values=False, status=200):
393
    if request.serialization == 'xml':
394
        data = render_to_string('metadata.xml', {'metadata': metadata})
395
    else:
396
        if use_values:
397
            d = {'metadata': {'values': metadata}}
398
        else:
399
            d = {'metadata': metadata}
400
        data = json.dumps(d)
401
    return HttpResponse(data, status=status)
402

    
403

    
404
def render_meta(request, meta, status=200):
405
    if request.serialization == 'xml':
406
        data = render_to_string('meta.xml', dict(key=key, val=val))
407
    else:
408
        data = json.dumps(dict(meta=meta))
409
    return HttpResponse(data, status=status)
410

    
411

    
412
def construct_nic_id(nic):
413
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
414

    
415

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

    
437

    
438
def values_from_flavor(flavor):
439
    """Get Ganeti connectivity info from flavor type.
440

441
    If link or mac_prefix equals to "pool", then the resources
442
    are allocated from the corresponding Pools.
443

444
    """
445
    try:
446
        flavor = Network.FLAVORS[flavor]
447
    except KeyError:
448
        raise faults.BadRequest("Unknown network flavor")
449

    
450
    mode = flavor.get("mode")
451

    
452
    link = flavor.get("link")
453
    if link == "pool":
454
        link = allocate_resource("bridge")
455

    
456
    mac_prefix = flavor.get("mac_prefix")
457
    if mac_prefix == "pool":
458
        mac_prefix = allocate_resource("mac_prefix")
459

    
460
    tags = flavor.get("tags")
461

    
462
    return mode, link, mac_prefix, tags
463

    
464

    
465
def allocate_resource(res_type):
466
    table = get_pool_table(res_type)
467
    pool = table.get_pool()
468
    value = pool.get()
469
    pool.save()
470
    return value
471

    
472

    
473
def release_resource(res_type, value):
474
    table = get_pool_table(res_type)
475
    pool = table.get_pool()
476
    pool.put(value)
477
    pool.save()
478

    
479

    
480
def get_pool_table(res_type):
481
    if res_type == "bridge":
482
        return BridgePoolTable
483
    elif res_type == "mac_prefix":
484
        return MacPrefixPoolTable
485
    else:
486
        raise Exception("Unknown resource type")
487

    
488

    
489
def get_existing_users():
490
    """
491
    Retrieve user ids stored in cyclades user agnostic models.
492
    """
493
    # also check PublicKeys a user with no servers/networks exist
494
    from synnefo.userdata.models import PublicKeyPair
495
    from synnefo.db.models import VirtualMachine, Network
496

    
497
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
498
                                                                  flat=True)
499
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
500
                                                                  flat=True)
501
    networkusernames = Network.objects.filter().values_list('userid',
502
                                                            flat=True)
503

    
504
    return set(list(keypairusernames) + list(serverusernames) +
505
               list(networkusernames))
506

    
507

    
508
def vm_to_links(vm_id):
509
    href = join_urls(SERVERS_URL, str(vm_id))
510
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
511

    
512

    
513
def network_to_links(network_id):
514
    href = join_urls(NETWORKS_URL, str(network_id))
515
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
516

    
517

    
518
def flavor_to_links(flavor_id):
519
    href = join_urls(FLAVORS_URL, str(flavor_id))
520
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
521

    
522

    
523
def image_to_links(image_id):
524
    href = join_urls(IMAGES_URL, str(image_id))
525
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
526
    links.append({"rel": "alternate",
527
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
528
    return links
529

    
530
def start_action(vm, action, jobId):
531
    vm.action = action
532
    vm.backendjobid = jobId
533
    vm.backendopcode = None
534
    vm.backendjobstatus = None
535
    vm.backendlogmsg = None
536
    vm.save()