Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (11.5 kB)

1
# Copyright 2011 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 dateutil.parser
36

    
37
from base64 import b64encode
38
from datetime import timedelta, tzinfo
39
from functools import wraps
40
from hashlib import sha256
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
from Crypto.Cipher import AES
48

    
49
from django.conf import settings
50
from django.http import HttpResponse
51
from django.template.loader import render_to_string
52
from django.utils import simplejson as json
53
from django.utils.cache import add_never_cache_headers
54

    
55
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress,
56
                                ItemNotFound, ServiceUnavailable, Unauthorized)
57
from synnefo.db.models import (Flavor, Image, ImageMetadata,
58
                                VirtualMachine, VirtualMachineMetadata,
59
                                Network, NetworkInterface)
60
from synnefo.plankton.backend import ImageBackend
61
from synnefo.util.log import getLogger
62

    
63

    
64
log = getLogger('synnefo.api')
65

    
66

    
67
class UTC(tzinfo):
68
    def utcoffset(self, dt):
69
        return timedelta(0)
70

    
71
    def tzname(self, dt):
72
        return 'UTC'
73

    
74
    def dst(self, dt):
75
        return timedelta(0)
76

    
77

    
78
def isoformat(d):
79
    """Return an ISO8601 date string that includes a timezone."""
80

    
81
    return d.replace(tzinfo=UTC()).isoformat()
82

    
83

    
84
def isoparse(s):
85
    """Parse an ISO8601 date string into a datetime object."""
86

    
87
    if not s:
88
        return None
89

    
90
    try:
91
        since = dateutil.parser.parse(s)
92
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
93
    except ValueError:
94
        raise BadRequest('Invalid changes-since parameter.')
95

    
96
    now = datetime.datetime.now()
97
    if utc_since > now:
98
        raise BadRequest('changes-since value set in the future.')
99

    
100
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
101
        raise BadRequest('Too old changes-since value.')
102

    
103
    return utc_since
104

    
105

    
106
def random_password():
107
    """Generates a random password
108
    
109
    We generate a windows compliant password: it must contain at least
110
    one charachter from each of the groups: upper case, lower case, digits.
111
    """
112
    
113
    pool = lowercase + uppercase + digits
114
    lowerset = set(lowercase)
115
    upperset = set(uppercase)
116
    digitset = set(digits)
117
    length = 10
118
    
119
    password = ''.join(choice(pool) for i in range(length - 2))
120
    
121
    # Make sure the password is compliant
122
    chars = set(password)
123
    if not chars & lowerset:
124
        password += choice(lowercase)
125
    if not chars & upperset:
126
        password += choice(uppercase)
127
    if not chars & digitset:
128
        password += choice(digits)
129
    
130
    # Pad if necessary to reach required length
131
    password += ''.join(choice(pool) for i in range(length - len(password)))
132
    
133
    return password
134

    
135

    
136
def zeropad(s):
137
    """Add zeros at the end of a string in order to make its length
138
       a multiple of 16."""
139

    
140
    npad = 16 - len(s) % 16
141
    return s + '\x00' * npad
142

    
143

    
144
def encrypt(plaintext):
145
    # Make sure key is 32 bytes long
146
    key = sha256(settings.SECRET_KEY).digest()
147

    
148
    aes = AES.new(key)
149
    enc = aes.encrypt(zeropad(plaintext))
150
    return b64encode(enc)
151

    
152

    
153
def get_vm(server_id, owner):
154
    """Return a VirtualMachine instance or raise ItemNotFound."""
155

    
156
    try:
157
        server_id = int(server_id)
158
        return VirtualMachine.objects.get(id=server_id, owner=owner)
159
    except ValueError:
160
        raise BadRequest('Invalid server ID.')
161
    except VirtualMachine.DoesNotExist:
162
        raise ItemNotFound('Server not found.')
163

    
164

    
165
def get_vm_meta(vm, key):
166
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
167

    
168
    try:
169
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
170
    except VirtualMachineMetadata.DoesNotExist:
171
        raise ItemNotFound('Metadata key not found.')
172

    
173

    
174
def get_image(image_id, owner):
175
    """Return an Image instance or raise ItemNotFound."""
176

    
177
    try:
178
        image_id = int(image_id)
179
        image = Image.objects.get(id=image_id)
180
        if not image.public and image.owner != owner:
181
            raise ItemNotFound('Image not found.')
182
        return image
183
    except ValueError:
184
        raise ItemNotFound('Image not found.')
185
    except Image.DoesNotExist:
186
        raise ItemNotFound('Image not found.')
187

    
188

    
189
def get_backend_image(image_id, owner):
190
    backend = ImageBackend(owner.uniq)
191
    try:
192
        image = backend.get_meta(image_id)
193
        if not image:
194
            raise ItemNotFound('Image not found.')
195
        return image
196
    finally:
197
        backend.close()
198

    
199

    
200
def get_image_meta(image, key):
201
    """Return a ImageMetadata instance or raise ItemNotFound."""
202

    
203
    try:
204
        return ImageMetadata.objects.get(meta_key=key, image=image)
205
    except ImageMetadata.DoesNotExist:
206
        raise ItemNotFound('Metadata key not found.')
207

    
208

    
209
def get_flavor(flavor_id):
210
    """Return a Flavor instance or raise ItemNotFound."""
211

    
212
    try:
213
        flavor_id = int(flavor_id)
214
        return Flavor.objects.get(id=flavor_id)
215
    except ValueError:
216
        raise BadRequest('Invalid flavor ID.')
217
    except Flavor.DoesNotExist:
218
        raise ItemNotFound('Flavor not found.')
219

    
220

    
221
def get_network(network_id, owner):
222
    """Return a Network instance or raise ItemNotFound."""
223

    
224
    try:
225
        if network_id == 'public':
226
            return Network.objects.get(public=True)
227
        else:
228
            network_id = int(network_id)
229
            return Network.objects.get(id=network_id, owner=owner)
230
    except ValueError:
231
        raise BadRequest('Invalid network ID.')
232
    except Network.DoesNotExist:
233
        raise ItemNotFound('Network not found.')
234

    
235

    
236
def get_nic(machine, network):
237
    try:
238
        return NetworkInterface.objects.get(machine=machine, network=network)
239
    except NetworkInterface.DoesNotExist:
240
        raise ItemNotFound('Server not connected to this network.')
241

    
242

    
243
def get_request_dict(request):
244
    """Returns data sent by the client as a python dict."""
245

    
246
    data = request.raw_post_data
247
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
248
        try:
249
            return json.loads(data)
250
        except ValueError:
251
            raise BadRequest('Invalid JSON data.')
252
    else:
253
        raise BadRequest('Unsupported Content-Type.')
254

    
255

    
256
def update_response_headers(request, response):
257
    if request.serialization == 'xml':
258
        response['Content-Type'] = 'application/xml'
259
    elif request.serialization == 'atom':
260
        response['Content-Type'] = 'application/atom+xml'
261
    else:
262
        response['Content-Type'] = 'application/json'
263

    
264
    if settings.TEST:
265
        response['Date'] = format_date_time(time())
266
    
267
    add_never_cache_headers(response)
268

    
269

    
270
def render_metadata(request, metadata, use_values=False, status=200):
271
    if request.serialization == 'xml':
272
        data = render_to_string('metadata.xml', {'metadata': metadata})
273
    else:
274
        if use_values:
275
            d = {'metadata': {'values': metadata}}
276
        else:
277
            d = {'metadata': metadata}
278
        data = json.dumps(d)
279
    return HttpResponse(data, status=status)
280

    
281

    
282
def render_meta(request, meta, status=200):
283
    if request.serialization == 'xml':
284
        data = render_to_string('meta.xml', {'meta': meta})
285
    else:
286
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
287
    return HttpResponse(data, status=status)
288

    
289

    
290
def render_fault(request, fault):
291
    if settings.DEBUG or settings.TEST:
292
        fault.details = format_exc(fault)
293

    
294
    if request.serialization == 'xml':
295
        data = render_to_string('fault.xml', {'fault': fault})
296
    else:
297
        d = {fault.name: {
298
                'code': fault.code,
299
                'message': fault.message,
300
                'details': fault.details}}
301
        data = json.dumps(d)
302

    
303
    resp = HttpResponse(data, status=fault.code)
304
    update_response_headers(request, resp)
305
    return resp
306

    
307

    
308
def request_serialization(request, atom_allowed=False):
309
    """Return the serialization format requested.
310

311
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
312
    """
313

    
314
    path = request.path
315

    
316
    if path.endswith('.json'):
317
        return 'json'
318
    elif path.endswith('.xml'):
319
        return 'xml'
320
    elif atom_allowed and path.endswith('.atom'):
321
        return 'atom'
322

    
323
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
324
        accept, sep, rest = item.strip().partition(';')
325
        if accept == 'application/json':
326
            return 'json'
327
        elif accept == 'application/xml':
328
            return 'xml'
329
        elif atom_allowed and accept == 'application/atom+xml':
330
            return 'atom'
331

    
332
    return 'json'
333

    
334

    
335
def api_method(http_method=None, atom_allowed=False):
336
    """Decorator function for views that implement an API method."""
337

    
338
    def decorator(func):
339
        @wraps(func)
340
        def wrapper(request, *args, **kwargs):
341
            u = request.user.uniq if request.user else ''
342
            try:
343

    
344
                request.serialization = request_serialization(
345
                    request,
346
                    atom_allowed)
347
                if not request.method == 'GET':
348
                    if 'readonly' in request.__dict__ and \
349
                       request.readonly == True:
350
                        raise BadRequest('Method not allowed')
351
                if not request.user:
352
                    raise Unauthorized('No user found.')
353
                if http_method and request.method != http_method:
354
                    raise BadRequest('Method not allowed.')
355

    
356
                resp = func(request, *args, **kwargs)
357
                update_response_headers(request, resp)
358
                return resp
359
            except VirtualMachine.DeletedError:
360
                fault = BadRequest('Server has been deleted.')
361
                return render_fault(request, fault)
362
            except VirtualMachine.BuildingError:
363
                fault = BuildInProgress('Server is being built.')
364
                return render_fault(request, fault)
365
            except Fault, fault:
366
                return render_fault(request, fault)
367
            except BaseException, e:
368
                log.exception('Unexpected error')
369
                fault = ServiceUnavailable('Unexpected error.')
370
                return render_fault(request, fault)
371
        return wrapper
372
    return decorator