Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (13.4 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)
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
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):
161
    """Return a VirtualMachine instance or raise ItemNotFound."""
162

    
163
    try:
164
        server_id = int(server_id)
165
        return VirtualMachine.objects.get(id=server_id, userid=user_id)
166
    except ValueError:
167
        raise BadRequest('Invalid server ID.')
168
    except VirtualMachine.DoesNotExist:
169
        raise ItemNotFound('Server not found.')
170

    
171

    
172
def get_vm_meta(vm, key):
173
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
174

    
175
    try:
176
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
177
    except VirtualMachineMetadata.DoesNotExist:
178
        raise ItemNotFound('Metadata key not found.')
179

    
180

    
181
def get_image(image_id, user_id):
182
    """Return an Image instance or raise ItemNotFound."""
183

    
184
    backend = ImageBackend(user_id)
185
    try:
186
        image = backend.get_image(image_id)
187
        if not image:
188
            raise ItemNotFound('Image not found.')
189
        return image
190
    finally:
191
        backend.close()
192

    
193

    
194
def get_flavor(flavor_id, include_deleted=False):
195
    """Return a Flavor instance or raise ItemNotFound."""
196

    
197
    try:
198
        flavor_id = int(flavor_id)
199
        if include_deleted:
200
            return Flavor.objects.get(id=flavor_id)
201
        else:
202
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
203
    except (ValueError, Flavor.DoesNotExist):
204
        raise ItemNotFound('Flavor not found.')
205

    
206

    
207
def get_network(network_id, user_id, for_update=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
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
216
    except (ValueError, Network.DoesNotExist):
217
        raise ItemNotFound('Network not found.')
218

    
219

    
220
def validate_network_size(cidr_block):
221
    """Return True if network size is allowed."""
222
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
223

    
224

    
225
def allocate_public_address(backend):
226
    """Allocate a public IP for a vm."""
227
    for network in backend_public_networks(backend):
228
        try:
229
            address = get_network_free_address(network)
230
            return (network, address)
231
        except EmptyPool:
232
            pass
233
    return (None, None)
234

    
235

    
236
def backend_public_networks(backend):
237
    """Return available public networks of the backend.
238

239
    Iterator for non-deleted public networks that are available
240
    to the specified backend.
241

242
    """
243
    for network in Network.objects.filter(public=True, deleted=False):
244
        if BackendNetwork.objects.filter(network=network,
245
                                         backend=backend).exists():
246
            yield network
247

    
248

    
249
def get_network_free_address(network):
250
    """Reserve an IP address from the IP Pool of the network.
251

252
    Raises EmptyPool
253

254
    """
255

    
256
    pool = network.get_pool()
257
    address = pool.get()
258
    pool.save()
259
    return address
260

    
261

    
262
def get_nic(machine, network):
263
    try:
264
        return NetworkInterface.objects.get(machine=machine, network=network)
265
    except NetworkInterface.DoesNotExist:
266
        raise ItemNotFound('Server not connected to this network.')
267

    
268

    
269
def get_nic_from_index(vm, nic_index):
270
    """Returns the nic_index-th nic of a vm
271
       Error Response Codes: itemNotFound (404), badMediaType (415)
272
    """
273
    matching_nics = vm.nics.filter(index=nic_index)
274
    matching_nics_len = len(matching_nics)
275
    if matching_nics_len < 1:
276
        raise  ItemNotFound('NIC not found on VM')
277
    elif matching_nics_len > 1:
278
        raise BadMediaType('NIC index conflict on VM')
279
    nic = matching_nics[0]
280
    return nic
281

    
282

    
283
def get_request_dict(request):
284
    """Returns data sent by the client as a python dict."""
285

    
286
    data = request.raw_post_data
287
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
288
        try:
289
            return json.loads(data)
290
        except ValueError:
291
            raise BadRequest('Invalid JSON data.')
292
    else:
293
        raise BadRequest('Unsupported Content-Type.')
294

    
295

    
296
def update_response_headers(request, response):
297
    if request.serialization == 'xml':
298
        response['Content-Type'] = 'application/xml'
299
    elif request.serialization == 'atom':
300
        response['Content-Type'] = 'application/atom+xml'
301
    else:
302
        response['Content-Type'] = 'application/json'
303

    
304
    if settings.TEST:
305
        response['Date'] = format_date_time(time())
306

    
307
    add_never_cache_headers(response)
308

    
309

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

    
321

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

    
329

    
330
def render_fault(request, fault):
331
    if settings.DEBUG or settings.TEST:
332
        fault.details = format_exc(fault)
333

    
334
    if request.serialization == 'xml':
335
        data = render_to_string('fault.xml', {'fault': fault})
336
    else:
337
        d = {fault.name: {
338
                'code': fault.code,
339
                'message': fault.message,
340
                'details': fault.details}}
341
        data = json.dumps(d)
342

    
343
    resp = HttpResponse(data, status=fault.code)
344
    update_response_headers(request, resp)
345
    return resp
346

    
347

    
348
def request_serialization(request, atom_allowed=False):
349
    """Return the serialization format requested.
350

351
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
352
    """
353

    
354
    path = request.path
355

    
356
    if path.endswith('.json'):
357
        return 'json'
358
    elif path.endswith('.xml'):
359
        return 'xml'
360
    elif atom_allowed and path.endswith('.atom'):
361
        return 'atom'
362

    
363
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
364
        accept, sep, rest = item.strip().partition(';')
365
        if accept == 'application/json':
366
            return 'json'
367
        elif accept == 'application/xml':
368
            return 'xml'
369
        elif atom_allowed and accept == 'application/atom+xml':
370
            return 'atom'
371

    
372
    return 'json'
373

    
374

    
375
def api_method(http_method=None, atom_allowed=False):
376
    """Decorator function for views that implement an API method."""
377

    
378
    def decorator(func):
379
        @wraps(func)
380
        def wrapper(request, *args, **kwargs):
381
            try:
382
                request.serialization = request_serialization(request,
383
                                                              atom_allowed)
384
                get_user(request, settings.ASTAKOS_URL)
385
                if not request.user_uniq:
386
                    raise Unauthorized('No user found.')
387
                if http_method and request.method != http_method:
388
                    raise BadRequest('Method not allowed.')
389

    
390
                resp = func(request, *args, **kwargs)
391
                update_response_headers(request, resp)
392
                return resp
393
            except VirtualMachine.DeletedError:
394
                fault = BadRequest('Server has been deleted.')
395
                return render_fault(request, fault)
396
            except VirtualMachine.BuildingError:
397
                fault = BuildInProgress('Server is being built.')
398
                return render_fault(request, fault)
399
            except Fault, fault:
400
                if fault.code >= 500:
401
                    log.exception('API fault')
402
                return render_fault(request, fault)
403
            except BaseException:
404
                log.exception('Unexpected error')
405
                fault = ServiceUnavailable('Unexpected error.')
406
                return render_fault(request, fault)
407
        return wrapper
408
    return decorator
409

    
410

    
411
def construct_nic_id(nic):
412
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
413

    
414

    
415
def net_resources(net_type):
416
    mac_prefix = settings.MAC_POOL_BASE
417
    if net_type == 'PRIVATE_MAC_FILTERED':
418
        link = settings.PRIVATE_MAC_FILTERED_BRIDGE
419
        mac_pool = MacPrefixPoolTable.get_pool()
420
        mac_prefix = mac_pool.get()
421
        mac_pool.save()
422
    elif net_type == 'PRIVATE_PHYSICAL_VLAN':
423
        pool = BridgePoolTable.get_pool()
424
        link = pool.get()
425
        pool.save()
426
    elif net_type == 'CUSTOM_ROUTED':
427
        link = settings.CUSTOM_ROUTED_ROUTING_TABLE
428
    elif net_type == 'CUSTOM_BRIDGED':
429
        link = settings.CUSTOM_BRIDGED_BRIDGE
430
    elif net_type == 'PUBLIC_ROUTED':
431
        link = settings.PUBLIC_ROUTED_ROUTING_TABLE
432
    else:
433
        raise BadRequest('Unknown network type')
434

    
435
    return link, mac_prefix