Statistics
| Branch: | Tag: | Revision:

root / snf-app / synnefo / api / util.py @ bfd9f988

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
def isoparse(s):
84
    """Parse an ISO8601 date string into a datetime object."""
85

    
86
    if not s:
87
        return None
88

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

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

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

    
102
    return utc_since
103

    
104

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

    
134

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

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

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

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

    
150

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

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

    
162
def get_vm_meta(vm, key):
163
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
164

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

    
170
def get_image(image_id, owner):
171
    """Return an Image instance or raise ItemNotFound."""
172

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

    
184
def get_backend_image(image_id, owner):
185
    backend = ImageBackend(owner.uniq)
186
    try:
187
        image = backend.get_meta(image_id)
188
        if not image:
189
            raise ItemNotFound('Image not found.')
190
        return image
191
    finally:
192
        backend.close()
193

    
194
def get_image_meta(image, key):
195
    """Return a ImageMetadata instance or raise ItemNotFound."""
196

    
197
    try:
198
        return ImageMetadata.objects.get(meta_key=key, image=image)
199
    except ImageMetadata.DoesNotExist:
200
        raise ItemNotFound('Metadata key not found.')
201

    
202
def get_flavor(flavor_id):
203
    """Return a Flavor instance or raise ItemNotFound."""
204

    
205
    try:
206
        flavor_id = int(flavor_id)
207
        return Flavor.objects.get(id=flavor_id)
208
    except ValueError:
209
        raise BadRequest('Invalid flavor ID.')
210
    except Flavor.DoesNotExist:
211
        raise ItemNotFound('Flavor not found.')
212

    
213
def get_network(network_id, owner):
214
    """Return a Network instance or raise ItemNotFound."""
215

    
216
    try:
217
        if network_id == 'public':
218
            return Network.objects.get(public=True)
219
        else:
220
            network_id = int(network_id)
221
            return Network.objects.get(id=network_id, owner=owner)
222
    except ValueError:
223
        raise BadRequest('Invalid network ID.')
224
    except Network.DoesNotExist:
225
        raise ItemNotFound('Network not found.')
226

    
227
def get_nic(machine, network):
228
    try:
229
        return NetworkInterface.objects.get(machine=machine, network=network)
230
    except NetworkInterface.DoesNotExist:
231
        raise ItemNotFound('Server not connected to this network.')
232

    
233

    
234
def get_request_dict(request):
235
    """Returns data sent by the client as a python dict."""
236

    
237
    data = request.raw_post_data
238
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
239
        try:
240
            return json.loads(data)
241
        except ValueError:
242
            raise BadRequest('Invalid JSON data.')
243
    else:
244
        raise BadRequest('Unsupported Content-Type.')
245

    
246
def update_response_headers(request, response):
247
    if request.serialization == 'xml':
248
        response['Content-Type'] = 'application/xml'
249
    elif request.serialization == 'atom':
250
        response['Content-Type'] = 'application/atom+xml'
251
    else:
252
        response['Content-Type'] = 'application/json'
253

    
254
    if settings.TEST:
255
        response['Date'] = format_date_time(time())
256
    
257
    add_never_cache_headers(response)
258

    
259

    
260
def render_metadata(request, metadata, use_values=False, status=200):
261
    if request.serialization == 'xml':
262
        data = render_to_string('metadata.xml', {'metadata': metadata})
263
    else:
264
        if use_values:
265
            d = {'metadata': {'values': metadata}}
266
        else:
267
            d = {'metadata': metadata}
268
        data = json.dumps(d)
269
    return HttpResponse(data, status=status)
270

    
271
def render_meta(request, meta, status=200):
272
    if request.serialization == 'xml':
273
        data = render_to_string('meta.xml', {'meta': meta})
274
    else:
275
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
276
    return HttpResponse(data, status=status)
277

    
278
def render_fault(request, fault):
279
    if settings.DEBUG or settings.TEST:
280
        fault.details = format_exc(fault)
281

    
282
    if request.serialization == 'xml':
283
        data = render_to_string('fault.xml', {'fault': fault})
284
    else:
285
        d = {fault.name: {
286
                'code': fault.code,
287
                'message': fault.message,
288
                'details': fault.details}}
289
        data = json.dumps(d)
290

    
291
    resp = HttpResponse(data, status=fault.code)
292
    update_response_headers(request, resp)
293
    return resp
294

    
295

    
296
def request_serialization(request, atom_allowed=False):
297
    """Return the serialization format requested.
298

299
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
300
    """
301

    
302
    path = request.path
303

    
304
    if path.endswith('.json'):
305
        return 'json'
306
    elif path.endswith('.xml'):
307
        return 'xml'
308
    elif atom_allowed and path.endswith('.atom'):
309
        return 'atom'
310

    
311
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
312
        accept, sep, rest = item.strip().partition(';')
313
        if accept == 'application/json':
314
            return 'json'
315
        elif accept == 'application/xml':
316
            return 'xml'
317
        elif atom_allowed and accept == 'application/atom+xml':
318
            return 'atom'
319

    
320
    return 'json'
321

    
322
def api_method(http_method=None, atom_allowed=False):
323
    """Decorator function for views that implement an API method."""
324

    
325
    def decorator(func):
326
        @wraps(func)
327
        def wrapper(request, *args, **kwargs):
328
            u = request.user.uniq if request.user else ''
329
            try:
330

    
331
                request.serialization = request_serialization(
332
                    request,
333
                    atom_allowed)
334
                if not request.method == 'GET':
335
                    if 'readonly' in request.__dict__ and \
336
                       request.readonly == True:
337
                        raise BadRequest('Method not allowed')
338
                if not request.user:
339
                    raise Unauthorized('No user found.')
340
                if http_method and request.method != http_method:
341
                    raise BadRequest('Method not allowed.')
342

    
343
                resp = func(request, *args, **kwargs)
344
                update_response_headers(request, resp)
345
                return resp
346
            except VirtualMachine.DeletedError:
347
                fault = BadRequest('Server has been deleted.')
348
                return render_fault(request, fault)
349
            except VirtualMachine.BuildingError:
350
                fault = BuildInProgress('Server is being built.')
351
                return render_fault(request, fault)
352
            except Fault, fault:
353
                return render_fault(request, fault)
354
            except BaseException, e:
355
                log.exception('Unexpected error')
356
                fault = ServiceUnavailable('Unexpected error.')
357
                return render_fault(request, fault)
358
        return wrapper
359
    return decorator