Statistics
| Branch: | Tag: | Revision:

root / snf-app / synnefo / api / util.py @ 3a9b3cde

History | View | Annotate | Download (10.8 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 ascii_letters, digits
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
def random_password(length=8):
105
    pool = ascii_letters + digits
106
    return ''.join(choice(pool) for i in range(length))
107

    
108
def zeropad(s):
109
    """Add zeros at the end of a string in order to make its length
110
       a multiple of 16."""
111

    
112
    npad = 16 - len(s) % 16
113
    return s + '\x00' * npad
114

    
115
def encrypt(plaintext):
116
    # Make sure key is 32 bytes long
117
    key = sha256(settings.SECRET_KEY).digest()
118

    
119
    aes = AES.new(key)
120
    enc = aes.encrypt(zeropad(plaintext))
121
    return b64encode(enc)
122

    
123

    
124
def get_vm(server_id, owner):
125
    """Return a VirtualMachine instance or raise ItemNotFound."""
126

    
127
    try:
128
        server_id = int(server_id)
129
        return VirtualMachine.objects.get(id=server_id, owner=owner)
130
    except ValueError:
131
        raise BadRequest('Invalid server ID.')
132
    except VirtualMachine.DoesNotExist:
133
        raise ItemNotFound('Server not found.')
134

    
135
def get_vm_meta(vm, key):
136
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
137

    
138
    try:
139
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
140
    except VirtualMachineMetadata.DoesNotExist:
141
        raise ItemNotFound('Metadata key not found.')
142

    
143
def get_image(image_id, owner):
144
    """Return an Image instance or raise ItemNotFound."""
145

    
146
    try:
147
        image_id = int(image_id)
148
        image = Image.objects.get(id=image_id)
149
        if not image.public and image.owner != owner:
150
            raise ItemNotFound('Image not found.')
151
        return image
152
    except ValueError:
153
        raise ItemNotFound('Image not found.')
154
    except Image.DoesNotExist:
155
        raise ItemNotFound('Image not found.')
156

    
157
def get_backend_image(image_id, owner):
158
    backend = ImageBackend(owner.uniq)
159
    try:
160
        image = backend.get_meta(image_id)
161
        if not image:
162
            raise ItemNotFound('Image not found.')
163
        return image
164
    finally:
165
        backend.close()
166

    
167
def get_image_meta(image, key):
168
    """Return a ImageMetadata instance or raise ItemNotFound."""
169

    
170
    try:
171
        return ImageMetadata.objects.get(meta_key=key, image=image)
172
    except ImageMetadata.DoesNotExist:
173
        raise ItemNotFound('Metadata key not found.')
174

    
175
def get_flavor(flavor_id):
176
    """Return a Flavor instance or raise ItemNotFound."""
177

    
178
    try:
179
        flavor_id = int(flavor_id)
180
        return Flavor.objects.get(id=flavor_id)
181
    except ValueError:
182
        raise BadRequest('Invalid flavor ID.')
183
    except Flavor.DoesNotExist:
184
        raise ItemNotFound('Flavor not found.')
185

    
186
def get_network(network_id, owner):
187
    """Return a Network instance or raise ItemNotFound."""
188

    
189
    try:
190
        if network_id == 'public':
191
            return Network.objects.get(public=True)
192
        else:
193
            network_id = int(network_id)
194
            return Network.objects.get(id=network_id, owner=owner)
195
    except ValueError:
196
        raise BadRequest('Invalid network ID.')
197
    except Network.DoesNotExist:
198
        raise ItemNotFound('Network not found.')
199

    
200
def get_nic(machine, network):
201
    try:
202
        return NetworkInterface.objects.get(machine=machine, network=network)
203
    except NetworkInterface.DoesNotExist:
204
        raise ItemNotFound('Server not connected to this network.')
205

    
206

    
207
def get_request_dict(request):
208
    """Returns data sent by the client as a python dict."""
209

    
210
    data = request.raw_post_data
211
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
212
        try:
213
            return json.loads(data)
214
        except ValueError:
215
            raise BadRequest('Invalid JSON data.')
216
    else:
217
        raise BadRequest('Unsupported Content-Type.')
218

    
219
def update_response_headers(request, response):
220
    if request.serialization == 'xml':
221
        response['Content-Type'] = 'application/xml'
222
    elif request.serialization == 'atom':
223
        response['Content-Type'] = 'application/atom+xml'
224
    else:
225
        response['Content-Type'] = 'application/json'
226

    
227
    if settings.TEST:
228
        response['Date'] = format_date_time(time())
229
    
230
    add_never_cache_headers(response)
231

    
232

    
233
def render_metadata(request, metadata, use_values=False, status=200):
234
    if request.serialization == 'xml':
235
        data = render_to_string('metadata.xml', {'metadata': metadata})
236
    else:
237
        if use_values:
238
            d = {'metadata': {'values': metadata}}
239
        else:
240
            d = {'metadata': metadata}
241
        data = json.dumps(d)
242
    return HttpResponse(data, status=status)
243

    
244
def render_meta(request, meta, status=200):
245
    if request.serialization == 'xml':
246
        data = render_to_string('meta.xml', {'meta': meta})
247
    else:
248
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
249
    return HttpResponse(data, status=status)
250

    
251
def render_fault(request, fault):
252
    if settings.DEBUG or settings.TEST:
253
        fault.details = format_exc(fault)
254

    
255
    if request.serialization == 'xml':
256
        data = render_to_string('fault.xml', {'fault': fault})
257
    else:
258
        d = {fault.name: {
259
                'code': fault.code,
260
                'message': fault.message,
261
                'details': fault.details}}
262
        data = json.dumps(d)
263

    
264
    resp = HttpResponse(data, status=fault.code)
265
    update_response_headers(request, resp)
266
    return resp
267

    
268

    
269
def request_serialization(request, atom_allowed=False):
270
    """Return the serialization format requested.
271

272
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
273
    """
274

    
275
    path = request.path
276

    
277
    if path.endswith('.json'):
278
        return 'json'
279
    elif path.endswith('.xml'):
280
        return 'xml'
281
    elif atom_allowed and path.endswith('.atom'):
282
        return 'atom'
283

    
284
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
285
        accept, sep, rest = item.strip().partition(';')
286
        if accept == 'application/json':
287
            return 'json'
288
        elif accept == 'application/xml':
289
            return 'xml'
290
        elif atom_allowed and accept == 'application/atom+xml':
291
            return 'atom'
292

    
293
    return 'json'
294

    
295
def api_method(http_method=None, atom_allowed=False):
296
    """Decorator function for views that implement an API method."""
297

    
298
    def decorator(func):
299
        @wraps(func)
300
        def wrapper(request, *args, **kwargs):
301
            u = request.user.uniq if request.user else ''
302
            try:
303

    
304
                request.serialization = request_serialization(
305
                    request,
306
                    atom_allowed)
307
                if not request.method == 'GET':
308
                    if 'readonly' in request.__dict__ and \
309
                       request.readonly == True:
310
                        raise BadRequest('Method not allowed')
311
                if not request.user:
312
                    raise Unauthorized('No user found.')
313
                if http_method and request.method != http_method:
314
                    raise BadRequest('Method not allowed.')
315

    
316
                resp = func(request, *args, **kwargs)
317
                update_response_headers(request, resp)
318
                return resp
319
            except VirtualMachine.DeletedError:
320
                fault = BadRequest('Server has been deleted.')
321
                return render_fault(request, fault)
322
            except VirtualMachine.BuildingError:
323
                fault = BuildInProgress('Server is being built.')
324
                return render_fault(request, fault)
325
            except Fault, fault:
326
                return render_fault(request, fault)
327
            except BaseException, e:
328
                log.exception('Unexpected error')
329
                fault = ServiceUnavailable('Unexpected error.')
330
                return render_fault(request, fault)
331
        return wrapper
332
    return decorator