2 # Copyright (c) 2011 Greek Research and Technology Network
\r
5 from datetime import timedelta, tzinfo
\r
6 from functools import wraps
\r
7 from random import choice
\r
8 from string import ascii_letters, digits
\r
9 from time import time
\r
10 from traceback import format_exc
\r
11 from wsgiref.handlers import format_date_time
\r
13 from django.conf import settings
\r
14 from django.http import HttpResponse
\r
15 from django.template.loader import render_to_string
\r
16 from django.utils import simplejson as json
\r
18 from pithos.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable
\r
19 #from synnefo.db.models import SynnefoUser, Image, ImageMetadata, VirtualMachine, VirtualMachineMetadata
\r
22 import dateutil.parser
\r
28 # Part of newer Django versions.
\r
30 __D = r'(?P<day>\d{2})'
\r
31 __D2 = r'(?P<day>[ \d]\d)'
\r
32 __M = r'(?P<mon>\w{3})'
\r
33 __Y = r'(?P<year>\d{4})'
\r
34 __Y2 = r'(?P<year>\d{2})'
\r
35 __T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
\r
36 RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))
\r
37 RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))
\r
38 ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))
\r
40 def parse_http_date(date):
\r
42 Parses a date format as specified by HTTP RFC2616 section 3.3.1.
\r
44 The three formats allowed by the RFC are accepted, even if only the first
\r
45 one is still in widespread use.
\r
47 Returns an floating point number expressed in seconds since the epoch, in
\r
50 # emails.Util.parsedate does the job for RFC1123 dates; unfortunately
\r
51 # RFC2616 makes it mandatory to support RFC850 dates too. So we roll
\r
52 # our own RFC-compliant parsing.
\r
53 for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
\r
54 m = regex.match(date)
\r
58 raise ValueError("%r is not in a valid HTTP date format" % date)
\r
60 year = int(m.group('year'))
\r
66 month = MONTHS.index(m.group('mon').lower()) + 1
\r
67 day = int(m.group('day'))
\r
68 hour = int(m.group('hour'))
\r
69 min = int(m.group('min'))
\r
70 sec = int(m.group('sec'))
\r
71 result = datetime.datetime(year, month, day, hour, min, sec)
\r
72 return calendar.timegm(result.utctimetuple())
\r
74 raise ValueError("%r is not a valid date" % date)
\r
76 def parse_http_date_safe(date):
\r
78 Same as parse_http_date, but returns None if the input is invalid.
\r
81 return parse_http_date(date)
\r
85 # Metadata handling.
\r
87 def format_meta_key(k):
\r
88 return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
\r
90 def get_meta(request, prefix):
\r
92 Get all prefix-* request headers in a dict.
\r
93 All underscores are converted to dashes.
\r
95 prefix = 'HTTP_' + prefix.upper().replace('-', '_')
\r
96 return dict([(format_meta_key(k[5:]), v) for k, v in request.META.iteritems() if k.startswith(prefix)])
\r
100 def get_range(request):
\r
102 Parse a Range header from the request.
\r
103 Either returns None, or an (offset, length) tuple.
\r
104 If no offset is defined offset equals 0.
\r
105 If no length is defined length is None.
\r
108 range = request.GET.get('range')
\r
112 range = range.replace(' ', '')
\r
113 if not range.startswith('bytes='):
\r
116 parts = range.split('-')
\r
117 if len(parts) != 2:
\r
120 offset, length = parts
\r
121 if offset == '' and length == '':
\r
126 offset = int(offset)
\r
134 length = int(length)
\r
140 return (offset, length)
\r
142 # def get_vm(server_id):
\r
143 # """Return a VirtualMachine instance or raise ItemNotFound."""
\r
146 # server_id = int(server_id)
\r
147 # return VirtualMachine.objects.get(id=server_id)
\r
148 # except ValueError:
\r
149 # raise BadRequest('Invalid server ID.')
\r
150 # except VirtualMachine.DoesNotExist:
\r
151 # raise ItemNotFound('Server not found.')
\r
153 # def get_vm_meta(server_id, key):
\r
154 # """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
\r
157 # server_id = int(server_id)
\r
158 # return VirtualMachineMetadata.objects.get(meta_key=key, vm=server_id)
\r
159 # except VirtualMachineMetadata.DoesNotExist:
\r
160 # raise ItemNotFound('Metadata key not found.')
\r
162 # def get_image(image_id):
\r
163 # """Return an Image instance or raise ItemNotFound."""
\r
166 # image_id = int(image_id)
\r
167 # return Image.objects.get(id=image_id)
\r
168 # except Image.DoesNotExist:
\r
169 # raise ItemNotFound('Image not found.')
\r
171 # def get_image_meta(image_id, key):
\r
172 # """Return a ImageMetadata instance or raise ItemNotFound."""
\r
175 # image_id = int(image_id)
\r
176 # return ImageMetadata.objects.get(meta_key=key, image=image_id)
\r
177 # except ImageMetadata.DoesNotExist:
\r
178 # raise ItemNotFound('Metadata key not found.')
\r
181 # def get_request_dict(request):
\r
182 # """Returns data sent by the client as a python dict."""
\r
184 # data = request.raw_post_data
\r
185 # if request.META.get('CONTENT_TYPE').startswith('application/json'):
\r
187 # return json.loads(data)
\r
188 # except ValueError:
\r
189 # raise BadRequest('Invalid JSON data.')
\r
191 # raise BadRequest('Unsupported Content-Type.')
\r
193 def update_response_headers(request, response):
\r
194 if request.serialization == 'xml':
\r
195 response['Content-Type'] = 'application/xml; charset=UTF-8'
\r
196 elif request.serialization == 'json':
\r
197 response['Content-Type'] = 'application/json; charset=UTF-8'
\r
199 response['Content-Type'] = 'text/plain; charset=UTF-8'
\r
202 response['Date'] = format_date_time(time())
\r
204 def render_fault(request, fault):
\r
205 if settings.DEBUG or settings.TEST:
\r
206 fault.details = format_exc(fault)
\r
208 # if request.serialization == 'xml':
\r
209 # data = render_to_string('fault.xml', {'fault': fault})
\r
211 # d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}
\r
212 # data = json.dumps(d)
\r
214 # resp = HttpResponse(data, status=fault.code)
\r
215 resp = HttpResponse(status = fault.code)
\r
216 update_response_headers(request, resp)
\r
219 def request_serialization(request, format_allowed=False):
\r
221 Return the serialization format requested.
\r
223 Valid formats are 'text' and 'json', 'xml' if `format_allowed` is True.
\r
226 if not format_allowed:
\r
229 format = request.GET.get('format')
\r
230 if format == 'json':
\r
232 elif format == 'xml':
\r
235 # TODO: Do we care of Accept headers?
\r
236 # for item in request.META.get('HTTP_ACCEPT', '').split(','):
\r
237 # accept, sep, rest = item.strip().partition(';')
\r
238 # if accept == 'application/json':
\r
240 # elif accept == 'application/xml':
\r
245 def api_method(http_method = None, format_allowed = False):
\r
247 Decorator function for views that implement an API method.
\r
250 def decorator(func):
\r
252 def wrapper(request, *args, **kwargs):
\r
254 request.serialization = request_serialization(request, format_allowed)
\r
255 # TODO: Authenticate.
\r
256 # TODO: Return 401/404 when the account is not found.
\r
257 request.user = "test"
\r
258 # TODO: Check parameter sizes.
\r
259 if http_method and request.method != http_method:
\r
260 raise BadRequest('Method not allowed.')
\r
262 resp = func(request, *args, **kwargs)
\r
263 update_response_headers(request, resp)
\r
266 except Fault, fault:
\r
267 return render_fault(request, fault)
\r
268 except BaseException, e:
\r
269 logging.exception('Unexpected error: %s' % e)
\r
270 fault = ServiceUnavailable('Unexpected error')
\r
271 return render_fault(request, fault)
\r