Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (16.2 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
import ipaddr
35

    
36
from base64 import b64encode, b64decode
37
from hashlib import sha256
38
from logging import getLogger
39
from random import choice
40
from string import digits, lowercase, uppercase
41

    
42
from Crypto.Cipher import AES
43

    
44
from django.conf import settings
45
from django.http import HttpResponse
46
from django.template.loader import render_to_string
47
from django.utils import simplejson as json
48
from django.db.models import Q
49

    
50
from snf_django.lib.api import faults
51
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
52
                               Network, BackendNetwork, NetworkInterface,
53
                               BridgePoolTable, MacPrefixPoolTable, Backend,
54
                               FloatingIP)
55
from synnefo.db.pools import EmptyPool
56

    
57
from synnefo.plankton.utils import image_backend
58
from synnefo.settings import MAX_CIDR_BLOCK
59

    
60
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
61
from synnefo.lib.services import get_service_path
62
from synnefo.lib import join_urls
63

    
64
COMPUTE_URL = \
65
    join_urls(BASE_HOST,
66
              get_service_path(cyclades_services, "compute", version="2"))
67
SERVERS_URL = join_urls(COMPUTE_URL, "servers/")
68
NETWORKS_URL = join_urls(COMPUTE_URL, "networks/")
69
FLAVORS_URL = join_urls(COMPUTE_URL, "flavors/")
70
IMAGES_URL = join_urls(COMPUTE_URL, "images/")
71
PLANKTON_URL = \
72
    join_urls(BASE_HOST,
73
              get_service_path(cyclades_services, "image", version="1.0"))
74
IMAGES_PLANKTON_URL = join_urls(PLANKTON_URL, "images/")
75

    
76

    
77
log = getLogger('synnefo.api')
78

    
79

    
80
def random_password():
81
    """Generates a random password
82

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

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

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

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

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

    
107
    return password
108

    
109

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

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

    
117

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

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

    
126

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

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

    
147

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

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

    
156

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

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

    
163

    
164
def get_image_dict(image_id, user_id):
165
    image = {}
166
    img = get_image(image_id, user_id)
167
    properties = img.get('properties', {})
168
    image["id"] = img["id"]
169
    image["name"] = img["name"]
170
    image['backend_id'] = img['location']
171
    image['format'] = img['disk_format']
172
    image['metadata'] = dict((key.upper(), val)
173
                             for key, val in properties.items())
174
    image['checksum'] = img['checksum']
175

    
176
    return image
177

    
178

    
179
def get_flavor(flavor_id, include_deleted=False):
180
    """Return a Flavor instance or raise ItemNotFound."""
181

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

    
191

    
192
def get_flavor_provider(flavor):
193
    """Extract provider from disk template.
194

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

199
    """
200
    disk_template = flavor.disk_template
201
    provider = None
202
    if disk_template.startswith("ext"):
203
        disk_template, provider = disk_template.split("_", 1)
204
    return disk_template, provider
205

    
206

    
207
def get_network(network_id, user_id, for_update=False, non_deleted=False):
208
    """Return a Network instance or raise ItemNotFound."""
209

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

    
223

    
224
def get_floating_ip(user_id, ipv4, for_update=False):
225
    try:
226
        objects = FloatingIP.objects
227
        if for_update:
228
            objects = objects.select_for_update()
229
        return objects.get(userid=user_id, ipv4=ipv4, deleted=False)
230
    except FloatingIP.DoesNotExist:
231
        raise faults.ItemNotFound("Floating IP does not exist.")
232

    
233

    
234
def validate_network_params(subnet, gateway=None, subnet6=None, gateway6=None):
235
    try:
236
        # Use strict option to not all subnets with host bits set
237
        network = ipaddr.IPv4Network(subnet, strict=True)
238
    except ValueError:
239
        raise faults.BadRequest("Invalid network IPv4 subnet")
240

    
241
    # Check that network size is allowed!
242
    if not validate_network_size(network.prefixlen):
243
        raise faults.OverLimit(message="Unsupported network size",
244
                               details="Network mask must be in range"
245
                                       " (%s, 29]" % MAX_CIDR_BLOCK)
246

    
247
    # Check that gateway belongs to network
248
    if gateway:
249
        try:
250
            gateway = ipaddr.IPv4Address(gateway)
251
        except ValueError:
252
            raise faults.BadRequest("Invalid network IPv4 gateway")
253
        if not gateway in network:
254
            raise faults.BadRequest("Invalid network IPv4 gateway")
255

    
256
    if subnet6:
257
        try:
258
            # Use strict option to not all subnets with host bits set
259
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
260
        except ValueError:
261
            raise faults.BadRequest("Invalid network IPv6 subnet")
262
        if gateway6:
263
            try:
264
                gateway6 = ipaddr.IPv6Address(gateway6)
265
            except ValueError:
266
                raise faults.BadRequest("Invalid network IPv6 gateway")
267
            if not gateway6 in network6:
268
                raise faults.BadRequest("Invalid network IPv6 gateway")
269

    
270

    
271
def validate_network_size(cidr_block):
272
    """Return True if network size is allowed."""
273
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
274

    
275

    
276
def allocate_public_address(backend):
277
    """Get a public IP for any available network of a backend."""
278
    # Guarantee exclusive access to backend, because accessing the IP pools of
279
    # the backend networks may result in a deadlock with backend allocator
280
    # which also checks that backend networks have a free IP.
281
    backend = Backend.objects.select_for_update().get(id=backend.id)
282
    public_networks = backend_public_networks(backend)
283
    return get_free_ip(public_networks)
284

    
285

    
286
def backend_public_networks(backend):
287
    """Return available public networks of the backend.
288

289
    Iterator for non-deleted public networks that are available
290
    to the specified backend.
291

292
    """
293
    bnets = BackendNetwork.objects.filter(backend=backend,
294
                                          network__public=True,
295
                                          network__deleted=False,
296
                                          network__drained=False)
297
    return [b.network for b in bnets]
298

    
299

    
300
def get_free_ip(networks):
301
    for network in networks:
302
        try:
303
            address = get_network_free_address(network)
304
            return network, address
305
        except faults.OverLimit:
306
            pass
307
    msg = "Can not allocate public IP. Public networks are full."
308
    log.error(msg)
309
    raise faults.OverLimit(msg)
310

    
311

    
312
def get_network_free_address(network):
313
    """Reserve an IP address from the IP Pool of the network."""
314

    
315
    pool = network.get_pool()
316
    try:
317
        address = pool.get()
318
    except EmptyPool:
319
        raise faults.OverLimit("Network %s is full." % network.backend_id)
320
    pool.save()
321
    return address
322

    
323

    
324
def get_nic(machine, network):
325
    try:
326
        return NetworkInterface.objects.get(machine=machine, network=network)
327
    except NetworkInterface.DoesNotExist:
328
        raise faults.ItemNotFound('Server not connected to this network.')
329

    
330

    
331
def get_nic_from_index(vm, nic_index):
332
    """Returns the nic_index-th nic of a vm
333
       Error Response Codes: itemNotFound (404), badMediaType (415)
334
    """
335
    matching_nics = vm.nics.filter(index=nic_index)
336
    matching_nics_len = len(matching_nics)
337
    if matching_nics_len < 1:
338
        raise faults.ItemNotFound('NIC not found on VM')
339
    elif matching_nics_len > 1:
340
        raise faults.BadMediaType('NIC index conflict on VM')
341
    nic = matching_nics[0]
342
    return nic
343

    
344

    
345
def render_metadata(request, metadata, use_values=False, status=200):
346
    if request.serialization == 'xml':
347
        data = render_to_string('metadata.xml', {'metadata': metadata})
348
    else:
349
        if use_values:
350
            d = {'metadata': {'values': metadata}}
351
        else:
352
            d = {'metadata': metadata}
353
        data = json.dumps(d)
354
    return HttpResponse(data, status=status)
355

    
356

    
357
def render_meta(request, meta, status=200):
358
    if request.serialization == 'xml':
359
        key, val = meta.items()[0]
360
        data = render_to_string('meta.xml', dict(key=key, val=val))
361
    else:
362
        data = json.dumps(dict(meta=meta))
363
    return HttpResponse(data, status=status)
364

    
365

    
366
def construct_nic_id(nic):
367
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
368

    
369

    
370
def verify_personality(personality):
371
    """Verify that a a list of personalities is well formed"""
372
    if len(personality) > settings.MAX_PERSONALITY:
373
        raise faults.OverLimit("Maximum number of personalities"
374
                               " exceeded")
375
    for p in personality:
376
        # Verify that personalities are well-formed
377
        try:
378
            assert isinstance(p, dict)
379
            keys = set(p.keys())
380
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
381
            assert keys.issubset(allowed)
382
            contents = p['contents']
383
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
384
                # No need to decode if contents already exceed limit
385
                raise faults.OverLimit("Maximum size of personality exceeded")
386
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
387
                raise faults.OverLimit("Maximum size of personality exceeded")
388
        except AssertionError:
389
            raise faults.BadRequest("Malformed personality in request")
390

    
391

    
392
def values_from_flavor(flavor):
393
    """Get Ganeti connectivity info from flavor type.
394

395
    If link or mac_prefix equals to "pool", then the resources
396
    are allocated from the corresponding Pools.
397

398
    """
399
    try:
400
        flavor = Network.FLAVORS[flavor]
401
    except KeyError:
402
        raise faults.BadRequest("Unknown network flavor")
403

    
404
    mode = flavor.get("mode")
405

    
406
    link = flavor.get("link")
407
    if link == "pool":
408
        link = allocate_resource("bridge")
409

    
410
    mac_prefix = flavor.get("mac_prefix")
411
    if mac_prefix == "pool":
412
        mac_prefix = allocate_resource("mac_prefix")
413

    
414
    tags = flavor.get("tags")
415

    
416
    return mode, link, mac_prefix, tags
417

    
418

    
419
def allocate_resource(res_type):
420
    table = get_pool_table(res_type)
421
    pool = table.get_pool()
422
    value = pool.get()
423
    pool.save()
424
    return value
425

    
426

    
427
def release_resource(res_type, value):
428
    table = get_pool_table(res_type)
429
    pool = table.get_pool()
430
    pool.put(value)
431
    pool.save()
432

    
433

    
434
def get_pool_table(res_type):
435
    if res_type == "bridge":
436
        return BridgePoolTable
437
    elif res_type == "mac_prefix":
438
        return MacPrefixPoolTable
439
    else:
440
        raise Exception("Unknown resource type")
441

    
442

    
443
def get_existing_users():
444
    """
445
    Retrieve user ids stored in cyclades user agnostic models.
446
    """
447
    # also check PublicKeys a user with no servers/networks exist
448
    from synnefo.userdata.models import PublicKeyPair
449
    from synnefo.db.models import VirtualMachine, Network
450

    
451
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
452
                                                                  flat=True)
453
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
454
                                                                  flat=True)
455
    networkusernames = Network.objects.filter().values_list('userid',
456
                                                            flat=True)
457

    
458
    return set(list(keypairusernames) + list(serverusernames) +
459
               list(networkusernames))
460

    
461

    
462
def vm_to_links(vm_id):
463
    href = join_urls(SERVERS_URL, str(vm_id))
464
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
465

    
466

    
467
def network_to_links(network_id):
468
    href = join_urls(NETWORKS_URL, str(network_id))
469
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
470

    
471

    
472
def flavor_to_links(flavor_id):
473
    href = join_urls(FLAVORS_URL, str(flavor_id))
474
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
475

    
476

    
477
def image_to_links(image_id):
478
    href = join_urls(IMAGES_URL, str(image_id))
479
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
480
    links.append({"rel": "alternate",
481
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
482
    return links
483

    
484

    
485
def start_action(vm, action, jobId):
486
    vm.action = action
487
    vm.backendjobid = jobId
488
    vm.backendopcode = None
489
    vm.backendjobstatus = None
490
    vm.backendlogmsg = None
491
    vm.save()