Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (15 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, BackendNetwork, NetworkInterface,
51
                               BridgePoolTable, MacPrefixPoolTable, Backend,
52
                               IPAddress)
53
from synnefo.db.pools import EmptyPool
54

    
55
from synnefo.plankton.utils import image_backend
56

    
57
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
58
from synnefo.lib.services import get_service_path
59
from synnefo.lib import join_urls
60

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

    
73
PITHOSMAP_PREFIX = "pithosmap://"
74

    
75
log = getLogger('synnefo.api')
76

    
77

    
78
def random_password():
79
    """Generates a random password
80

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

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

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

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

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

    
105
    return password
106

    
107

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

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

    
115

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

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

    
124

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

    
129
    try:
130
        server_id = int(server_id)
131
        servers = VirtualMachine.objects
132
        if for_update:
133
            servers = servers.select_for_update()
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
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 not found.')
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
233
        if for_update:
234
            objects = objects.select_for_update()
235

    
236
        port = objects.get(network__userid=user_id, id=port_id)
237

    
238
        if (port.device_owner != "vm") and for_update:
239
            raise faults.BadRequest('Can not update non vm port')
240

    
241
        return port
242
    except (ValueError, NetworkInterface.DoesNotExist):
243
        raise faults.ItemNotFound('Port not found.')
244

    
245

    
246
def get_floating_ip(user_id, ipv4, for_update=False):
247
    try:
248
        objects = IPAddress.objects
249
        if for_update:
250
            objects = objects.select_for_update()
251
        return objects.get(userid=user_id, ipv4=ipv4, deleted=False)
252
    except IPAddress.DoesNotExist:
253
        raise faults.ItemNotFound("Floating IP does not exist.")
254

    
255

    
256
def allocate_public_address(backend, userid):
257
    """Get a public IP for any available network of a backend."""
258
    # Guarantee exclusive access to backend, because accessing the IP pools of
259
    # the backend networks may result in a deadlock with backend allocator
260
    # which also checks that backend networks have a free IP.
261
    backend = Backend.objects.select_for_update().get(id=backend.id)
262
    public_networks = backend_public_networks(backend)
263
    return get_free_ip(public_networks, userid)
264

    
265

    
266
def backend_public_networks(backend):
267
    """Return available public networks of the backend.
268

269
    Iterator for non-deleted public networks that are available
270
    to the specified backend.
271

272
    """
273
    bnets = BackendNetwork.objects.filter(backend=backend,
274
                                          network__public=True,
275
                                          network__deleted=False,
276
                                          network__floating_ip_pool=False,
277
                                          network__drained=False)
278
    return [b.network for b in bnets]
279

    
280

    
281
def get_free_ip(networks, userid):
282
    for network in networks:
283
        try:
284
            return network.allocate_address(userid=userid)
285
        except faults.OverLimit:
286
            pass
287
    msg = "Can not allocate public IP. Public networks are full."
288
    log.error(msg)
289
    raise faults.OverLimit(msg)
290

    
291

    
292
def get_network_free_address(network, userid):
293
    """Reserve an IP address from the IP Pool of the network."""
294

    
295
    try:
296
        return network.allocate_address(userid=userid)
297
    except EmptyPool:
298
        raise faults.OverLimit("Network %s is full." % network.backend_id)
299

    
300

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

    
308

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

    
315

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

    
327

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

    
336

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

    
358

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

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

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

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

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

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

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

    
383
    return mode, link, mac_prefix, tags
384

    
385

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

    
393

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

    
400

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

    
409

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

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

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

    
428

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

    
433

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

    
438

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

    
443

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

    
451

    
452
def start_action(vm, action, jobId):
453
    vm.action = action
454
    vm.backendjobid = jobId
455
    vm.backendopcode = None
456
    vm.backendjobstatus = None
457
    vm.backendlogmsg = None
458
    vm.save()