Statistics
| Branch: | Tag: | Revision:

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

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

    
40
from Crypto.Cipher import AES
41

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

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

    
55
from synnefo.plankton.utils import image_backend
56

    
57
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
58
from synnefo.lib.services import get_service_path
59
from synnefo.lib import join_urls
60

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

    
73
PITHOSMAP_PREFIX = "pithosmap://"
74

    
75
log = getLogger('synnefo.api')
76

    
77

    
78
def random_password():
79
    """Generates a random password
80

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

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

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

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

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

    
105
    return password
106

    
107

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

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

    
115

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

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

    
124

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

    
129
    try:
130
        server_id = int(server_id)
131
        servers = VirtualMachine.objects
132
        if for_update:
133
            servers = servers.select_for_update()
134
        if prefetch_related is not None:
135
            servers = servers.prefetch_related(prefetch_related)
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
    image["id"] = img["id"]
168
    image["name"] = img["name"]
169
    image["format"] = img["disk_format"]
170
    image["checksum"] = img["checksum"]
171
    image["location"] = img["location"]
172

    
173
    checksum = image["checksum"] = img["checksum"]
174
    size = image["size"] = img["size"]
175
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([checksum, str(size)])
176

    
177
    properties = img.get("properties", {})
178
    image["metadata"] = dict((key.upper(), val)
179
                             for key, val in properties.items())
180

    
181
    return image
182

    
183

    
184
def get_flavor(flavor_id, include_deleted=False):
185
    """Return a Flavor instance or raise ItemNotFound."""
186

    
187
    try:
188
        flavor_id = int(flavor_id)
189
        if include_deleted:
190
            return Flavor.objects.get(id=flavor_id)
191
        else:
192
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
193
    except (ValueError, Flavor.DoesNotExist):
194
        raise faults.ItemNotFound('Flavor not found.')
195

    
196

    
197
def get_flavor_provider(flavor):
198
    """Extract provider from disk template.
199

200
    Provider for `ext` disk_template is encoded in the disk template
201
    name, which is formed `ext_<provider_name>`. Provider is None
202
    for all other disk templates.
203

204
    """
205
    disk_template = flavor.disk_template
206
    provider = None
207
    if disk_template.startswith("ext"):
208
        disk_template, provider = disk_template.split("_", 1)
209
    return disk_template, provider
210

    
211

    
212
def get_network(network_id, user_id, for_update=False, non_deleted=False):
213
    """Return a Network instance or raise ItemNotFound."""
214

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

    
228

    
229
def get_port(port_id, user_id, for_update=False):
230
    """
231
    Return a NetworkInteface instance or raise ItemNotFound.
232
    """
233
    try:
234
        objects = NetworkInterface.objects
235
        if for_update:
236
            objects = objects.select_for_update()
237

    
238
        port = objects.get(network__userid=user_id, id=port_id)
239

    
240
        if (port.device_owner != "vm") and for_update:
241
            raise faults.BadRequest('Can not update non vm port')
242

    
243
        return port
244
    except (ValueError, NetworkInterface.DoesNotExist):
245
        raise faults.ItemNotFound('Port not found.')
246

    
247

    
248
def get_floating_ip(user_id, ipv4, for_update=False):
249
    try:
250
        objects = IPAddress.objects
251
        if for_update:
252
            objects = objects.select_for_update()
253
        return objects.get(userid=user_id, ipv4=ipv4, deleted=False)
254
    except IPAddress.DoesNotExist:
255
        raise faults.ItemNotFound("Floating IP does not exist.")
256

    
257

    
258
def allocate_public_address(backend, userid):
259
    """Get a public IP for any available network of a backend."""
260
    # Guarantee exclusive access to backend, because accessing the IP pools of
261
    # the backend networks may result in a deadlock with backend allocator
262
    # which also checks that backend networks have a free IP.
263
    backend = Backend.objects.select_for_update().get(id=backend.id)
264
    public_networks = backend_public_networks(backend)
265
    return get_free_ip(public_networks, userid)
266

    
267

    
268
def backend_public_networks(backend):
269
    """Return available public networks of the backend.
270

271
    Iterator for non-deleted public networks that are available
272
    to the specified backend.
273

274
    """
275
    bnets = BackendNetwork.objects.filter(backend=backend,
276
                                          network__public=True,
277
                                          network__deleted=False,
278
                                          network__floating_ip_pool=False,
279
                                          network__drained=False)
280
    return [b.network for b in bnets]
281

    
282

    
283
def get_free_ip(networks, userid):
284
    for network in networks:
285
        try:
286
            return network.allocate_address(userid=userid)
287
        except faults.OverLimit:
288
            pass
289
    msg = "Can not allocate public IP. Public networks are full."
290
    log.error(msg)
291
    raise faults.OverLimit(msg)
292

    
293

    
294
def get_network_free_address(network, userid):
295
    """Reserve an IP address from the IP Pool of the network."""
296

    
297
    try:
298
        return network.allocate_address(userid=userid)
299
    except EmptyPool:
300
        raise faults.OverLimit("Network %s is full." % network.backend_id)
301

    
302

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

    
310

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

    
317

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

    
329

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

    
338

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

    
360

    
361
def values_from_flavor(flavor):
362
    """Get Ganeti connectivity info from flavor type.
363

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

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

    
373
    mode = flavor.get("mode")
374

    
375
    link = flavor.get("link")
376
    if link == "pool":
377
        link = allocate_resource("bridge")
378

    
379
    mac_prefix = flavor.get("mac_prefix")
380
    if mac_prefix == "pool":
381
        mac_prefix = allocate_resource("mac_prefix")
382

    
383
    tags = flavor.get("tags")
384

    
385
    return mode, link, mac_prefix, tags
386

    
387

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

    
395

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

    
402

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

    
411

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

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

    
427
    return set(list(keypairusernames) + list(serverusernames) +
428
               list(networkusernames))
429

    
430

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

    
435

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

    
440

    
441
def flavor_to_links(flavor_id):
442
    href = join_urls(FLAVORS_URL, str(flavor_id))
443
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
444

    
445

    
446
def image_to_links(image_id):
447
    href = join_urls(IMAGES_URL, str(image_id))
448
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
449
    links.append({"rel": "alternate",
450
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
451
    return links
452

    
453

    
454
def start_action(vm, action, jobId):
455
    vm.action = action
456
    vm.backendjobid = jobId
457
    vm.backendopcode = None
458
    vm.backendjobstatus = None
459
    vm.backendlogmsg = None
460
    vm.save()