Statistics
| Branch: | Tag: | Revision:

root / api / util.py @ 28f97815

History | View | Annotate | Download (8.1 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,
23
                                ItemNotFound, ServiceUnavailable, Unauthorized)
24
from synnefo.db.models import (SynnefoUser, Flavor, Image, ImageMetadata,
25
                                VirtualMachine, VirtualMachineMetadata,
26
                                Network, NetworkInterface)
27

    
28

    
29
class UTC(tzinfo):
30
    def utcoffset(self, dt):
31
        return timedelta(0)
32

    
33
    def tzname(self, dt):
34
        return 'UTC'
35

    
36
    def dst(self, dt):
37
        return timedelta(0)
38

    
39

    
40
def isoformat(d):
41
    """Return an ISO8601 date string that includes a timezone."""
42

    
43
    return d.replace(tzinfo=UTC()).isoformat()
44

    
45
def isoparse(s):
46
    """Parse an ISO8601 date string into a datetime object."""
47

    
48
    if not s:
49
        return None
50

    
51
    try:
52
        since = dateutil.parser.parse(s)
53
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
54
    except ValueError:
55
        raise BadRequest('Invalid changes-since parameter.')
56

    
57
    now = datetime.datetime.now()
58
    if utc_since > now:
59
        raise BadRequest('changes-since value set in the future.')
60

    
61
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
62
        raise BadRequest('Too old changes-since value.')
63

    
64
    return utc_since
65

    
66
def random_password(length=8):
67
    pool = ascii_letters + digits
68
    return ''.join(choice(pool) for i in range(length))
69

    
70

    
71
def get_vm(server_id, owner):
72
    """Return a VirtualMachine instance or raise ItemNotFound."""
73

    
74
    try:
75
        server_id = int(server_id)
76
        return VirtualMachine.objects.get(id=server_id, owner=owner)
77
    except ValueError:
78
        raise BadRequest('Invalid server ID.')
79
    except VirtualMachine.DoesNotExist:
80
        raise ItemNotFound('Server not found.')
81

    
82
def get_vm_meta(vm, key):
83
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
84

    
85
    try:
86
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
87
    except VirtualMachineMetadata.DoesNotExist:
88
        raise ItemNotFound('Metadata key not found.')
89

    
90
def get_image(image_id, owner):
91
    """Return an Image instance or raise ItemNotFound."""
92

    
93
    try:
94
        image_id = int(image_id)
95
        return Image.objects.get(id=image_id, owner=owner)
96
    except ValueError:
97
        raise BadRequest('Invalid image ID.')
98
    except Image.DoesNotExist:
99
        raise ItemNotFound('Image not found.')
100

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

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

    
109
def get_flavor(flavor_id):
110
    """Return a Flavor instance or raise ItemNotFound."""
111

    
112
    try:
113
        flavor_id = int(flavor_id)
114
        return Flavor.objects.get(id=flavor_id)
115
    except ValueError:
116
        raise BadRequest('Invalid flavor ID.')
117
    except Flavor.DoesNotExist:
118
        raise ItemNotFound('Flavor not found.')
119

    
120
def get_network(network_id, owner):
121
    """Return a Network instance or raise ItemNotFound."""
122

    
123
    try:
124
        if network_id == 'public':
125
            return Network.objects.get(public=True)
126
        else:
127
            network_id = int(network_id)
128
            return Network.objects.get(id=network_id, owner=owner)
129
    except ValueError:
130
        raise BadRequest('Invalid network ID.')
131
    except Network.DoesNotExist:
132
        raise ItemNotFound('Network not found.')
133

    
134
def get_nic(machine, network):
135
    try:
136
        return NetworkInterface.objects.get(machine=machine, network=network)
137
    except NetworkInterface.DoesNotExist:
138
        raise ItemNotFound('Server not connected to this network.')
139

    
140

    
141
def get_request_dict(request):
142
    """Returns data sent by the client as a python dict."""
143

    
144
    data = request.raw_post_data
145
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
146
        try:
147
            return json.loads(data)
148
        except ValueError:
149
            raise BadRequest('Invalid JSON data.')
150
    else:
151
        raise BadRequest('Unsupported Content-Type.')
152

    
153
def update_response_headers(request, response):
154
    if request.serialization == 'xml':
155
        response['Content-Type'] = 'application/xml'
156
    elif request.serialization == 'atom':
157
        response['Content-Type'] = 'application/atom+xml'
158
    else:
159
        response['Content-Type'] = 'application/json'
160

    
161
    if settings.TEST:
162
        response['Date'] = format_date_time(time())
163

    
164
def render_metadata(request, metadata, use_values=False, status=200):
165
    if request.serialization == 'xml':
166
        data = render_to_string('metadata.xml', {'metadata': metadata})
167
    else:
168
        if use_values:
169
            d = {'metadata': {'values': metadata}}
170
        else:
171
            d = {'metadata': metadata}
172
        data = json.dumps(d)
173
    return HttpResponse(data, status=status)
174

    
175
def render_meta(request, meta, status=200):
176
    if request.serialization == 'xml':
177
        data = render_to_string('meta.xml', {'meta': meta})
178
    else:
179
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
180
    return HttpResponse(data, status=status)
181

    
182
def render_fault(request, fault):
183
    if settings.DEBUG or settings.TEST:
184
        fault.details = format_exc(fault)
185

    
186
    if request.serialization == 'xml':
187
        data = render_to_string('fault.xml', {'fault': fault})
188
    else:
189
        d = {fault.name: {
190
                'code': fault.code,
191
                'message': fault.message,
192
                'details': fault.details}}
193
        data = json.dumps(d)
194

    
195
    resp = HttpResponse(data, status=fault.code)
196
    update_response_headers(request, resp)
197
    return resp
198

    
199

    
200
def request_serialization(request, atom_allowed=False):
201
    """Return the serialization format requested.
202

203
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
204
    """
205

    
206
    path = request.path
207

    
208
    if path.endswith('.json'):
209
        return 'json'
210
    elif path.endswith('.xml'):
211
        return 'xml'
212
    elif atom_allowed and path.endswith('.atom'):
213
        return 'atom'
214

    
215
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
216
        accept, sep, rest = item.strip().partition(';')
217
        if accept == 'application/json':
218
            return 'json'
219
        elif accept == 'application/xml':
220
            return 'xml'
221
        elif atom_allowed and accept == 'application/atom+xml':
222
            return 'atom'
223

    
224
    return 'json'
225

    
226
def api_method(http_method=None, atom_allowed=False):
227
    """Decorator function for views that implement an API method."""
228

    
229
    def decorator(func):
230
        @wraps(func)
231
        def wrapper(request, *args, **kwargs):
232
            try:
233
                request.serialization = request_serialization(
234
                    request,
235
                    atom_allowed)
236
                if not request.user:
237
                    raise Unauthorized('No user found.')
238
                if http_method and request.method != http_method:
239
                    raise BadRequest('Method not allowed.')
240

    
241
                resp = func(request, *args, **kwargs)
242
                update_response_headers(request, resp)
243
                return resp
244
            except VirtualMachine.DeletedError:
245
                fault = BadRequest('Server has been deleted.')
246
                return render_fault(request, fault)
247
            except VirtualMachine.BuildingError:
248
                fault = BuildInProgress('Server is being built.')
249
                return render_fault(request, fault)
250
            except Fault, fault:
251
                return render_fault(request, fault)
252
            except BaseException, e:
253
                logging.exception('Unexpected error: %s', e)
254
                fault = ServiceUnavailable('Unexpected error.')
255
                return render_fault(request, fault)
256
        return wrapper
257
    return decorator