Statistics
| Branch: | Tag: | Revision:

root / api / util.py @ 40777cc8

History | View | Annotate | Download (7.3 kB)

1
#
2
# Copyright (c) 2010 Greek Research and Technology Network
3
#
4

    
5
from datetime import timedelta, tzinfo
6
from functools import wraps
7
from random import choice
8
from string import ascii_letters, digits
9
from time import time
10
from traceback import format_exc
11
from wsgiref.handlers import format_date_time
12

    
13
import datetime
14
import dateutil.parser
15
import logging
16

    
17
from django.conf import settings
18
from django.http import HttpResponse
19
from django.template.loader import render_to_string
20
from django.utils import simplejson as json
21

    
22
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress, ItemNotFound,
23
                                ServiceUnavailable, Unauthorized)
24
from synnefo.db.models import (SynnefoUser, Flavor, Image, ImageMetadata,
25
                                VirtualMachine, VirtualMachineMetadata)
26

    
27

    
28
class UTC(tzinfo):
29
    def utcoffset(self, dt):
30
        return timedelta(0)
31
    
32
    def tzname(self, dt):
33
        return 'UTC'
34
    
35
    def dst(self, dt):
36
        return timedelta(0)
37

    
38

    
39
def isoformat(d):
40
    """Return an ISO8601 date string that includes a timezone."""
41
    
42
    return d.replace(tzinfo=UTC()).isoformat()
43

    
44
def isoparse(s):
45
    """Parse an ISO8601 date string into a datetime object."""
46
    
47
    if not s:
48
        return None
49
    
50
    try:
51
        since = dateutil.parser.parse(s)
52
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
53
    except ValueError:
54
        raise BadRequest('Invalid changes-since parameter.')
55
    
56
    now = datetime.datetime.now()
57
    if utc_since > now:
58
        raise BadRequest('changes-since value set in the future.')
59
    
60
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
61
        raise BadRequest('Too old changes-since value.')
62
    
63
    return utc_since
64
    
65
def random_password(length=8):
66
    pool = ascii_letters + digits
67
    return ''.join(choice(pool) for i in range(length))
68

    
69

    
70
def get_vm(server_id, owner):
71
    """Return a VirtualMachine instance or raise ItemNotFound."""
72
    
73
    try:
74
        server_id = int(server_id)
75
        return VirtualMachine.objects.get(id=server_id, owner=owner)
76
    except ValueError:
77
        raise BadRequest('Invalid server ID.')
78
    except VirtualMachine.DoesNotExist:
79
        raise ItemNotFound('Server not found.')
80

    
81
def get_vm_meta(vm, key):
82
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
83
    
84
    try:
85
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
86
    except VirtualMachineMetadata.DoesNotExist:
87
        raise ItemNotFound('Metadata key not found.')
88

    
89
def get_image(image_id, owner):
90
    """Return an Image instance or raise ItemNotFound."""
91
    
92
    try:
93
        image_id = int(image_id)
94
        return Image.objects.get(id=image_id, owner=owner)
95
    except ValueError:
96
        raise BadRequest('Invalid image ID.')
97
    except Image.DoesNotExist:
98
        raise ItemNotFound('Image not found.')
99

    
100
def get_image_meta(image, key):
101
    """Return a ImageMetadata instance or raise ItemNotFound."""
102

    
103
    try:
104
        return ImageMetadata.objects.get(meta_key=key, image=image)
105
    except ImageMetadata.DoesNotExist:
106
        raise ItemNotFound('Metadata key not found.')
107

    
108
def get_flavor(flavor_id):
109
    """Return a Flavor instance or raise ItemNotFound."""
110
    
111
    try:
112
        flavor_id = int(flavor_id)
113
        return Flavor.objects.get(id=flavor_id)
114
    except ValueError:
115
        raise BadRequest('Invalid flavor ID.')
116
    except Flavor.DoesNotExist:
117
        raise ItemNotFound('Flavor not found.')
118

    
119
def get_request_dict(request):
120
    """Returns data sent by the client as a python dict."""
121
    
122
    data = request.raw_post_data
123
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
124
        try:
125
            return json.loads(data)
126
        except ValueError:
127
            raise BadRequest('Invalid JSON data.')
128
    else:
129
        raise BadRequest('Unsupported Content-Type.')
130

    
131

    
132
def update_response_headers(request, response):
133
    if request.serialization == 'xml':
134
        response['Content-Type'] = 'application/xml'
135
    elif request.serialization == 'atom':
136
        response['Content-Type'] = 'application/atom+xml'
137
    else:
138
        response['Content-Type'] = 'application/json'
139
    
140
    if settings.TEST:
141
        response['Date'] = format_date_time(time())
142

    
143
def render_metadata(request, metadata, use_values=False, status=200):
144
    if request.serialization == 'xml':
145
        data = render_to_string('metadata.xml', {'metadata': metadata})
146
    else:
147
        d = {'metadata': {'values': metadata}} if use_values else {'metadata': metadata}
148
        data = json.dumps(d)
149
    return HttpResponse(data, status=status)
150

    
151
def render_meta(request, meta, status=200):
152
    if request.serialization == 'xml':
153
        data = render_to_string('meta.xml', {'meta': meta})
154
    else:
155
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
156
    return HttpResponse(data, status=status)
157

    
158
def render_fault(request, fault):
159
    if settings.DEBUG or settings.TEST:
160
        fault.details = format_exc(fault)
161
    
162
    if request.serialization == 'xml':
163
        data = render_to_string('fault.xml', {'fault': fault})
164
    else:
165
        d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}
166
        data = json.dumps(d)
167
    
168
    resp = HttpResponse(data, status=fault.code)
169
    update_response_headers(request, resp)
170
    return resp
171

    
172

    
173
def request_serialization(request, atom_allowed=False):
174
    """Return the serialization format requested.
175
       
176
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
177
    """
178
    
179
    path = request.path
180
    
181
    if path.endswith('.json'):
182
        return 'json'
183
    elif path.endswith('.xml'):
184
        return 'xml'
185
    elif atom_allowed and path.endswith('.atom'):
186
        return 'atom'
187
    
188
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
189
        accept, sep, rest = item.strip().partition(';')
190
        if accept == 'application/json':
191
            return 'json'
192
        elif accept == 'application/xml':
193
            return 'xml'
194
        elif atom_allowed and accept == 'application/atom+xml':
195
            return 'atom'
196
    
197
    return 'json'
198

    
199
def api_method(http_method=None, atom_allowed=False):
200
    """Decorator function for views that implement an API method."""
201
    
202
    def decorator(func):
203
        @wraps(func)
204
        def wrapper(request, *args, **kwargs):
205
            try:
206
                if not request.user:
207
                    raise Unauthorized('No user found.')
208
                request.serialization = request_serialization(request, atom_allowed)
209
                if http_method and request.method != http_method:
210
                    raise BadRequest('Method not allowed.')
211
                
212
                resp = func(request, *args, **kwargs)
213
                update_response_headers(request, resp)
214
                return resp
215
            except VirtualMachine.DeletedError:
216
                fault = BadRequest('Server has been deleted.')
217
                return render_fault(request, fault)
218
            except VirtualMachine.BuildingError:
219
                fault = BuildInProgress('Server is being built.')
220
                return render_fault(request, fault)
221
            except Fault, fault:
222
                return render_fault(request, fault)
223
            except BaseException, e:
224
                logging.exception('Unexpected error: %s', e)
225
                fault = ServiceUnavailable('Unexpected error.')
226
                return render_fault(request, fault)
227
        return wrapper
228
    return decorator