Statistics
| Branch: | Tag: | Revision:

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

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

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

    
189
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([unprefixed_mapfile,
190
                                                       str(size)])
191
    image["mapfile"] = mapfile
192

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

    
197
    return image
198

    
199

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

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

    
214

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

    
218
    try:
219
        network_id = int(network_id)
220
        objects = Network.objects
221
        if for_update:
222
            objects = objects.select_for_update()
223
        network = objects.get(Q(userid=user_id) | Q(public=True),
224
                              id=network_id)
225
        if non_deleted and network.deleted:
226
            raise faults.BadRequest("Network has been deleted.")
227
        return network
228
    except (ValueError, TypeError):
229
        raise faults.BadRequest("Invalid network ID '%s'" % network_id)
230
    except 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, TypeError):
246
        raise faults.BadRequest("Invalid port ID '%s'" % port_id)
247
    except NetworkInterface.DoesNotExist:
248
        raise faults.ItemNotFound("Port '%s' not found." % port_id)
249

    
250

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

    
258

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

    
269

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

    
284

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

    
299

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

    
304

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

    
312

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

    
319

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

    
331

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

    
340

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

    
362

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

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

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

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

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

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

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

    
387
    return mode, link, mac_prefix, tags
388

    
389

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

    
397

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

    
404

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

    
413

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

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

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

    
432

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

    
437

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

    
442

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

    
447

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

    
452

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

    
457

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

    
465

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