Statistics
| Branch: | Tag: | Revision:

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

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 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(length=10):
85
    """Generates a random password
86

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

    
91
    pool = (lowercase * 5) + (uppercase * 5) + (digits * 13)
92
    lowerset = set(lowercase)
93
    upperset = set(uppercase)
94
    digitset = set(digits)
95

    
96
    while True:
97
        password = ''.join(choice(pool) for i in range(length))
98

    
99
        # Make sure the password is compliant
100
        chars = set(password)
101
        if (chars & lowerset) and (chars & upperset) and (chars & digitset):
102
            return password
103

    
104
        password = ""
105

    
106

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

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

    
114

    
115
def stats_encrypt(plaintext):
116
    # Make sure key is 32 bytes long
117
    key = sha256(settings.CYCLADES_STATS_SECRET_KEY).digest()
118

    
119
    aes = AES.new(key)
120
    enc = aes.encrypt(zeropad(plaintext))
121
    return quote(urlsafe_b64encode(enc))
122

    
123

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

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

    
146

    
147
def get_vm_meta(vm, key):
148
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
149

    
150
    try:
151
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
152
    except VirtualMachineMetadata.DoesNotExist:
153
        raise faults.ItemNotFound('Metadata key not found.')
154

    
155

    
156
def get_image(image_id, user_id):
157
    """Return an Image instance or raise ItemNotFound."""
158

    
159
    with image_backend(user_id) as backend:
160
        return backend.get_image(image_id)
161

    
162

    
163
def get_image_dict(image_id, user_id):
164
    image = {}
165
    img = get_image(image_id, user_id)
166
    image["id"] = img["id"]
167
    image["name"] = img["name"]
168
    image["format"] = img["disk_format"]
169
    image["checksum"] = img["checksum"]
170
    image["location"] = img["location"]
171

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

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

    
180
    return image
181

    
182

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

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

    
195

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

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

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

    
210

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

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

    
227

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

    
242

    
243
def get_security_group(sg_id):
244
    try:
245
        sg = SecurityGroup.objects.get(id=sg_id)
246
        return sg
247
    except (ValueError, SecurityGroup.DoesNotExist):
248
        raise faults.ItemNotFound("Not valid security group")
249

    
250

    
251
def get_floating_ip_by_address(userid, address, for_update=False):
252
    try:
253
        objects = IPAddress.objects
254
        if for_update:
255
            objects = objects.select_for_update()
256
        return objects.get(userid=userid, floating_ip=True,
257
                           address=address, deleted=False)
258
    except IPAddress.DoesNotExist:
259
        raise faults.ItemNotFound("Floating IP does not exist.")
260

    
261

    
262
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
263
    try:
264
        objects = IPAddress.objects
265
        if for_update:
266
            objects = objects.select_for_update()
267
        return objects.get(id=floating_ip_id, floating_ip=True,
268
                           userid=userid, deleted=False)
269
    except IPAddress.DoesNotExist:
270
        raise faults.ItemNotFound("Floating IP with ID %s does not exist." %
271
                                  floating_ip_id)
272

    
273

    
274
def backend_has_free_public_ip(backend):
275
    """Check if a backend has a free public IPv4 address."""
276
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
277
        .filter(subnet__network__public=True)\
278
        .filter(subnet__network__drained=False)\
279
        .filter(subnet__deleted=False)\
280
        .filter(subnet__network__backend_networks__backend=backend)
281
    for pool_row in ip_pool_rows:
282
        pool = pool_row.pool
283
        if pool.empty():
284
            continue
285
        else:
286
            return True
287

    
288

    
289
def backend_public_networks(backend):
290
    return Network.objects.filter(deleted=False, public=True,
291
                                  backend_networks__backend=backend)
292

    
293

    
294
def get_vm_nic(vm, nic_id):
295
    """Get a VMs NIC by its ID."""
296
    try:
297
        return vm.nics.get(id=nic_id)
298
    except NetworkInterface.DoesNotExist:
299
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
300

    
301

    
302
def get_nic(nic_id):
303
    try:
304
        return NetworkInterface.objects.get(id=nic_id)
305
    except NetworkInterface.DoesNotExist:
306
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
307

    
308

    
309
def render_metadata(request, metadata, use_values=False, status=200):
310
    if request.serialization == 'xml':
311
        data = render_to_string('metadata.xml', {'metadata': metadata})
312
    else:
313
        if use_values:
314
            d = {'metadata': {'values': metadata}}
315
        else:
316
            d = {'metadata': metadata}
317
        data = json.dumps(d)
318
    return HttpResponse(data, status=status)
319

    
320

    
321
def render_meta(request, meta, status=200):
322
    if request.serialization == 'xml':
323
        key, val = meta.items()[0]
324
        data = render_to_string('meta.xml', dict(key=key, val=val))
325
    else:
326
        data = json.dumps(dict(meta=meta))
327
    return HttpResponse(data, status=status)
328

    
329

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

    
351

    
352
def values_from_flavor(flavor):
353
    """Get Ganeti connectivity info from flavor type.
354

355
    If link or mac_prefix equals to "pool", then the resources
356
    are allocated from the corresponding Pools.
357

358
    """
359
    try:
360
        flavor = Network.FLAVORS[flavor]
361
    except KeyError:
362
        raise faults.BadRequest("Unknown network flavor")
363

    
364
    mode = flavor.get("mode")
365

    
366
    link = flavor.get("link")
367
    if link == "pool":
368
        link = allocate_resource("bridge")
369

    
370
    mac_prefix = flavor.get("mac_prefix")
371
    if mac_prefix == "pool":
372
        mac_prefix = allocate_resource("mac_prefix")
373

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

    
376
    return mode, link, mac_prefix, tags
377

    
378

    
379
def allocate_resource(res_type):
380
    table = get_pool_table(res_type)
381
    pool = table.get_pool()
382
    value = pool.get()
383
    pool.save()
384
    return value
385

    
386

    
387
def release_resource(res_type, value):
388
    table = get_pool_table(res_type)
389
    pool = table.get_pool()
390
    pool.put(value)
391
    pool.save()
392

    
393

    
394
def get_pool_table(res_type):
395
    if res_type == "bridge":
396
        return BridgePoolTable
397
    elif res_type == "mac_prefix":
398
        return MacPrefixPoolTable
399
    else:
400
        raise Exception("Unknown resource type")
401

    
402

    
403
def get_existing_users():
404
    """
405
    Retrieve user ids stored in cyclades user agnostic models.
406
    """
407
    # also check PublicKeys a user with no servers/networks exist
408
    from synnefo.userdata.models import PublicKeyPair
409
    from synnefo.db.models import VirtualMachine, Network
410

    
411
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
412
                                                                  flat=True)
413
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
414
                                                                  flat=True)
415
    networkusernames = Network.objects.filter().values_list('userid',
416
                                                            flat=True)
417

    
418
    return set(list(keypairusernames) + list(serverusernames) +
419
               list(networkusernames))
420

    
421

    
422
def vm_to_links(vm_id):
423
    href = join_urls(SERVERS_URL, str(vm_id))
424
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
425

    
426

    
427
def network_to_links(network_id):
428
    href = join_urls(NETWORKS_URL, str(network_id))
429
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
430

    
431

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

    
436

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

    
441

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

    
446

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

    
454

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