Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.1 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

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

    
47
import dateutil.parser
48

    
49
from Crypto.Cipher import AES
50

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

    
58
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress,
59
                                ItemNotFound, ServiceUnavailable, Unauthorized,
60
                                BadMediaType, Forbidden)
61
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
62
                               Network, BackendNetwork, NetworkInterface,
63
                               BridgePoolTable, MacPrefixPoolTable)
64
from synnefo.db.pools import EmptyPool
65

    
66
from synnefo.lib.astakos import get_user
67
from synnefo.plankton.backend import ImageBackend, NotAllowedError
68
from synnefo.settings import MAX_CIDR_BLOCK
69

    
70

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

    
73

    
74
class UTC(tzinfo):
75
    def utcoffset(self, dt):
76
        return timedelta(0)
77

    
78
    def tzname(self, dt):
79
        return 'UTC'
80

    
81
    def dst(self, dt):
82
        return timedelta(0)
83

    
84

    
85
def isoformat(d):
86
    """Return an ISO8601 date string that includes a timezone."""
87

    
88
    return d.replace(tzinfo=UTC()).isoformat()
89

    
90

    
91
def isoparse(s):
92
    """Parse an ISO8601 date string into a datetime object."""
93

    
94
    if not s:
95
        return None
96

    
97
    try:
98
        since = dateutil.parser.parse(s)
99
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
100
    except ValueError:
101
        raise BadRequest('Invalid changes-since parameter.')
102

    
103
    now = datetime.datetime.now()
104
    if utc_since > now:
105
        raise BadRequest('changes-since value set in the future.')
106

    
107
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
108
        raise BadRequest('Too old changes-since value.')
109

    
110
    return utc_since
111

    
112

    
113
def random_password():
114
    """Generates a random password
115

116
    We generate a windows compliant password: it must contain at least
117
    one charachter from each of the groups: upper case, lower case, digits.
118
    """
119

    
120
    pool = lowercase + uppercase + digits
121
    lowerset = set(lowercase)
122
    upperset = set(uppercase)
123
    digitset = set(digits)
124
    length = 10
125

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

    
128
    # Make sure the password is compliant
129
    chars = set(password)
130
    if not chars & lowerset:
131
        password += choice(lowercase)
132
    if not chars & upperset:
133
        password += choice(uppercase)
134
    if not chars & digitset:
135
        password += choice(digits)
136

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

    
140
    return password
141

    
142

    
143
def zeropad(s):
144
    """Add zeros at the end of a string in order to make its length
145
       a multiple of 16."""
146

    
147
    npad = 16 - len(s) % 16
148
    return s + '\x00' * npad
149

    
150

    
151
def encrypt(plaintext):
152
    # Make sure key is 32 bytes long
153
    key = sha256(settings.SECRET_KEY).digest()
154

    
155
    aes = AES.new(key)
156
    enc = aes.encrypt(zeropad(plaintext))
157
    return b64encode(enc)
158

    
159

    
160
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
161
           non_suspended=False):
162
    """Find a VirtualMachine instance based on ID and owner."""
163

    
164
    try:
165
        server_id = int(server_id)
166
        servers = VirtualMachine.objects
167
        if for_update:
168
            servers = servers.select_for_update()
169
        vm = servers.get(id=server_id, userid=user_id)
170
        if non_deleted and vm.deleted:
171
            raise VirtualMachine.DeletedError
172
        if non_suspended and vm.suspended:
173
            raise Forbidden("Administratively Suspended VM")
174
        return vm
175
    except ValueError:
176
        raise BadRequest('Invalid server ID.')
177
    except VirtualMachine.DoesNotExist:
178
        raise ItemNotFound('Server not found.')
179

    
180

    
181
def get_vm_meta(vm, key):
182
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
183

    
184
    try:
185
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
186
    except VirtualMachineMetadata.DoesNotExist:
187
        raise ItemNotFound('Metadata key not found.')
188

    
189

    
190
def get_image(image_id, user_id):
191
    """Return an Image instance or raise ItemNotFound."""
192

    
193
    backend = ImageBackend(user_id)
194
    try:
195
        image = backend.get_image(image_id)
196
        if not image:
197
            raise ItemNotFound('Image not found.')
198
        return image
199
    finally:
200
        backend.close()
201

    
202

    
203
def get_flavor(flavor_id, include_deleted=False):
204
    """Return a Flavor instance or raise ItemNotFound."""
205

    
206
    try:
207
        flavor_id = int(flavor_id)
208
        if include_deleted:
209
            return Flavor.objects.get(id=flavor_id)
210
        else:
211
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
212
    except (ValueError, Flavor.DoesNotExist):
213
        raise ItemNotFound('Flavor not found.')
214

    
215

    
216
def get_network(network_id, user_id, for_update=False):
217
    """Return a Network instance or raise ItemNotFound."""
218

    
219
    try:
220
        network_id = int(network_id)
221
        objects = Network.objects
222
        if for_update:
223
            objects = objects.select_for_update()
224
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
225
    except (ValueError, Network.DoesNotExist):
226
        raise ItemNotFound('Network not found.')
227

    
228

    
229
def validate_network_size(cidr_block):
230
    """Return True if network size is allowed."""
231
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
232

    
233

    
234
def allocate_public_address(backend):
235
    """Allocate a public IP for a vm."""
236
    for network in backend_public_networks(backend):
237
        try:
238
            address = get_network_free_address(network)
239
            return (network, address)
240
        except EmptyPool:
241
            pass
242
    return (None, None)
243

    
244

    
245
def backend_public_networks(backend):
246
    """Return available public networks of the backend.
247

248
    Iterator for non-deleted public networks that are available
249
    to the specified backend.
250

251
    """
252
    for network in Network.objects.filter(public=True, deleted=False):
253
        if BackendNetwork.objects.filter(network=network,
254
                                         backend=backend).exists():
255
            yield network
256

    
257

    
258
def get_network_free_address(network):
259
    """Reserve an IP address from the IP Pool of the network.
260

261
    Raises EmptyPool
262

263
    """
264

    
265
    pool = network.get_pool()
266
    address = pool.get()
267
    pool.save()
268
    return address
269

    
270

    
271
def get_nic(machine, network):
272
    try:
273
        return NetworkInterface.objects.get(machine=machine, network=network)
274
    except NetworkInterface.DoesNotExist:
275
        raise ItemNotFound('Server not connected to this network.')
276

    
277

    
278
def get_nic_from_index(vm, nic_index):
279
    """Returns the nic_index-th nic of a vm
280
       Error Response Codes: itemNotFound (404), badMediaType (415)
281
    """
282
    matching_nics = vm.nics.filter(index=nic_index)
283
    matching_nics_len = len(matching_nics)
284
    if matching_nics_len < 1:
285
        raise  ItemNotFound('NIC not found on VM')
286
    elif matching_nics_len > 1:
287
        raise BadMediaType('NIC index conflict on VM')
288
    nic = matching_nics[0]
289
    return nic
290

    
291

    
292
def get_request_dict(request):
293
    """Returns data sent by the client as a python dict."""
294

    
295
    data = request.raw_post_data
296
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
297
        try:
298
            return json.loads(data)
299
        except ValueError:
300
            raise BadRequest('Invalid JSON data.')
301
    else:
302
        raise BadRequest('Unsupported Content-Type.')
303

    
304

    
305
def update_response_headers(request, response):
306
    if request.serialization == 'xml':
307
        response['Content-Type'] = 'application/xml'
308
    elif request.serialization == 'atom':
309
        response['Content-Type'] = 'application/atom+xml'
310
    else:
311
        response['Content-Type'] = 'application/json'
312

    
313
    if settings.TEST:
314
        response['Date'] = format_date_time(time())
315

    
316
    add_never_cache_headers(response)
317

    
318

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

    
330

    
331
def render_meta(request, meta, status=200):
332
    if request.serialization == 'xml':
333
        data = render_to_string('meta.xml', dict(key=key, val=val))
334
    else:
335
        data = json.dumps(dict(meta=meta))
336
    return HttpResponse(data, status=status)
337

    
338

    
339
def render_fault(request, fault):
340
    if settings.DEBUG or settings.TEST:
341
        fault.details = format_exc(fault)
342

    
343
    if request.serialization == 'xml':
344
        data = render_to_string('fault.xml', {'fault': fault})
345
    else:
346
        d = {fault.name: {
347
                'code': fault.code,
348
                'message': fault.message,
349
                'details': fault.details}}
350
        data = json.dumps(d)
351

    
352
    resp = HttpResponse(data, status=fault.code)
353
    update_response_headers(request, resp)
354
    return resp
355

    
356

    
357
def request_serialization(request, atom_allowed=False):
358
    """Return the serialization format requested.
359

360
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
361
    """
362

    
363
    path = request.path
364

    
365
    if path.endswith('.json'):
366
        return 'json'
367
    elif path.endswith('.xml'):
368
        return 'xml'
369
    elif atom_allowed and path.endswith('.atom'):
370
        return 'atom'
371

    
372
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
373
        accept, sep, rest = item.strip().partition(';')
374
        if accept == 'application/json':
375
            return 'json'
376
        elif accept == 'application/xml':
377
            return 'xml'
378
        elif atom_allowed and accept == 'application/atom+xml':
379
            return 'atom'
380

    
381
    return 'json'
382

    
383

    
384
def api_method(http_method=None, atom_allowed=False):
385
    """Decorator function for views that implement an API method."""
386

    
387
    def decorator(func):
388
        @wraps(func)
389
        def wrapper(request, *args, **kwargs):
390
            try:
391
                request.serialization = request_serialization(request,
392
                                                              atom_allowed)
393
                get_user(request, settings.ASTAKOS_URL)
394
                if not request.user_uniq:
395
                    raise Unauthorized('No user found.')
396
                if http_method and request.method != http_method:
397
                    raise BadRequest('Method not allowed.')
398

    
399
                resp = func(request, *args, **kwargs)
400
                update_response_headers(request, resp)
401
                return resp
402
            except VirtualMachine.DeletedError:
403
                fault = BadRequest('Server has been deleted.')
404
                return render_fault(request, fault)
405
            except Network.DeletedError:
406
                fault = BadRequest('Network has been deleted.')
407
                return render_fault(request, fault)
408
            except VirtualMachine.BuildingError:
409
                fault = BuildInProgress('Server is being built.')
410
                return render_fault(request, fault)
411
            except NotAllowedError:
412
                # Image Backend Unathorized
413
                fault = Forbidden('Request not allowed.')
414
                return render_fault(request, fault)
415
            except Fault, fault:
416
                if fault.code >= 500:
417
                    log.exception('API fault')
418
                return render_fault(request, fault)
419
            except BaseException:
420
                log.exception('Unexpected error')
421
                fault = ServiceUnavailable('Unexpected error.')
422
                return render_fault(request, fault)
423
        return wrapper
424
    return decorator
425

    
426

    
427
def construct_nic_id(nic):
428
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
429

    
430

    
431
def net_resources(net_type):
432
    mac_prefix = settings.MAC_POOL_BASE
433
    if net_type == 'PRIVATE_MAC_FILTERED':
434
        link = settings.PRIVATE_MAC_FILTERED_BRIDGE
435
        mac_pool = MacPrefixPoolTable.get_pool()
436
        mac_prefix = mac_pool.get()
437
        mac_pool.save()
438
    elif net_type == 'PRIVATE_PHYSICAL_VLAN':
439
        pool = BridgePoolTable.get_pool()
440
        link = pool.get()
441
        pool.save()
442
    elif net_type == 'CUSTOM_ROUTED':
443
        link = settings.CUSTOM_ROUTED_ROUTING_TABLE
444
    elif net_type == 'CUSTOM_BRIDGED':
445
        link = settings.CUSTOM_BRIDGED_BRIDGE
446
    elif net_type == 'PUBLIC_ROUTED':
447
        link = settings.PUBLIC_ROUTED_ROUTING_TABLE
448
    else:
449
        raise BadRequest('Unknown network type')
450

    
451
    return link, mac_prefix