Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (15.8 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
    mapfile = img["mapfile"]
181
    if mapfile.startswith("archip:"):
182
        _, unprefixed_mapfile, = mapfile.split("archip:")
183
        mapfile = unprefixed_mapfile
184
    else:
185
        unprefixed_mapfile = mapfile
186
        mapfile = "pithos:" + mapfile
187

    
188
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([unprefixed_mapfile,
189
                                                       str(size)])
190
    image["mapfile"] = mapfile
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_network(network_id, user_id, for_update=False, non_deleted=False):
215
    """Return a Network instance or raise ItemNotFound."""
216

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

    
232

    
233
def get_port(port_id, user_id, for_update=False):
234
    """
235
    Return a NetworkInteface instance or raise ItemNotFound.
236
    """
237
    try:
238
        objects = NetworkInterface.objects.filter(userid=user_id)
239
        if for_update:
240
            objects = objects.select_for_update()
241
        # if (port.device_owner != "vm") and for_update:
242
        #     raise faults.BadRequest('Cannot update non vm port')
243
        return objects.get(id=port_id)
244
    except (ValueError, TypeError):
245
        raise faults.BadRequest("Invalid port ID '%s'" % port_id)
246
    except 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
        floating_ip_id = int(floating_ip_id)
272
        objects = IPAddress.objects
273
        if for_update:
274
            objects = objects.select_for_update()
275
        return objects.get(id=floating_ip_id, floating_ip=True,
276
                           userid=userid, deleted=False)
277
    except IPAddress.DoesNotExist:
278
        raise faults.ItemNotFound("Floating IP with ID %s does not exist." %
279
                                  floating_ip_id)
280
    except (ValueError, TypeError):
281
        raise faults.BadRequest("Invalid Floating IP ID %s" % floating_ip_id)
282

    
283

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

    
298

    
299
def backend_public_networks(backend):
300
    return Network.objects.filter(deleted=False, public=True,
301
                                  backend_networks__backend=backend)
302

    
303

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

    
311

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

    
318

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

    
330

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

    
339

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

    
361

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

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

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

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

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

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

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

    
386
    return mode, link, mac_prefix, tags
387

    
388

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

    
396

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

    
403

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

    
412

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

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

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

    
431

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

    
436

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

    
441

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

    
446

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

    
451

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

    
456

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

    
464

    
465
def start_action(vm, action, jobId):
466
    vm.action = action
467
    vm.backendjobid = jobId
468
    vm.backendopcode = None
469
    vm.backendjobstatus = None
470
    vm.backendlogmsg = None
471
    vm.save()