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 def get_object_meta(request):
\r
87 Get all X-Object-Meta-* headers in a dict.
\r
89 prefix = 'HTTP_X_OBJECT_META_'
\r
90 return dict([(k[len(prefix):].lower(), v) for k, v in request.META.iteritems() if k.startswith(prefix)])
\r
92 def get_range(request):
\r
94 Parse a Range header from the request.
\r
95 Either returns None, or an (offset, length) tuple.
\r
96 If no offset is defined offset equals 0.
\r
97 If no length is defined length is None.
\r
100 range = request.GET.get('range')
\r
104 range = range.replace(' ', '')
\r
105 if not range.startswith('bytes='):
\r
108 parts = range.split('-')
\r
109 if len(parts) != 2:
\r
112 offset, length = parts
\r
113 if offset == '' and length == '':
\r
118 offset = int(offset)
\r
126 length = int(length)
\r
132 return (offset, length)
\r
134 # def get_vm(server_id):
\r
135 # """Return a VirtualMachine instance or raise ItemNotFound."""
\r
138 # server_id = int(server_id)
\r
139 # return VirtualMachine.objects.get(id=server_id)
\r
140 # except ValueError:
\r
141 # raise BadRequest('Invalid server ID.')
\r
142 # except VirtualMachine.DoesNotExist:
\r
143 # raise ItemNotFound('Server not found.')
\r
145 # def get_vm_meta(server_id, key):
\r
146 # """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
\r
149 # server_id = int(server_id)
\r
150 # return VirtualMachineMetadata.objects.get(meta_key=key, vm=server_id)
\r
151 # except VirtualMachineMetadata.DoesNotExist:
\r
152 # raise ItemNotFound('Metadata key not found.')
\r
154 # def get_image(image_id):
\r
155 # """Return an Image instance or raise ItemNotFound."""
\r
158 # image_id = int(image_id)
\r
159 # return Image.objects.get(id=image_id)
\r
160 # except Image.DoesNotExist:
\r
161 # raise ItemNotFound('Image not found.')
\r
163 # def get_image_meta(image_id, key):
\r
164 # """Return a ImageMetadata instance or raise ItemNotFound."""
\r
167 # image_id = int(image_id)
\r
168 # return ImageMetadata.objects.get(meta_key=key, image=image_id)
\r
169 # except ImageMetadata.DoesNotExist:
\r
170 # raise ItemNotFound('Metadata key not found.')
\r
173 # def get_request_dict(request):
\r
174 # """Returns data sent by the client as a python dict."""
\r
176 # data = request.raw_post_data
\r
177 # if request.META.get('CONTENT_TYPE').startswith('application/json'):
\r
179 # return json.loads(data)
\r
180 # except ValueError:
\r
181 # raise BadRequest('Invalid JSON data.')
\r
183 # raise BadRequest('Unsupported Content-Type.')
\r
185 def update_response_headers(request, response):
\r
186 if request.serialization == 'xml':
\r
187 response['Content-Type'] = 'application/xml; charset=UTF-8'
\r
188 elif request.serialization == 'json':
\r
189 response['Content-Type'] = 'application/json; charset=UTF-8'
\r
191 response['Content-Type'] = 'text/plain; charset=UTF-8'
\r
194 response['Date'] = format_date_time(time())
\r
196 def render_fault(request, fault):
\r
197 if settings.DEBUG or settings.TEST:
\r
198 fault.details = format_exc(fault)
\r
200 # if request.serialization == 'xml':
\r
201 # data = render_to_string('fault.xml', {'fault': fault})
\r
203 # d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}
\r
204 # data = json.dumps(d)
\r
206 # resp = HttpResponse(data, status=fault.code)
\r
207 resp = HttpResponse(status = fault.code)
\r
208 update_response_headers(request, resp)
\r
211 def request_serialization(request, format_allowed=False):
\r
213 Return the serialization format requested.
\r
215 Valid formats are 'text' and 'json', 'xml' if `format_allowed` is True.
\r
218 if not format_allowed:
\r
221 format = request.GET.get('format')
\r
222 if format == 'json':
\r
224 elif format == 'xml':
\r
227 # TODO: Do we care of Accept headers?
\r
228 # for item in request.META.get('HTTP_ACCEPT', '').split(','):
\r
229 # accept, sep, rest = item.strip().partition(';')
\r
230 # if accept == 'application/json':
\r
232 # elif accept == 'application/xml':
\r
237 def api_method(http_method = None, format_allowed = False):
\r
239 Decorator function for views that implement an API method.
\r
242 def decorator(func):
\r
244 def wrapper(request, *args, **kwargs):
\r
246 request.serialization = request_serialization(request, format_allowed)
\r
247 # TODO: Authenticate.
\r
248 # TODO: Return 401/404 when the account is not found.
\r
249 request.user = "test"
\r
250 # TODO: Check parameter sizes.
\r
251 if http_method and request.method != http_method:
\r
252 raise BadRequest('Method not allowed.')
\r
254 resp = func(request, *args, **kwargs)
\r
255 update_response_headers(request, resp)
\r
258 except Fault, fault:
\r
259 return render_fault(request, fault)
\r
260 except BaseException, e:
\r
261 logging.exception('Unexpected error: %s' % e)
\r
262 fault = ServiceUnavailable('Unexpected error')
\r
263 return render_fault(request, fault)
\r