Statistics
| Branch: | Tag: | Revision:

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

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
    image["is_snapshot"] = img["is_snapshot"]
178
    size = image["size"] = img["size"]
179

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

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

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

    
196
    return image
197

    
198

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

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

    
213

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

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

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

    
228

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

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

    
247

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

    
264

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

    
272

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

    
283

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

    
298

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

    
313

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

    
318

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

    
326

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

    
333

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

    
345

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

    
354

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

    
376

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

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

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

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

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

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

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

    
401
    return mode, link, mac_prefix, tags
402

    
403

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

    
411

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

    
418

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

    
427

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

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

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

    
446

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

    
451

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

    
456

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

    
461

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

    
466

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

    
471

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

    
479

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