Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.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
import datetime
35
import ipaddr
36

    
37
from base64 import b64encode, b64decode
38
from datetime import timedelta, tzinfo
39
from functools import wraps
40
from hashlib import sha256
41
from logging import getLogger
42
from random import choice
43
from string import digits, lowercase, uppercase
44
from time import time
45
from traceback import format_exc
46
from wsgiref.handlers import format_date_time
47

    
48
import dateutil.parser
49

    
50
from Crypto.Cipher import AES
51

    
52
from django.conf import settings
53
from django.http import HttpResponse
54
from django.template.loader import render_to_string
55
from django.utils import simplejson as json
56
from django.utils.cache import add_never_cache_headers
57
from django.db.models import Q
58

    
59
from snf_django.lib.api import faults
60
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
61
                               Network, BackendNetwork, NetworkInterface,
62
                               BridgePoolTable, MacPrefixPoolTable, Backend)
63
from synnefo.db.pools import EmptyPool
64

    
65
from snf_django.lib.astakos import get_user
66
from synnefo.plankton.utils import image_backend
67
from synnefo.settings import MAX_CIDR_BLOCK
68

    
69

    
70
log = getLogger('synnefo.api')
71

    
72

    
73
def random_password():
74
    """Generates a random password
75

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

    
80
    pool = lowercase + uppercase + digits
81
    lowerset = set(lowercase)
82
    upperset = set(uppercase)
83
    digitset = set(digits)
84
    length = 10
85

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

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

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

    
100
    return password
101

    
102

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

    
107
    npad = 16 - len(s) % 16
108
    return s + '\x00' * npad
109

    
110

    
111
def encrypt(plaintext):
112
    # Make sure key is 32 bytes long
113
    key = sha256(settings.SECRET_KEY).digest()
114

    
115
    aes = AES.new(key)
116
    enc = aes.encrypt(zeropad(plaintext))
117
    return b64encode(enc)
118

    
119

    
120
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
121
           non_suspended=False):
122
    """Find a VirtualMachine instance based on ID and owner."""
123

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

    
140

    
141
def get_vm_meta(vm, key):
142
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
143

    
144
    try:
145
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
146
    except VirtualMachineMetadata.DoesNotExist:
147
        raise faults.ItemNotFound('Metadata key not found.')
148

    
149

    
150
def get_image(image_id, user_id):
151
    """Return an Image instance or raise ItemNotFound."""
152

    
153
    with image_backend(user_id) as backend:
154
        return backend.get_image(image_id)
155

    
156

    
157
def get_image_dict(image_id, user_id):
158
    image = {}
159
    img = get_image(image_id, user_id)
160
    properties = img.get('properties', {})
161
    image["id"] = img["id"]
162
    image["name"] = img["name"]
163
    image['backend_id'] = img['location']
164
    image['format'] = img['disk_format']
165
    image['metadata'] = dict((key.upper(), val)
166
                             for key, val in properties.items())
167
    image['checksum'] = img['checksum']
168

    
169
    return image
170

    
171

    
172
def get_flavor(flavor_id, include_deleted=False):
173
    """Return a Flavor instance or raise ItemNotFound."""
174

    
175
    try:
176
        flavor_id = int(flavor_id)
177
        if include_deleted:
178
            return Flavor.objects.get(id=flavor_id)
179
        else:
180
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
181
    except (ValueError, Flavor.DoesNotExist):
182
        raise faults.ItemNotFound('Flavor not found.')
183

    
184

    
185
def get_flavor_provider(flavor):
186
    """Extract provider from disk template.
187

188
    Provider for `ext` disk_template is encoded in the disk template
189
    name, which is formed `ext_<provider_name>`. Provider is None
190
    for all other disk templates.
191

192
    """
193
    disk_template = flavor.disk_template
194
    provider = None
195
    if disk_template.startswith("ext"):
196
        disk_template, provider = disk_template.split("_", 1)
197
    return disk_template, provider
198

    
199

    
200
def get_network(network_id, user_id, for_update=False):
201
    """Return a Network instance or raise ItemNotFound."""
202

    
203
    try:
204
        network_id = int(network_id)
205
        objects = Network.objects
206
        if for_update:
207
            objects = objects.select_for_update()
208
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
209
    except (ValueError, Network.DoesNotExist):
210
        raise faults.ItemNotFound('Network not found.')
211

    
212

    
213
def validate_network_params(subnet, gateway=None, subnet6=None, gateway6=None):
214
    try:
215
        # Use strict option to not all subnets with host bits set
216
        network = ipaddr.IPv4Network(subnet, strict=True)
217
    except ValueError:
218
        raise faults.BadRequest("Invalid network IPv4 subnet")
219

    
220
    # Check that network size is allowed!
221
    if not validate_network_size(network.prefixlen):
222
        raise faults.OverLimit(message="Unsupported network size",
223
                        details="Network mask must be in range (%s, 29]" %
224
                                MAX_CIDR_BLOCK)
225

    
226
    # Check that gateway belongs to network
227
    if gateway:
228
        try:
229
            gateway = ipaddr.IPv4Address(gateway)
230
        except ValueError:
231
            raise faults.BadRequest("Invalid network IPv4 gateway")
232
        if not gateway in network:
233
            raise faults.BadRequest("Invalid network IPv4 gateway")
234

    
235
    if subnet6:
236
        try:
237
            # Use strict option to not all subnets with host bits set
238
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
239
        except ValueError:
240
            raise faults.BadRequest("Invalid network IPv6 subnet")
241
        if gateway6:
242
            try:
243
                gateway6 = ipaddr.IPv6Address(gateway6)
244
            except ValueError:
245
                raise faults.BadRequest("Invalid network IPv6 gateway")
246
            if not gateway6 in network6:
247
                raise faults.BadRequest("Invalid network IPv6 gateway")
248

    
249

    
250
def validate_network_size(cidr_block):
251
    """Return True if network size is allowed."""
252
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
253

    
254

    
255
def allocate_public_address(backend):
256
    """Allocate a public IP for a vm."""
257
    for network in backend_public_networks(backend):
258
        try:
259
            address = get_network_free_address(network)
260
            return (network, address)
261
        except EmptyPool:
262
            pass
263
    return (None, None)
264

    
265

    
266
def get_public_ip(backend):
267
    """Reserve an IP from a public network.
268

269
    This method should run inside a transaction.
270

271
    """
272

    
273
    # Guarantee exclusive access to backend, because accessing the IP pools of
274
    # the backend networks may result in a deadlock with backend allocator
275
    # which also checks that backend networks have a free IP.
276
    backend = Backend.objects.select_for_update().get(id=backend.id)
277

    
278
    address = None
279
    if settings.PUBLIC_USE_POOL:
280
        (network, address) = allocate_public_address(backend)
281
    else:
282
        for net in list(backend_public_networks(backend)):
283
            pool = net.get_pool()
284
            if not pool.empty():
285
                address = 'pool'
286
                network = net
287
                break
288
    if address is None:
289
        log.error("Public networks of backend %s are full", backend)
290
        raise faults.OverLimit("Can not allocate IP for new machine."
291
                        " Public networks are full.")
292
    return (network, address)
293

    
294

    
295
def backend_public_networks(backend):
296
    """Return available public networks of the backend.
297

298
    Iterator for non-deleted public networks that are available
299
    to the specified backend.
300

301
    """
302
    for network in Network.objects.filter(public=True, deleted=False,
303
                                          drained=False):
304
        if BackendNetwork.objects.filter(network=network,
305
                                         backend=backend).exists():
306
            yield network
307

    
308

    
309
def get_network_free_address(network):
310
    """Reserve an IP address from the IP Pool of the network.
311

312
    Raises EmptyPool
313

314
    """
315

    
316
    pool = network.get_pool()
317
    address = pool.get()
318
    pool.save()
319
    return address
320

    
321

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

    
328

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

    
342

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

    
354

    
355
def render_meta(request, meta, status=200):
356
    if request.serialization == 'xml':
357
        data = render_to_string('meta.xml', dict(key=key, val=val))
358
    else:
359
        data = json.dumps(dict(meta=meta))
360
    return HttpResponse(data, status=status)
361

    
362

    
363
def construct_nic_id(nic):
364
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
365

    
366

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

    
388

    
389
def values_from_flavor(flavor):
390
    """Get Ganeti connectivity info from flavor type.
391

392
    If link or mac_prefix equals to "pool", then the resources
393
    are allocated from the corresponding Pools.
394

395
    """
396
    try:
397
        flavor = Network.FLAVORS[flavor]
398
    except KeyError:
399
        raise faults.BadRequest("Unknown network flavor")
400

    
401
    mode = flavor.get("mode")
402

    
403
    link = flavor.get("link")
404
    if link == "pool":
405
        link = allocate_resource("bridge")
406

    
407
    mac_prefix = flavor.get("mac_prefix")
408
    if mac_prefix == "pool":
409
        mac_prefix = allocate_resource("mac_prefix")
410

    
411
    tags = flavor.get("tags")
412

    
413
    return mode, link, mac_prefix, tags
414

    
415

    
416
def allocate_resource(res_type):
417
    table = get_pool_table(res_type)
418
    pool = table.get_pool()
419
    value = pool.get()
420
    pool.save()
421
    return value
422

    
423

    
424
def release_resource(res_type, value):
425
    table = get_pool_table(res_type)
426
    pool = table.get_pool()
427
    pool.put(value)
428
    pool.save()
429

    
430

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

    
439

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

    
448
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
449
                                                                  flat=True)
450
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
451
                                                                  flat=True)
452
    networkusernames = Network.objects.filter().values_list('userid',
453
                                                            flat=True)
454

    
455
    return set(list(keypairusernames) + list(serverusernames) +
456
               list(networkusernames))