Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (15.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 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.backend import PlanktonBackend
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 PlanktonBackend(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["checksum"] = img["checksum"]
177
    image["location"] = img["location"]
178

    
179
    checksum = image["checksum"] = img["checksum"]
180
    size = image["size"] = img["size"]
181
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([checksum, str(size)])
182

    
183
    properties = img.get("properties", {})
184
    image["metadata"] = dict((key.upper(), val)
185
                             for key, val in properties.items())
186

    
187
    return image
188

    
189

    
190
def get_flavor(flavor_id, include_deleted=False):
191
    """Return a Flavor instance or raise ItemNotFound."""
192

    
193
    try:
194
        flavor_id = int(flavor_id)
195
        if include_deleted:
196
            return Flavor.objects.get(id=flavor_id)
197
        else:
198
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
199
    except (ValueError, TypeError):
200
        raise faults.BadRequest("Invalid flavor ID '%s'" % flavor_id)
201
    except Flavor.DoesNotExist:
202
        raise faults.ItemNotFound('Flavor not found.')
203

    
204

    
205
def get_flavor_provider(flavor):
206
    """Extract provider from disk template.
207

208
    Provider for `ext` disk_template is encoded in the disk template
209
    name, which is formed `ext_<provider_name>`. Provider is None
210
    for all other disk templates.
211

212
    """
213
    disk_template = flavor.disk_template
214
    provider = None
215
    if disk_template.startswith("ext"):
216
        disk_template, provider = disk_template.split("_", 1)
217
    return disk_template, provider
218

    
219

    
220
def get_network(network_id, user_id, for_update=False, non_deleted=False):
221
    """Return a Network instance or raise ItemNotFound."""
222

    
223
    try:
224
        network_id = int(network_id)
225
        objects = Network.objects
226
        if for_update:
227
            objects = objects.select_for_update()
228
        network = objects.get(Q(userid=user_id) | Q(public=True),
229
                              id=network_id)
230
        if non_deleted and network.deleted:
231
            raise faults.BadRequest("Network has been deleted.")
232
        return network
233
    except (ValueError, TypeError):
234
        raise faults.BadRequest("Invalid network ID '%s'" % network_id)
235
    except Network.DoesNotExist:
236
        raise faults.ItemNotFound('Network %s not found.' % network_id)
237

    
238

    
239
def get_port(port_id, user_id, for_update=False):
240
    """
241
    Return a NetworkInteface instance or raise ItemNotFound.
242
    """
243
    try:
244
        objects = NetworkInterface.objects.filter(userid=user_id)
245
        if for_update:
246
            objects = objects.select_for_update()
247
        # if (port.device_owner != "vm") and for_update:
248
        #     raise faults.BadRequest('Cannot update non vm port')
249
        return objects.get(id=port_id)
250
    except (ValueError, TypeError):
251
        raise faults.BadRequest("Invalid port ID '%s'" % port_id)
252
    except NetworkInterface.DoesNotExist:
253
        raise faults.ItemNotFound("Port '%s' not found." % port_id)
254

    
255

    
256
def get_security_group(sg_id):
257
    try:
258
        sg = SecurityGroup.objects.get(id=sg_id)
259
        return sg
260
    except (ValueError, SecurityGroup.DoesNotExist):
261
        raise faults.ItemNotFound("Not valid security group")
262

    
263

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

    
274

    
275
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
276
    try:
277
        floating_ip_id = int(floating_ip_id)
278
        objects = IPAddress.objects
279
        if for_update:
280
            objects = objects.select_for_update()
281
        return objects.get(id=floating_ip_id, floating_ip=True,
282
                           userid=userid, deleted=False)
283
    except IPAddress.DoesNotExist:
284
        raise faults.ItemNotFound("Floating IP with ID %s does not exist." %
285
                                  floating_ip_id)
286
    except (ValueError, TypeError):
287
        raise faults.BadRequest("Invalid Floating IP ID %s" % floating_ip_id)
288

    
289

    
290
def backend_has_free_public_ip(backend):
291
    """Check if a backend has a free public IPv4 address."""
292
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
293
        .filter(subnet__network__public=True)\
294
        .filter(subnet__network__drained=False)\
295
        .filter(subnet__deleted=False)\
296
        .filter(subnet__network__backend_networks__backend=backend)
297
    for pool_row in ip_pool_rows:
298
        pool = pool_row.pool
299
        if pool.empty():
300
            continue
301
        else:
302
            return True
303

    
304

    
305
def backend_public_networks(backend):
306
    return Network.objects.filter(deleted=False, public=True,
307
                                  backend_networks__backend=backend)
308

    
309

    
310
def get_vm_nic(vm, nic_id):
311
    """Get a VMs NIC by its ID."""
312
    try:
313
        return vm.nics.get(id=nic_id)
314
    except NetworkInterface.DoesNotExist:
315
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
316

    
317

    
318
def get_nic(nic_id):
319
    try:
320
        return NetworkInterface.objects.get(id=nic_id)
321
    except NetworkInterface.DoesNotExist:
322
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
323

    
324

    
325
def render_metadata(request, metadata, use_values=False, status=200):
326
    if request.serialization == 'xml':
327
        data = render_to_string('metadata.xml', {'metadata': metadata})
328
    else:
329
        if use_values:
330
            d = {'metadata': {'values': metadata}}
331
        else:
332
            d = {'metadata': metadata}
333
        data = json.dumps(d)
334
    return HttpResponse(data, status=status)
335

    
336

    
337
def render_meta(request, meta, status=200):
338
    if request.serialization == 'xml':
339
        key, val = meta.items()[0]
340
        data = render_to_string('meta.xml', dict(key=key, val=val))
341
    else:
342
        data = json.dumps(dict(meta=meta))
343
    return HttpResponse(data, status=status)
344

    
345

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

    
367

    
368
def values_from_flavor(flavor):
369
    """Get Ganeti connectivity info from flavor type.
370

371
    If link or mac_prefix equals to "pool", then the resources
372
    are allocated from the corresponding Pools.
373

374
    """
375
    try:
376
        flavor = Network.FLAVORS[flavor]
377
    except KeyError:
378
        raise faults.BadRequest("Unknown network flavor")
379

    
380
    mode = flavor.get("mode")
381

    
382
    link = flavor.get("link")
383
    if link == "pool":
384
        link = allocate_resource("bridge")
385

    
386
    mac_prefix = flavor.get("mac_prefix")
387
    if mac_prefix == "pool":
388
        mac_prefix = allocate_resource("mac_prefix")
389

    
390
    tags = flavor.get("tags")
391

    
392
    return mode, link, mac_prefix, tags
393

    
394

    
395
def allocate_resource(res_type):
396
    table = get_pool_table(res_type)
397
    pool = table.get_pool()
398
    value = pool.get()
399
    pool.save()
400
    return value
401

    
402

    
403
def release_resource(res_type, value):
404
    table = get_pool_table(res_type)
405
    pool = table.get_pool()
406
    pool.put(value)
407
    pool.save()
408

    
409

    
410
def get_pool_table(res_type):
411
    if res_type == "bridge":
412
        return BridgePoolTable
413
    elif res_type == "mac_prefix":
414
        return MacPrefixPoolTable
415
    else:
416
        raise Exception("Unknown resource type")
417

    
418

    
419
def get_existing_users():
420
    """
421
    Retrieve user ids stored in cyclades user agnostic models.
422
    """
423
    # also check PublicKeys a user with no servers/networks exist
424
    from synnefo.userdata.models import PublicKeyPair
425
    from synnefo.db.models import VirtualMachine, Network
426

    
427
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
428
                                                                  flat=True)
429
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
430
                                                                  flat=True)
431
    networkusernames = Network.objects.filter().values_list('userid',
432
                                                            flat=True)
433

    
434
    return set(list(keypairusernames) + list(serverusernames) +
435
               list(networkusernames))
436

    
437

    
438
def vm_to_links(vm_id):
439
    href = join_urls(SERVERS_URL, str(vm_id))
440
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
441

    
442

    
443
def network_to_links(network_id):
444
    href = join_urls(NETWORKS_URL, str(network_id))
445
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
446

    
447

    
448
def subnet_to_links(subnet_id):
449
    href = join_urls(SUBNETS_URL, str(subnet_id))
450
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
451

    
452

    
453
def port_to_links(port_id):
454
    href = join_urls(PORTS_URL, str(port_id))
455
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
456

    
457

    
458
def flavor_to_links(flavor_id):
459
    href = join_urls(FLAVORS_URL, str(flavor_id))
460
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
461

    
462

    
463
def image_to_links(image_id):
464
    href = join_urls(IMAGES_URL, str(image_id))
465
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
466
    links.append({"rel": "alternate",
467
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
468
    return links
469

    
470

    
471
def start_action(vm, action, jobId):
472
    vm.action = action
473
    vm.backendjobid = jobId
474
    vm.backendopcode = None
475
    vm.backendjobstatus = None
476
    vm.backendlogmsg = None
477
    vm.save()