Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (16.2 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 urlsafe_b64encode, b64decode
35
from urllib import quote
36
from hashlib import sha256
37
from logging import getLogger
38
from random import choice
39
from string import digits, lowercase, uppercase
40

    
41
from Crypto.Cipher import AES
42

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

    
49
from snf_django.lib.api import faults
50
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
51
                               Network, NetworkInterface, SecurityGroup,
52
                               BridgePoolTable, MacPrefixPoolTable, IPAddress,
53
                               IPPoolTable)
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
FLAVORS_URL = join_urls(COMPUTE_URL, "flavors/")
65
IMAGES_URL = join_urls(COMPUTE_URL, "images/")
66
PLANKTON_URL = \
67
    join_urls(BASE_HOST,
68
              get_service_path(cyclades_services, "image", version="v1.0"))
69
IMAGES_PLANKTON_URL = join_urls(PLANKTON_URL, "images/")
70

    
71
NETWORK_URL = \
72
    join_urls(BASE_HOST,
73
              get_service_path(cyclades_services, "network", version="v2.0"))
74
NETWORKS_URL = join_urls(NETWORK_URL, "networks/")
75
PORTS_URL = join_urls(NETWORK_URL, "ports/")
76
SUBNETS_URL = join_urls(NETWORK_URL, "subnets/")
77
FLOATING_IPS_URL = join_urls(NETWORK_URL, "floatingips/")
78

    
79
PITHOSMAP_PREFIX = "pithosmap://"
80

    
81
log = getLogger('synnefo.api')
82

    
83

    
84
def random_password():
85
    """Generates a random password
86

87
    We generate a windows compliant password: it must contain at least
88
    one charachter from each of the groups: upper case, lower case, digits.
89
    """
90

    
91
    pool = lowercase + uppercase + digits
92
    lowerset = set(lowercase)
93
    upperset = set(uppercase)
94
    digitset = set(digits)
95
    length = 10
96

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

    
99
    # Make sure the password is compliant
100
    chars = set(password)
101
    if not chars & lowerset:
102
        password += choice(lowercase)
103
    if not chars & upperset:
104
        password += choice(uppercase)
105
    if not chars & digitset:
106
        password += choice(digits)
107

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

    
111
    return password
112

    
113

    
114
def zeropad(s):
115
    """Add zeros at the end of a string in order to make its length
116
       a multiple of 16."""
117

    
118
    npad = 16 - len(s) % 16
119
    return s + '\x00' * npad
120

    
121

    
122
def stats_encrypt(plaintext):
123
    # Make sure key is 32 bytes long
124
    key = sha256(settings.CYCLADES_STATS_SECRET_KEY).digest()
125

    
126
    aes = AES.new(key)
127
    enc = aes.encrypt(zeropad(plaintext))
128
    return quote(urlsafe_b64encode(enc))
129

    
130

    
131
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
132
           non_suspended=False, prefetch_related=None):
133
    """Find a VirtualMachine instance based on ID and owner."""
134

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

    
153

    
154
def get_vm_meta(vm, key):
155
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
156

    
157
    try:
158
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
159
    except VirtualMachineMetadata.DoesNotExist:
160
        raise faults.ItemNotFound('Metadata key not found.')
161

    
162

    
163
def get_image(image_id, user_id):
164
    """Return an Image instance or raise ItemNotFound."""
165

    
166
    with image_backend(user_id) as backend:
167
        return backend.get_image(image_id)
168

    
169

    
170
def get_image_dict(image_id, user_id):
171
    image = {}
172
    img = get_image(image_id, user_id)
173
    image["id"] = img["id"]
174
    image["name"] = img["name"]
175
    image["format"] = img["disk_format"]
176
    image["location"] = img["location"]
177
    size = image["size"] = img["size"]
178

    
179
    checksum = img["checksum"]
180
    if checksum.startswith("archip:"):
181
        unprefixed_checksum, _ = checksum.split("archip:")
182
        checksum = unprefixed_checksum
183
    else:
184
        unprefixed_checksum = checksum
185
        checksum = "pithos:" + checksum
186

    
187
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([unprefixed_checksum,
188
                                                       str(size)])
189
    image["checksum"] = checksum
190

    
191
    properties = img.get("properties", {})
192
    image["metadata"] = dict((key.upper(), val)
193
                             for key, val in properties.items())
194

    
195
    return image
196

    
197

    
198
def get_flavor(flavor_id, include_deleted=False):
199
    """Return a Flavor instance or raise ItemNotFound."""
200

    
201
    try:
202
        flavor_id = int(flavor_id)
203
        if include_deleted:
204
            return Flavor.objects.get(id=flavor_id)
205
        else:
206
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
207
    except (ValueError, TypeError):
208
        raise faults.BadRequest("Invalid flavor ID '%s'" % flavor_id)
209
    except Flavor.DoesNotExist:
210
        raise faults.ItemNotFound('Flavor not found.')
211

    
212

    
213
def get_flavor_provider(flavor):
214
    """Extract provider from disk template.
215

216
    Provider for `ext` disk_template is encoded in the disk template
217
    name, which is formed `ext_<provider_name>`. Provider is None
218
    for all other disk templates.
219

220
    """
221
    disk_template = flavor.disk_template
222
    provider = None
223
    if disk_template.startswith("ext"):
224
        disk_template, provider = disk_template.split("_", 1)
225
    return disk_template, provider
226

    
227

    
228
def get_network(network_id, user_id, for_update=False, non_deleted=False):
229
    """Return a Network instance or raise ItemNotFound."""
230

    
231
    try:
232
        network_id = int(network_id)
233
        objects = Network.objects
234
        if for_update:
235
            objects = objects.select_for_update()
236
        network = objects.get(Q(userid=user_id) | Q(public=True),
237
                              id=network_id)
238
        if non_deleted and network.deleted:
239
            raise faults.BadRequest("Network has been deleted.")
240
        return network
241
    except (ValueError, TypeError):
242
        raise faults.BadRequest("Invalid network ID '%s'" % network_id)
243
    except Network.DoesNotExist:
244
        raise faults.ItemNotFound('Network %s not found.' % network_id)
245

    
246

    
247
def get_port(port_id, user_id, for_update=False):
248
    """
249
    Return a NetworkInteface instance or raise ItemNotFound.
250
    """
251
    try:
252
        objects = NetworkInterface.objects.filter(userid=user_id)
253
        if for_update:
254
            objects = objects.select_for_update()
255
        # if (port.device_owner != "vm") and for_update:
256
        #     raise faults.BadRequest('Cannot update non vm port')
257
        return objects.get(id=port_id)
258
    except (ValueError, TypeError):
259
        raise faults.BadRequest("Invalid port ID '%s'" % port_id)
260
    except NetworkInterface.DoesNotExist:
261
        raise faults.ItemNotFound("Port '%s' not found." % port_id)
262

    
263

    
264
def get_security_group(sg_id):
265
    try:
266
        sg = SecurityGroup.objects.get(id=sg_id)
267
        return sg
268
    except (ValueError, SecurityGroup.DoesNotExist):
269
        raise faults.ItemNotFound("Not valid security group")
270

    
271

    
272
def get_floating_ip_by_address(userid, address, for_update=False):
273
    try:
274
        objects = IPAddress.objects
275
        if for_update:
276
            objects = objects.select_for_update()
277
        return objects.get(userid=userid, floating_ip=True,
278
                           address=address, deleted=False)
279
    except IPAddress.DoesNotExist:
280
        raise faults.ItemNotFound("Floating IP does not exist.")
281

    
282

    
283
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
284
    try:
285
        floating_ip_id = int(floating_ip_id)
286
        objects = IPAddress.objects
287
        if for_update:
288
            objects = objects.select_for_update()
289
        return objects.get(id=floating_ip_id, floating_ip=True,
290
                           userid=userid, deleted=False)
291
    except IPAddress.DoesNotExist:
292
        raise faults.ItemNotFound("Floating IP with ID %s does not exist." %
293
                                  floating_ip_id)
294
    except (ValueError, TypeError):
295
        raise faults.BadRequest("Invalid Floating IP ID %s" % floating_ip_id)
296

    
297

    
298
def backend_has_free_public_ip(backend):
299
    """Check if a backend has a free public IPv4 address."""
300
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
301
        .filter(subnet__network__public=True)\
302
        .filter(subnet__network__drained=False)\
303
        .filter(subnet__deleted=False)\
304
        .filter(subnet__network__backend_networks__backend=backend)
305
    for pool_row in ip_pool_rows:
306
        pool = pool_row.pool
307
        if pool.empty():
308
            continue
309
        else:
310
            return True
311

    
312

    
313
def backend_public_networks(backend):
314
    return Network.objects.filter(deleted=False, public=True,
315
                                  backend_networks__backend=backend)
316

    
317

    
318
def get_vm_nic(vm, nic_id):
319
    """Get a VMs NIC by its ID."""
320
    try:
321
        return vm.nics.get(id=nic_id)
322
    except NetworkInterface.DoesNotExist:
323
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
324

    
325

    
326
def get_nic(nic_id):
327
    try:
328
        return NetworkInterface.objects.get(id=nic_id)
329
    except NetworkInterface.DoesNotExist:
330
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
331

    
332

    
333
def render_metadata(request, metadata, use_values=False, status=200):
334
    if request.serialization == 'xml':
335
        data = render_to_string('metadata.xml', {'metadata': metadata})
336
    else:
337
        if use_values:
338
            d = {'metadata': {'values': metadata}}
339
        else:
340
            d = {'metadata': metadata}
341
        data = json.dumps(d)
342
    return HttpResponse(data, status=status)
343

    
344

    
345
def render_meta(request, meta, status=200):
346
    if request.serialization == 'xml':
347
        key, val = meta.items()[0]
348
        data = render_to_string('meta.xml', dict(key=key, val=val))
349
    else:
350
        data = json.dumps(dict(meta=meta))
351
    return HttpResponse(data, status=status)
352

    
353

    
354
def verify_personality(personality):
355
    """Verify that a a list of personalities is well formed"""
356
    if len(personality) > settings.MAX_PERSONALITY:
357
        raise faults.OverLimit("Maximum number of personalities"
358
                               " exceeded")
359
    for p in personality:
360
        # Verify that personalities are well-formed
361
        try:
362
            assert isinstance(p, dict)
363
            keys = set(p.keys())
364
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
365
            assert keys.issubset(allowed)
366
            contents = p['contents']
367
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
368
                # No need to decode if contents already exceed limit
369
                raise faults.OverLimit("Maximum size of personality exceeded")
370
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
371
                raise faults.OverLimit("Maximum size of personality exceeded")
372
        except (AssertionError, TypeError):
373
            raise faults.BadRequest("Malformed personality in request")
374

    
375

    
376
def values_from_flavor(flavor):
377
    """Get Ganeti connectivity info from flavor type.
378

379
    If link or mac_prefix equals to "pool", then the resources
380
    are allocated from the corresponding Pools.
381

382
    """
383
    try:
384
        flavor = Network.FLAVORS[flavor]
385
    except KeyError:
386
        raise faults.BadRequest("Unknown network flavor")
387

    
388
    mode = flavor.get("mode")
389

    
390
    link = flavor.get("link")
391
    if link == "pool":
392
        link = allocate_resource("bridge")
393

    
394
    mac_prefix = flavor.get("mac_prefix")
395
    if mac_prefix == "pool":
396
        mac_prefix = allocate_resource("mac_prefix")
397

    
398
    tags = flavor.get("tags")
399

    
400
    return mode, link, mac_prefix, tags
401

    
402

    
403
def allocate_resource(res_type):
404
    table = get_pool_table(res_type)
405
    pool = table.get_pool()
406
    value = pool.get()
407
    pool.save()
408
    return value
409

    
410

    
411
def release_resource(res_type, value):
412
    table = get_pool_table(res_type)
413
    pool = table.get_pool()
414
    pool.put(value)
415
    pool.save()
416

    
417

    
418
def get_pool_table(res_type):
419
    if res_type == "bridge":
420
        return BridgePoolTable
421
    elif res_type == "mac_prefix":
422
        return MacPrefixPoolTable
423
    else:
424
        raise Exception("Unknown resource type")
425

    
426

    
427
def get_existing_users():
428
    """
429
    Retrieve user ids stored in cyclades user agnostic models.
430
    """
431
    # also check PublicKeys a user with no servers/networks exist
432
    from synnefo.userdata.models import PublicKeyPair
433
    from synnefo.db.models import VirtualMachine, Network
434

    
435
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
436
                                                                  flat=True)
437
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
438
                                                                  flat=True)
439
    networkusernames = Network.objects.filter().values_list('userid',
440
                                                            flat=True)
441

    
442
    return set(list(keypairusernames) + list(serverusernames) +
443
               list(networkusernames))
444

    
445

    
446
def vm_to_links(vm_id):
447
    href = join_urls(SERVERS_URL, str(vm_id))
448
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
449

    
450

    
451
def network_to_links(network_id):
452
    href = join_urls(NETWORKS_URL, str(network_id))
453
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
454

    
455

    
456
def subnet_to_links(subnet_id):
457
    href = join_urls(SUBNETS_URL, str(subnet_id))
458
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
459

    
460

    
461
def port_to_links(port_id):
462
    href = join_urls(PORTS_URL, str(port_id))
463
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
464

    
465

    
466
def flavor_to_links(flavor_id):
467
    href = join_urls(FLAVORS_URL, str(flavor_id))
468
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
469

    
470

    
471
def image_to_links(image_id):
472
    href = join_urls(IMAGES_URL, str(image_id))
473
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
474
    links.append({"rel": "alternate",
475
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
476
    return links
477

    
478

    
479
def start_action(vm, action, jobId):
480
    vm.action = action
481
    vm.backendjobid = jobId
482
    vm.backendopcode = None
483
    vm.backendjobstatus = None
484
    vm.backendlogmsg = None
485
    vm.save()