Revision d8e50a39 api/util.py

b/api/util.py
2 2
# Copyright (c) 2010 Greek Research and Technology Network
3 3
#
4 4

  
5
from synnefo.api.errors import *
6
from synnefo.db.models import *
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
7 10

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

  
12
from functools import wraps
13
from logging import getLogger
14
from random import choice
15
from string import ascii_letters, digits
16
from traceback import format_exc
17
from xml.etree import ElementTree
18
from xml.parsers.expat import ExpatError
16
from synnefo.api.faults import *
17
from synnefo.db.models import *
19 18

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

  
21
log = getLogger('synnefo.api')
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)
23 33

  
24
def tag_name(e):
25
    ns, sep, name = e.tag.partition('}')
26
    return name if sep else e.tag
27 34

  
28
def xml_to_dict(s):
29
    # XXX Quick and dirty
30
    def _xml_to_dict(e):
31
        root = {}
32
        d = root[tag_name(e)] = dict(e.items())
33
        for child in e.getchildren():
34
            d.update(_xml_to_dict(child))
35
        return root
36
    return _xml_to_dict(ElementTree.fromstring(s.strip()))
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
    except ValueError:
49
        raise BadRequest('Invalid changes-since parameter.')
50
    
51
    now = datetime.datetime.now(UTC())
52
    if since > now:
53
        raise BadRequest('changes-since value set in the future.')
54
    
55
    if now - since > timedelta(seconds=settings.POLL_LIMIT):
56
        raise BadRequest('Too old changes-since value.')
57
    
58
    return since
59
    
60
def random_password(length=8):
61
    pool = ascii_letters + digits
62
    return ''.join(choice(pool) for i in range(length))
63

  
37 64

  
38 65
def get_user():
39 66
    # XXX Placeholder function, everything belongs to a single SynnefoUser for now
......
42 69
    except IndexError:
43 70
        raise Unauthorized
44 71

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

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

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

  
101

  
45 102
def get_request_dict(request):
103
    """Returns data sent by the client as a python dict."""
104
    
46 105
    data = request.raw_post_data
47
    if request.type == 'xml':
48
        try:
49
            return xml_to_dict(data)
50
        except ExpatError:
51
            raise BadRequest
52
    else:
106
    if request.META.get('CONTENT_TYPE') == 'application/json':
53 107
        try:
54 108
            return json.loads(data)
55 109
        except ValueError:
56
            raise BadRequest
57

  
58
def random_password(length=8):
59
    pool = ascii_letters + digits
60
    return ''.join(choice(pool) for i in range(length))
61

  
110
            raise BadRequest('Invalid JSON data.')
111
    else:
112
        raise BadRequest('Unsupported Content-Type.')
62 113

  
63 114
def render_fault(request, fault):
64
    if settings.DEBUG or request.META.get('SERVER_NAME', None) == 'testserver':
115
    if settings.DEBUG or request.META.get('SERVER_NAME') == 'testserver':
65 116
        fault.details = format_exc(fault)
66
    if request.type == 'xml':
67
        mimetype = 'application/xml'
68
        data = render_to_string('fault.xml', dict(fault=fault))
117
    
118
    if request.serialization == 'xml':
119
        data = render_to_string('fault.xml', {'fault': fault})
69 120
    else:
70
        mimetype = 'application/json'
71 121
        d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}
72 122
        data = json.dumps(d)
73
    return HttpResponse(data, mimetype=mimetype, status=fault.code)    
123
    
124
    return HttpResponse(data, status=fault.code)
125

  
126
def request_serialization(request, atom_allowed=False):
127
    """Return the serialization format requested.
128
       
129
       Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
130
    """
131
    
132
    path = request.path
133
    
134
    if path.endswith('.json'):
135
        return 'json'
136
    elif path.endswith('.xml'):
137
        return 'xml'
138
    elif atom_allowed and path.endswith('.atom'):
139
        return 'atom'
140
    
141
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
142
        accept, sep, rest = item.strip().partition(';')
143
        if accept == 'application/json':
144
            return 'json'
145
        elif accept == 'application/xml':
146
            return 'xml'
147
        elif atom_allowed and accept == 'application/atom+xml':
148
            return 'atom'
149
    
150
    return 'json'
74 151

  
75
def api_method(http_method):
152
def api_method(http_method=None, atom_allowed=False):
153
    """Decorator function for views that implement an API method."""
154
    
76 155
    def decorator(func):
77 156
        @wraps(func)
78 157
        def wrapper(request, *args, **kwargs):
79 158
            try:
80
                if request.path.endswith('.json'):
81
                    request.type = 'json'
82
                elif request.path.endswith('.xml'):
83
                    request.type = 'xml'
84
                else:
85
                    request.type = 'json'       # Default response format
86
                    for item in request.META.get('HTTP_ACCEPT', '').split(','):
87
                        accept, sep, rest = item.strip().partition(';')
88
                        if accept == 'application/json':
89
                            break
90
                        elif accept == 'application/xml':
91
                            request.type = 'xml'
92
                            break
93
                
94
                if request.method != http_method:
95
                    raise BadRequest()
159
                request.serialization = request_serialization(request, atom_allowed)
160
                if http_method and request.method != http_method:
161
                    raise BadRequest('Method not allowed.')
96 162
                
97 163
                resp = func(request, *args, **kwargs)
98
                resp['Content-Type'] = 'application/xml' if request.type == 'xml' else 'application/json'
164
                if request.serialization == 'xml':
165
                    resp['Content-Type'] = 'application/xml'
166
                elif request.serialization == 'atom':
167
                    resp['Content-Type'] = 'application/atom+xml'
168
                else:
169
                    resp['Content-Type'] = 'application/json'
170
                
99 171
                return resp
172
            
100 173
            except Fault, fault:
101 174
                return render_fault(request, fault)
102 175
            except BaseException, e:
103
                log.exception('Unexpected error: %s' % e)
104
                fault = ServiceUnavailable()
176
                logging.exception('Unexpected error: %s' % e)
177
                fault = ServiceUnavailable('Unexpected error')
105 178
                return render_fault(request, fault)
106 179
        return wrapper
107 180
    return decorator

Also available in: Unified diff