Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.9 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
NETWORKS_URL = join_urls(COMPUTE_URL, "networks/")
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
PITHOSMAP_PREFIX = "pithosmap://"
72

    
73
log = getLogger('synnefo.api')
74

    
75

    
76
def random_password():
77
    """Generates a random password
78

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

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

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

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

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

    
103
    return password
104

    
105

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

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

    
113

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

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

    
122

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

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

    
145

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

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

    
154

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

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

    
161

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

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

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

    
179
    return image
180

    
181

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

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

    
194

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

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

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

    
209

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

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

    
226

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

    
241

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

    
249

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

    
260

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

    
272

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

    
287

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

    
292

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

    
300

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

    
307

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

    
319

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

    
328

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

    
350

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

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

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

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

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

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

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

    
375
    return mode, link, mac_prefix, tags
376

    
377

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

    
385

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

    
392

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

    
401

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

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

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

    
420

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

    
425

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

    
430

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

    
435

    
436
def image_to_links(image_id):
437
    href = join_urls(IMAGES_URL, str(image_id))
438
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
439
    links.append({"rel": "alternate",
440
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
441
    return links
442

    
443

    
444
def start_action(vm, action, jobId):
445
    vm.action = action
446
    vm.backendjobid = jobId
447
    vm.backendopcode = None
448
    vm.backendjobstatus = None
449
    vm.backendlogmsg = None
450
    vm.save()