Statistics
| Branch: | Tag: | Revision:

root / api / util.py @ f4fe8796

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

    
54
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress,
55
                                ItemNotFound, ServiceUnavailable, Unauthorized)
56
from synnefo.db.models import (SynnefoUser, Flavor, Image, ImageMetadata,
57
                                VirtualMachine, VirtualMachineMetadata,
58
                                Network, NetworkInterface)
59
from synnefo.logic import log
60

    
61
class UTC(tzinfo):
62
    def utcoffset(self, dt):
63
        return timedelta(0)
64

    
65
    def tzname(self, dt):
66
        return 'UTC'
67

    
68
    def dst(self, dt):
69
        return timedelta(0)
70

    
71

    
72
def isoformat(d):
73
    """Return an ISO8601 date string that includes a timezone."""
74

    
75
    return d.replace(tzinfo=UTC()).isoformat()
76

    
77
def isoparse(s):
78
    """Parse an ISO8601 date string into a datetime object."""
79

    
80
    if not s:
81
        return None
82

    
83
    try:
84
        since = dateutil.parser.parse(s)
85
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
86
    except ValueError:
87
        raise BadRequest('Invalid changes-since parameter.')
88

    
89
    now = datetime.datetime.now()
90
    if utc_since > now:
91
        raise BadRequest('changes-since value set in the future.')
92

    
93
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
94
        raise BadRequest('Too old changes-since value.')
95

    
96
    return utc_since
97

    
98
def random_password(length=8):
99
    pool = ascii_letters + digits
100
    return ''.join(choice(pool) for i in range(length))
101

    
102
def zeropad(s):
103
    """Add zeros at the end of a string in order to make its length
104
       a multiple of 16."""
105

    
106
    npad = 16 - len(s) % 16
107
    return s + '\x00' * npad
108

    
109
def encrypt(plaintext):
110
    # Make sure key is 32 bytes long
111
    key = sha256(settings.SECRET_KEY).digest()
112

    
113
    aes = AES.new(key)
114
    enc = aes.encrypt(zeropad(plaintext))
115
    return b64encode(enc)
116

    
117

    
118
def get_vm(server_id, owner):
119
    """Return a VirtualMachine instance or raise ItemNotFound."""
120

    
121
    try:
122
        server_id = int(server_id)
123
        return VirtualMachine.objects.get(id=server_id, owner=owner)
124
    except ValueError:
125
        raise BadRequest('Invalid server ID.')
126
    except VirtualMachine.DoesNotExist:
127
        raise ItemNotFound('Server not found.')
128

    
129
def get_vm_meta(vm, key):
130
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
131

    
132
    try:
133
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
134
    except VirtualMachineMetadata.DoesNotExist:
135
        raise ItemNotFound('Metadata key not found.')
136

    
137
def get_image(image_id, owner):
138
    """Return an Image instance or raise ItemNotFound."""
139

    
140
    try:
141
        image_id = int(image_id)
142
        image = Image.objects.get(id=image_id)
143
        if not image.public and image.owner != owner:
144
            raise ItemNotFound('Image not found.')
145
        return image
146
    except ValueError:
147
        raise BadRequest('Invalid image ID.')
148
    except Image.DoesNotExist:
149
        raise ItemNotFound('Image not found.')
150

    
151
def get_image_meta(image, key):
152
    """Return a ImageMetadata instance or raise ItemNotFound."""
153

    
154
    try:
155
        return ImageMetadata.objects.get(meta_key=key, image=image)
156
    except ImageMetadata.DoesNotExist:
157
        raise ItemNotFound('Metadata key not found.')
158

    
159
def get_flavor(flavor_id):
160
    """Return a Flavor instance or raise ItemNotFound."""
161

    
162
    try:
163
        flavor_id = int(flavor_id)
164
        return Flavor.objects.get(id=flavor_id)
165
    except ValueError:
166
        raise BadRequest('Invalid flavor ID.')
167
    except Flavor.DoesNotExist:
168
        raise ItemNotFound('Flavor not found.')
169

    
170
def get_network(network_id, owner):
171
    """Return a Network instance or raise ItemNotFound."""
172

    
173
    try:
174
        if network_id == 'public':
175
            return Network.objects.get(public=True)
176
        else:
177
            network_id = int(network_id)
178
            return Network.objects.get(id=network_id, owner=owner)
179
    except ValueError:
180
        raise BadRequest('Invalid network ID.')
181
    except Network.DoesNotExist:
182
        raise ItemNotFound('Network not found.')
183

    
184
def get_nic(machine, network):
185
    try:
186
        return NetworkInterface.objects.get(machine=machine, network=network)
187
    except NetworkInterface.DoesNotExist:
188
        raise ItemNotFound('Server not connected to this network.')
189

    
190

    
191
def get_request_dict(request):
192
    """Returns data sent by the client as a python dict."""
193

    
194
    data = request.raw_post_data
195
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
196
        try:
197
            return json.loads(data)
198
        except ValueError:
199
            raise BadRequest('Invalid JSON data.')
200
    else:
201
        raise BadRequest('Unsupported Content-Type.')
202

    
203
def update_response_headers(request, response):
204
    if request.serialization == 'xml':
205
        response['Content-Type'] = 'application/xml'
206
    elif request.serialization == 'atom':
207
        response['Content-Type'] = 'application/atom+xml'
208
    else:
209
        response['Content-Type'] = 'application/json'
210

    
211
    if settings.TEST:
212
        response['Date'] = format_date_time(time())
213

    
214
def render_metadata(request, metadata, use_values=False, status=200):
215
    if request.serialization == 'xml':
216
        data = render_to_string('metadata.xml', {'metadata': metadata})
217
    else:
218
        if use_values:
219
            d = {'metadata': {'values': metadata}}
220
        else:
221
            d = {'metadata': metadata}
222
        data = json.dumps(d)
223
    return HttpResponse(data, status=status)
224

    
225
def render_meta(request, meta, status=200):
226
    if request.serialization == 'xml':
227
        data = render_to_string('meta.xml', {'meta': meta})
228
    else:
229
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
230
    return HttpResponse(data, status=status)
231

    
232
def render_fault(request, fault):
233
    if settings.DEBUG or settings.TEST:
234
        fault.details = format_exc(fault)
235

    
236
    if request.serialization == 'xml':
237
        data = render_to_string('fault.xml', {'fault': fault})
238
    else:
239
        d = {fault.name: {
240
                'code': fault.code,
241
                'message': fault.message,
242
                'details': fault.details}}
243
        data = json.dumps(d)
244

    
245
    resp = HttpResponse(data, status=fault.code)
246
    update_response_headers(request, resp)
247
    return resp
248

    
249

    
250
def request_serialization(request, atom_allowed=False):
251
    """Return the serialization format requested.
252

253
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
254
    """
255

    
256
    path = request.path
257

    
258
    if path.endswith('.json'):
259
        return 'json'
260
    elif path.endswith('.xml'):
261
        return 'xml'
262
    elif atom_allowed and path.endswith('.atom'):
263
        return 'atom'
264

    
265
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
266
        accept, sep, rest = item.strip().partition(';')
267
        if accept == 'application/json':
268
            return 'json'
269
        elif accept == 'application/xml':
270
            return 'xml'
271
        elif atom_allowed and accept == 'application/atom+xml':
272
            return 'atom'
273

    
274
    return 'json'
275

    
276
def api_method(http_method=None, atom_allowed=False):
277
    """Decorator function for views that implement an API method."""
278

    
279
    def decorator(func):
280
        @wraps(func)
281
        def wrapper(request, *args, **kwargs):
282
            u = request.user.uniq if request.user else ''
283
            logger = log.get_logger("synnefo.api")
284
            logger.debug("%s <%s>" % (request.path, u))
285
            try:
286

    
287
                request.serialization = request_serialization(
288
                    request,
289
                    atom_allowed)
290
                if not request.method == 'GET':
291
                    if 'readonly' in request.__dict__ and \
292
                       request.readonly == True:
293
                        raise BadRequest('Method not allowed')
294
                if not request.user:
295
                    raise Unauthorized('No user found.')
296
                if http_method and request.method != http_method:
297
                    raise BadRequest('Method not allowed.')
298

    
299
                resp = func(request, *args, **kwargs)
300
                update_response_headers(request, resp)
301
                return resp
302
            except VirtualMachine.DeletedError:
303
                fault = BadRequest('Server has been deleted.')
304
                return render_fault(request, fault)
305
            except VirtualMachine.BuildingError:
306
                fault = BuildInProgress('Server is being built.')
307
                return render_fault(request, fault)
308
            except Fault, fault:
309
                return render_fault(request, fault)
310
            except BaseException, e:
311
                logger.exception('Unexpected error')
312
                fault = ServiceUnavailable('Unexpected error.')
313
                return render_fault(request, fault)
314
        return wrapper
315
    return decorator