Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (15.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
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["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, Flavor.DoesNotExist):
200
        raise faults.ItemNotFound('Flavor not found.')
201

    
202

    
203
def get_flavor_provider(flavor):
204
    """Extract provider from disk template.
205

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

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

    
217

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

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

    
234

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

    
249

    
250
def get_security_group(sg_id):
251
    try:
252
        sg = SecurityGroup.objects.get(id=sg_id)
253
        return sg
254
    except (ValueError, SecurityGroup.DoesNotExist):
255
        raise faults.ItemNotFound("Not valid security group")
256

    
257

    
258
def get_floating_ip_by_address(userid, address, for_update=False):
259
    try:
260
        objects = IPAddress.objects
261
        if for_update:
262
            objects = objects.select_for_update()
263
        return objects.get(userid=userid, floating_ip=True,
264
                           address=address, deleted=False)
265
    except IPAddress.DoesNotExist:
266
        raise faults.ItemNotFound("Floating IP does not exist.")
267

    
268

    
269
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
270
    try:
271
        objects = IPAddress.objects
272
        if for_update:
273
            objects = objects.select_for_update()
274
        return objects.get(id=floating_ip_id, floating_ip=True,
275
                           userid=userid, deleted=False)
276
    except IPAddress.DoesNotExist:
277
        raise faults.ItemNotFound("Floating IP with ID %s does not exist." %
278
                                  floating_ip_id)
279

    
280

    
281
def backend_has_free_public_ip(backend):
282
    """Check if a backend has a free public IPv4 address."""
283
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
284
        .filter(subnet__network__public=True)\
285
        .filter(subnet__network__drained=False)\
286
        .filter(subnet__deleted=False)\
287
        .filter(subnet__network__backend_networks__backend=backend)
288
    for pool_row in ip_pool_rows:
289
        pool = pool_row.pool
290
        if pool.empty():
291
            continue
292
        else:
293
            return True
294

    
295

    
296
def backend_public_networks(backend):
297
    return Network.objects.filter(deleted=False, public=True,
298
                                  backend_networks__backend=backend)
299

    
300

    
301
def get_vm_nic(vm, nic_id):
302
    """Get a VMs NIC by its ID."""
303
    try:
304
        return vm.nics.get(id=nic_id)
305
    except NetworkInterface.DoesNotExist:
306
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
307

    
308

    
309
def get_nic(nic_id):
310
    try:
311
        return NetworkInterface.objects.get(id=nic_id)
312
    except NetworkInterface.DoesNotExist:
313
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
314

    
315

    
316
def render_metadata(request, metadata, use_values=False, status=200):
317
    if request.serialization == 'xml':
318
        data = render_to_string('metadata.xml', {'metadata': metadata})
319
    else:
320
        if use_values:
321
            d = {'metadata': {'values': metadata}}
322
        else:
323
            d = {'metadata': metadata}
324
        data = json.dumps(d)
325
    return HttpResponse(data, status=status)
326

    
327

    
328
def render_meta(request, meta, status=200):
329
    if request.serialization == 'xml':
330
        key, val = meta.items()[0]
331
        data = render_to_string('meta.xml', dict(key=key, val=val))
332
    else:
333
        data = json.dumps(dict(meta=meta))
334
    return HttpResponse(data, status=status)
335

    
336

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

    
358

    
359
def values_from_flavor(flavor):
360
    """Get Ganeti connectivity info from flavor type.
361

362
    If link or mac_prefix equals to "pool", then the resources
363
    are allocated from the corresponding Pools.
364

365
    """
366
    try:
367
        flavor = Network.FLAVORS[flavor]
368
    except KeyError:
369
        raise faults.BadRequest("Unknown network flavor")
370

    
371
    mode = flavor.get("mode")
372

    
373
    link = flavor.get("link")
374
    if link == "pool":
375
        link = allocate_resource("bridge")
376

    
377
    mac_prefix = flavor.get("mac_prefix")
378
    if mac_prefix == "pool":
379
        mac_prefix = allocate_resource("mac_prefix")
380

    
381
    tags = flavor.get("tags")
382

    
383
    return mode, link, mac_prefix, tags
384

    
385

    
386
def allocate_resource(res_type):
387
    table = get_pool_table(res_type)
388
    pool = table.get_pool()
389
    value = pool.get()
390
    pool.save()
391
    return value
392

    
393

    
394
def release_resource(res_type, value):
395
    table = get_pool_table(res_type)
396
    pool = table.get_pool()
397
    pool.put(value)
398
    pool.save()
399

    
400

    
401
def get_pool_table(res_type):
402
    if res_type == "bridge":
403
        return BridgePoolTable
404
    elif res_type == "mac_prefix":
405
        return MacPrefixPoolTable
406
    else:
407
        raise Exception("Unknown resource type")
408

    
409

    
410
def get_existing_users():
411
    """
412
    Retrieve user ids stored in cyclades user agnostic models.
413
    """
414
    # also check PublicKeys a user with no servers/networks exist
415
    from synnefo.userdata.models import PublicKeyPair
416
    from synnefo.db.models import VirtualMachine, Network
417

    
418
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
419
                                                                  flat=True)
420
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
421
                                                                  flat=True)
422
    networkusernames = Network.objects.filter().values_list('userid',
423
                                                            flat=True)
424

    
425
    return set(list(keypairusernames) + list(serverusernames) +
426
               list(networkusernames))
427

    
428

    
429
def vm_to_links(vm_id):
430
    href = join_urls(SERVERS_URL, str(vm_id))
431
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
432

    
433

    
434
def network_to_links(network_id):
435
    href = join_urls(NETWORKS_URL, str(network_id))
436
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
437

    
438

    
439
def subnet_to_links(subnet_id):
440
    href = join_urls(SUBNETS_URL, str(subnet_id))
441
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
442

    
443

    
444
def port_to_links(port_id):
445
    href = join_urls(PORTS_URL, str(port_id))
446
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
447

    
448

    
449
def flavor_to_links(flavor_id):
450
    href = join_urls(FLAVORS_URL, str(flavor_id))
451
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
452

    
453

    
454
def image_to_links(image_id):
455
    href = join_urls(IMAGES_URL, str(image_id))
456
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
457
    links.append({"rel": "alternate",
458
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
459
    return links
460

    
461

    
462
def start_action(vm, action, jobId):
463
    vm.action = action
464
    vm.backendjobid = jobId
465
    vm.backendopcode = None
466
    vm.backendjobstatus = None
467
    vm.backendlogmsg = None
468
    vm.save()