Statistics
| Branch: | Tag: | Revision:

root / api / util.py @ 9e98ba3c

History | View | Annotate | Download (10.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 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 (SynnefoUser, Flavor, Image, ImageMetadata,
58
                                VirtualMachine, VirtualMachineMetadata,
59
                                Network, NetworkInterface)
60
from synnefo.util.log import getLogger
61

    
62

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

    
65

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

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

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

    
76

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

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

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

    
85
    if not s:
86
        return None
87

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

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

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

    
101
    return utc_since
102

    
103
def random_password(length=8):
104
    pool = ascii_letters + digits
105
    return ''.join(choice(pool) for i in range(length))
106

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

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

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

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

    
122

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

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

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

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

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

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

    
156
def get_image_meta(image, key):
157
    """Return a ImageMetadata instance or raise ItemNotFound."""
158

    
159
    try:
160
        return ImageMetadata.objects.get(meta_key=key, image=image)
161
    except ImageMetadata.DoesNotExist:
162
        raise ItemNotFound('Metadata key not found.')
163

    
164
def get_flavor(flavor_id):
165
    """Return a Flavor instance or raise ItemNotFound."""
166

    
167
    try:
168
        flavor_id = int(flavor_id)
169
        return Flavor.objects.get(id=flavor_id)
170
    except ValueError:
171
        raise BadRequest('Invalid flavor ID.')
172
    except Flavor.DoesNotExist:
173
        raise ItemNotFound('Flavor not found.')
174

    
175
def get_network(network_id, owner):
176
    """Return a Network instance or raise ItemNotFound."""
177

    
178
    try:
179
        if network_id == 'public':
180
            return Network.objects.get(public=True)
181
        else:
182
            network_id = int(network_id)
183
            return Network.objects.get(id=network_id, owner=owner)
184
    except ValueError:
185
        raise BadRequest('Invalid network ID.')
186
    except Network.DoesNotExist:
187
        raise ItemNotFound('Network not found.')
188

    
189
def get_nic(machine, network):
190
    try:
191
        return NetworkInterface.objects.get(machine=machine, network=network)
192
    except NetworkInterface.DoesNotExist:
193
        raise ItemNotFound('Server not connected to this network.')
194

    
195

    
196
def get_request_dict(request):
197
    """Returns data sent by the client as a python dict."""
198

    
199
    data = request.raw_post_data
200
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
201
        try:
202
            return json.loads(data)
203
        except ValueError:
204
            raise BadRequest('Invalid JSON data.')
205
    else:
206
        raise BadRequest('Unsupported Content-Type.')
207

    
208
def update_response_headers(request, response):
209
    if request.serialization == 'xml':
210
        response['Content-Type'] = 'application/xml'
211
    elif request.serialization == 'atom':
212
        response['Content-Type'] = 'application/atom+xml'
213
    else:
214
        response['Content-Type'] = 'application/json'
215

    
216
    if settings.TEST:
217
        response['Date'] = format_date_time(time())
218
    
219
    add_never_cache_headers(response)
220

    
221

    
222
def render_metadata(request, metadata, use_values=False, status=200):
223
    if request.serialization == 'xml':
224
        data = render_to_string('metadata.xml', {'metadata': metadata})
225
    else:
226
        if use_values:
227
            d = {'metadata': {'values': metadata}}
228
        else:
229
            d = {'metadata': metadata}
230
        data = json.dumps(d)
231
    return HttpResponse(data, status=status)
232

    
233
def render_meta(request, meta, status=200):
234
    if request.serialization == 'xml':
235
        data = render_to_string('meta.xml', {'meta': meta})
236
    else:
237
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
238
    return HttpResponse(data, status=status)
239

    
240
def render_fault(request, fault):
241
    if settings.DEBUG or settings.TEST:
242
        fault.details = format_exc(fault)
243

    
244
    if request.serialization == 'xml':
245
        data = render_to_string('fault.xml', {'fault': fault})
246
    else:
247
        d = {fault.name: {
248
                'code': fault.code,
249
                'message': fault.message,
250
                'details': fault.details}}
251
        data = json.dumps(d)
252

    
253
    resp = HttpResponse(data, status=fault.code)
254
    update_response_headers(request, resp)
255
    return resp
256

    
257

    
258
def request_serialization(request, atom_allowed=False):
259
    """Return the serialization format requested.
260

261
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
262
    """
263

    
264
    path = request.path
265

    
266
    if path.endswith('.json'):
267
        return 'json'
268
    elif path.endswith('.xml'):
269
        return 'xml'
270
    elif atom_allowed and path.endswith('.atom'):
271
        return 'atom'
272

    
273
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
274
        accept, sep, rest = item.strip().partition(';')
275
        if accept == 'application/json':
276
            return 'json'
277
        elif accept == 'application/xml':
278
            return 'xml'
279
        elif atom_allowed and accept == 'application/atom+xml':
280
            return 'atom'
281

    
282
    return 'json'
283

    
284
def api_method(http_method=None, atom_allowed=False):
285
    """Decorator function for views that implement an API method."""
286

    
287
    def decorator(func):
288
        @wraps(func)
289
        def wrapper(request, *args, **kwargs):
290
            u = request.user.uniq if request.user else ''
291
            try:
292

    
293
                request.serialization = request_serialization(
294
                    request,
295
                    atom_allowed)
296
                if not request.method == 'GET':
297
                    if 'readonly' in request.__dict__ and \
298
                       request.readonly == True:
299
                        raise BadRequest('Method not allowed')
300
                if not request.user:
301
                    raise Unauthorized('No user found.')
302
                if http_method and request.method != http_method:
303
                    raise BadRequest('Method not allowed.')
304

    
305
                resp = func(request, *args, **kwargs)
306
                update_response_headers(request, resp)
307
                return resp
308
            except VirtualMachine.DeletedError:
309
                fault = BadRequest('Server has been deleted.')
310
                return render_fault(request, fault)
311
            except VirtualMachine.BuildingError:
312
                fault = BuildInProgress('Server is being built.')
313
                return render_fault(request, fault)
314
            except Fault, fault:
315
                return render_fault(request, fault)
316
            except BaseException, e:
317
                log.exception('Unexpected error')
318
                fault = ServiceUnavailable('Unexpected error.')
319
                return render_fault(request, fault)
320
        return wrapper
321
    return decorator