Statistics
| Branch: | Tag: | Revision:

root / api / util.py @ 0140e54b

History | View | Annotate | Download (6.8 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 traceback import format_exc
10

    
11
from django.conf import settings
12
from django.http import HttpResponse
13
from django.template.loader import render_to_string
14
from django.utils import simplejson as json
15

    
16
from synnefo.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable
17
from synnefo.db.models import SynnefoUser, Image, ImageMetadata, VirtualMachine, VirtualMachineMetadata
18

    
19
import datetime
20
import dateutil.parser
21
import logging
22

    
23

    
24
class UTC(tzinfo):
25
    def utcoffset(self, dt):
26
        return timedelta(0)
27
    
28
    def tzname(self, dt):
29
        return 'UTC'
30
    
31
    def dst(self, dt):
32
        return timedelta(0)
33

    
34

    
35
def isoformat(d):
36
    """Return an ISO8601 date string that includes a timezon."""
37
    
38
    return d.replace(tzinfo=UTC()).isoformat()
39

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

    
65

    
66
def get_user():
67
    # XXX Placeholder function, everything belongs to a single SynnefoUser for now
68
    try:
69
        return SynnefoUser.objects.all()[0]
70
    except IndexError:
71
        raise Unauthorized
72

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

    
84
def get_vm_meta(server_id, key):
85
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
86
    
87
    try:
88
        server_id = int(server_id)
89
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=server_id)
90
    except VirtualMachineMetadata.DoesNotExist:
91
        raise ItemNotFound('Metadata key not found.')
92

    
93
def get_image(image_id):
94
    """Return an Image instance or raise ItemNotFound."""
95
    
96
    try:
97
        image_id = int(image_id)
98
        return Image.objects.get(id=image_id)
99
    except Image.DoesNotExist:
100
        raise ItemNotFound('Image not found.')
101

    
102
def get_image_meta(image_id, key):
103
    """Return a ImageMetadata instance or raise ItemNotFound."""
104

    
105
    try:
106
        image_id = int(image_id)
107
        return ImageMetadata.objects.get(meta_key=key, image=image_id)
108
    except ImageMetadata.DoesNotExist:
109
        raise ItemNotFound('Metadata key not found.')
110

    
111

    
112
def get_request_dict(request):
113
    """Returns data sent by the client as a python dict."""
114
    
115
    data = request.raw_post_data
116
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
117
        try:
118
            return json.loads(data)
119
        except ValueError:
120
            raise BadRequest('Invalid JSON data.')
121
    else:
122
        raise BadRequest('Unsupported Content-Type.')
123

    
124

    
125
def render_metadata(request, metadata, use_values=False, status=200):
126
    if request.serialization == 'xml':
127
        data = render_to_string('metadata.xml', {'metadata': metadata})
128
    else:
129
        d = {'metadata': {'values': metadata}} if use_values else {'metadata': metadata}
130
        data = json.dumps(d)
131
    return HttpResponse(data, status=status)
132

    
133
def render_meta(request, meta, status=200):
134
    if request.serialization == 'xml':
135
        data = render_to_string('meta.xml', {'meta': meta})
136
    else:
137
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
138
    return HttpResponse(data, status=status)
139

    
140
def render_fault(request, fault):
141
    if settings.DEBUG or request.META.get('SERVER_NAME') == 'testserver':
142
        fault.details = format_exc(fault)
143
    
144
    if request.serialization == 'xml':
145
        data = render_to_string('fault.xml', {'fault': fault})
146
    else:
147
        d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}
148
        data = json.dumps(d)
149
    
150
    resp = HttpResponse(data, status=fault.code)
151
    
152
    if request.serialization == 'xml':
153
        resp['Content-Type'] = 'application/xml'
154
    elif request.serialization == 'atom':
155
        resp['Content-Type'] = 'application/atom+xml'
156
    else:
157
        resp['Content-Type'] = 'application/json'
158
    
159
    return resp
160

    
161

    
162
def request_serialization(request, atom_allowed=False):
163
    """Return the serialization format requested.
164
       
165
       Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
166
    """
167
    
168
    path = request.path
169
    
170
    if path.endswith('.json'):
171
        return 'json'
172
    elif path.endswith('.xml'):
173
        return 'xml'
174
    elif atom_allowed and path.endswith('.atom'):
175
        return 'atom'
176
    
177
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
178
        accept, sep, rest = item.strip().partition(';')
179
        if accept == 'application/json':
180
            return 'json'
181
        elif accept == 'application/xml':
182
            return 'xml'
183
        elif atom_allowed and accept == 'application/atom+xml':
184
            return 'atom'
185
    
186
    return 'json'
187

    
188
def api_method(http_method=None, atom_allowed=False):
189
    """Decorator function for views that implement an API method."""
190
    
191
    def decorator(func):
192
        @wraps(func)
193
        def wrapper(request, *args, **kwargs):
194
            try:
195
                request.serialization = request_serialization(request, atom_allowed)
196
                if http_method and request.method != http_method:
197
                    raise BadRequest('Method not allowed.')
198
                
199
                resp = func(request, *args, **kwargs)
200
                if request.serialization == 'xml':
201
                    resp['Content-Type'] = 'application/xml'
202
                elif request.serialization == 'atom':
203
                    resp['Content-Type'] = 'application/atom+xml'
204
                else:
205
                    resp['Content-Type'] = 'application/json'
206
                
207
                return resp
208
            
209
            except Fault, fault:
210
                return render_fault(request, fault)
211
            except BaseException, e:
212
                logging.exception('Unexpected error: %s' % e)
213
                fault = ServiceUnavailable('Unexpected error')
214
                return render_fault(request, fault)
215
        return wrapper
216
    return decorator