Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (15.4 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, NetworkInterface, SecurityGroup,
51
                               BridgePoolTable, MacPrefixPoolTable, IPAddress,
52
                               IPPoolTable)
53
from synnefo.plankton.utils import image_backend
54

    
55
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
56
from synnefo.lib.services import get_service_path
57
from synnefo.lib import join_urls
58

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

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

    
78
PITHOSMAP_PREFIX = "pithosmap://"
79

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

    
82

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

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

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

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

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

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

    
110
    return password
111

    
112

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

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

    
120

    
121
def encrypt(plaintext):
122
    # Make sure key is 32 bytes long
123
    key = sha256(settings.SECRET_KEY).digest()
124

    
125
    aes = AES.new(key)
126
    enc = aes.encrypt(zeropad(plaintext))
127
    return b64encode(enc)
128

    
129

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

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

    
152

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

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

    
161

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

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

    
168

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

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

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

    
186
    return image
187

    
188

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

    
192
    try:
193
        flavor_id = int(flavor_id)
194
        if include_deleted:
195
            return Flavor.objects.get(id=flavor_id)
196
        else:
197
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
198
    except (ValueError, Flavor.DoesNotExist):
199
        raise faults.ItemNotFound('Flavor not found.')
200

    
201

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

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

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

    
216

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

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

    
233

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

    
248

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

    
256

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

    
267

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

    
279

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

    
294

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

    
299

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

    
307

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

    
314

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

    
326

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

    
335

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

    
357

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

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

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

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

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

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

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

    
382
    return mode, link, mac_prefix, tags
383

    
384

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

    
392

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

    
399

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

    
408

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

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

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

    
427

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

    
432

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

    
437

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

    
442

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

    
447

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

    
452

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

    
460

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