Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (16.6 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 ipaddr
35

    
36
from base64 import b64encode, b64decode
37
from hashlib import sha256
38
from logging import getLogger
39
from random import choice
40
from string import digits, lowercase, uppercase
41

    
42
from Crypto.Cipher import AES
43

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

    
50
from snf_django.lib.api import faults
51
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
52
                               Network, BackendNetwork, NetworkInterface,
53
                               BridgePoolTable, MacPrefixPoolTable, Backend,
54
                               FloatingIP)
55
from synnefo.db.pools import EmptyPool
56

    
57
from synnefo.plankton.utils import image_backend
58
from synnefo.settings import MAX_CIDR_BLOCK
59

    
60
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
61
from synnefo.lib.services import get_service_path
62
from synnefo.lib import join_urls
63

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

    
76

    
77
log = getLogger('synnefo.api')
78

    
79

    
80
def random_password():
81
    """Generates a random password
82

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

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

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

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

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

    
107
    return password
108

    
109

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

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

    
117

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

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

    
126

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

    
131
    try:
132
        server_id = int(server_id)
133
        servers = VirtualMachine.objects
134
        if for_update:
135
            servers = servers.select_for_update()
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
    properties = img.get('properties', {})
168
    image["id"] = img["id"]
169
    image["name"] = img["name"]
170
    image['backend_id'] = img['location']
171
    image['format'] = img['disk_format']
172
    image['metadata'] = dict((key.upper(), val)
173
                             for key, val in properties.items())
174
    image['checksum'] = img['checksum']
175

    
176
    return image
177

    
178

    
179
def get_flavor(flavor_id, include_deleted=False):
180
    """Return a Flavor instance or raise ItemNotFound."""
181

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

    
191

    
192
def get_flavor_provider(flavor):
193
    """Extract provider from disk template.
194

195
    Provider for `ext` disk_template is encoded in the disk template
196
    name, which is formed `ext_<provider_name>`. Provider is None
197
    for all other disk templates.
198

199
    """
200
    disk_template = flavor.disk_template
201
    provider = None
202
    if disk_template.startswith("ext"):
203
        disk_template, provider = disk_template.split("_", 1)
204
    return disk_template, provider
205

    
206

    
207
def get_network(network_id, user_id, for_update=False, non_deleted=False):
208
    """Return a Network instance or raise ItemNotFound."""
209

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

    
223

    
224
def get_floating_ip(user_id, ipv4, for_update=False):
225
    try:
226
        objects = FloatingIP.objects
227
        if for_update:
228
            objects = objects.select_for_update()
229
        return objects.get(userid=user_id, ipv4=ipv4, deleted=False)
230
    except FloatingIP.DoesNotExist:
231
        raise faults.ItemNotFound("Floating IP does not exist.")
232

    
233

    
234
def validate_network_params(subnet=None, gateway=None, subnet6=None,
235
                            gateway6=None):
236
    if (subnet is None) and (subnet6 is None):
237
        raise faults.BadRequest("subnet or subnet6 is required")
238

    
239
    if subnet:
240
        try:
241
            # Use strict option to not all subnets with host bits set
242
            network = ipaddr.IPv4Network(subnet, strict=True)
243
        except ValueError:
244
            raise faults.BadRequest("Invalid network IPv4 subnet")
245

    
246
        # Check that network size is allowed!
247
        if not validate_network_size(network.prefixlen):
248
            raise faults.OverLimit(message="Unsupported network size",
249
                                   details="Network mask must be in range"
250
                                           " (%s, 29]" % MAX_CIDR_BLOCK)
251
        if gateway:  # Check that gateway belongs to network
252
            try:
253
                gateway = ipaddr.IPv4Address(gateway)
254
            except ValueError:
255
                raise faults.BadRequest("Invalid network IPv4 gateway")
256
            if not gateway in network:
257
                raise faults.BadRequest("Invalid network IPv4 gateway")
258

    
259
    if subnet6:
260
        try:
261
            # Use strict option to not all subnets with host bits set
262
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
263
        except ValueError:
264
            raise faults.BadRequest("Invalid network IPv6 subnet")
265
        if gateway6:
266
            try:
267
                gateway6 = ipaddr.IPv6Address(gateway6)
268
            except ValueError:
269
                raise faults.BadRequest("Invalid network IPv6 gateway")
270
            if not gateway6 in network6:
271
                raise faults.BadRequest("Invalid network IPv6 gateway")
272

    
273

    
274
def validate_network_size(cidr_block):
275
    """Return True if network size is allowed."""
276
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
277

    
278

    
279
def allocate_public_address(backend):
280
    """Get a public IP for any available network of a backend."""
281
    # Guarantee exclusive access to backend, because accessing the IP pools of
282
    # the backend networks may result in a deadlock with backend allocator
283
    # which also checks that backend networks have a free IP.
284
    backend = Backend.objects.select_for_update().get(id=backend.id)
285
    public_networks = backend_public_networks(backend)
286
    return get_free_ip(public_networks)
287

    
288

    
289
def backend_public_networks(backend):
290
    """Return available public networks of the backend.
291

292
    Iterator for non-deleted public networks that are available
293
    to the specified backend.
294

295
    """
296
    bnets = BackendNetwork.objects.filter(backend=backend,
297
                                          network__public=True,
298
                                          network__deleted=False,
299
                                          network__floating_ip_pool=False,
300
                                          network__subnet__isnull=False,
301
                                          network__drained=False)
302
    return [b.network for b in bnets]
303

    
304

    
305
def get_free_ip(networks):
306
    for network in networks:
307
        try:
308
            address = get_network_free_address(network)
309
            return network, address
310
        except faults.OverLimit:
311
            pass
312
    msg = "Can not allocate public IP. Public networks are full."
313
    log.error(msg)
314
    raise faults.OverLimit(msg)
315

    
316

    
317
def get_network_free_address(network):
318
    """Reserve an IP address from the IP Pool of the network."""
319

    
320
    pool = network.get_pool()
321
    try:
322
        address = pool.get()
323
    except EmptyPool:
324
        raise faults.OverLimit("Network %s is full." % network.backend_id)
325
    pool.save()
326
    return address
327

    
328

    
329
def get_nic(machine, network):
330
    try:
331
        return NetworkInterface.objects.get(machine=machine, network=network)
332
    except NetworkInterface.DoesNotExist:
333
        raise faults.ItemNotFound('Server not connected to this network.')
334

    
335

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

    
349

    
350
def render_metadata(request, metadata, use_values=False, status=200):
351
    if request.serialization == 'xml':
352
        data = render_to_string('metadata.xml', {'metadata': metadata})
353
    else:
354
        if use_values:
355
            d = {'metadata': {'values': metadata}}
356
        else:
357
            d = {'metadata': metadata}
358
        data = json.dumps(d)
359
    return HttpResponse(data, status=status)
360

    
361

    
362
def render_meta(request, meta, status=200):
363
    if request.serialization == 'xml':
364
        key, val = meta.items()[0]
365
        data = render_to_string('meta.xml', dict(key=key, val=val))
366
    else:
367
        data = json.dumps(dict(meta=meta))
368
    return HttpResponse(data, status=status)
369

    
370

    
371
def construct_nic_id(nic):
372
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
373

    
374

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

    
396

    
397
def values_from_flavor(flavor):
398
    """Get Ganeti connectivity info from flavor type.
399

400
    If link or mac_prefix equals to "pool", then the resources
401
    are allocated from the corresponding Pools.
402

403
    """
404
    try:
405
        flavor = Network.FLAVORS[flavor]
406
    except KeyError:
407
        raise faults.BadRequest("Unknown network flavor")
408

    
409
    mode = flavor.get("mode")
410

    
411
    link = flavor.get("link")
412
    if link == "pool":
413
        link = allocate_resource("bridge")
414

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

    
419
    tags = flavor.get("tags")
420

    
421
    return mode, link, mac_prefix, tags
422

    
423

    
424
def allocate_resource(res_type):
425
    table = get_pool_table(res_type)
426
    pool = table.get_pool()
427
    value = pool.get()
428
    pool.save()
429
    return value
430

    
431

    
432
def release_resource(res_type, value):
433
    table = get_pool_table(res_type)
434
    pool = table.get_pool()
435
    pool.put(value)
436
    pool.save()
437

    
438

    
439
def get_pool_table(res_type):
440
    if res_type == "bridge":
441
        return BridgePoolTable
442
    elif res_type == "mac_prefix":
443
        return MacPrefixPoolTable
444
    else:
445
        raise Exception("Unknown resource type")
446

    
447

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

    
456
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
457
                                                                  flat=True)
458
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
459
                                                                  flat=True)
460
    networkusernames = Network.objects.filter().values_list('userid',
461
                                                            flat=True)
462

    
463
    return set(list(keypairusernames) + list(serverusernames) +
464
               list(networkusernames))
465

    
466

    
467
def vm_to_links(vm_id):
468
    href = join_urls(SERVERS_URL, str(vm_id))
469
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
470

    
471

    
472
def network_to_links(network_id):
473
    href = join_urls(NETWORKS_URL, str(network_id))
474
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
475

    
476

    
477
def flavor_to_links(flavor_id):
478
    href = join_urls(FLAVORS_URL, str(flavor_id))
479
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
480

    
481

    
482
def image_to_links(image_id):
483
    href = join_urls(IMAGES_URL, str(image_id))
484
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
485
    links.append({"rel": "alternate",
486
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
487
    return links
488

    
489

    
490
def start_action(vm, action, jobId):
491
    vm.action = action
492
    vm.backendjobid = jobId
493
    vm.backendopcode = None
494
    vm.backendjobstatus = None
495
    vm.backendlogmsg = None
496
    vm.save()