Allow for account/container metadata.
[pithos] / pithos / api / util.py
1 #\r
2 # Copyright (c) 2011 Greek Research and Technology Network\r
3 #\r
4 \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
12 \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
17 \r
18 from pithos.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable\r
19 #from synnefo.db.models import SynnefoUser, Image, ImageMetadata, VirtualMachine, VirtualMachineMetadata\r
20 \r
21 import datetime\r
22 import dateutil.parser\r
23 import logging\r
24 \r
25 import re\r
26 import calendar\r
27 \r
28 # Part of newer Django versions.\r
29 \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
39 \r
40 def parse_http_date(date):\r
41     """\r
42     Parses a date format as specified by HTTP RFC2616 section 3.3.1.\r
43 \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
46 \r
47     Returns an floating point number expressed in seconds since the epoch, in\r
48     UTC.\r
49     """\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
55         if m is not None:\r
56             break\r
57     else:\r
58         raise ValueError("%r is not in a valid HTTP date format" % date)\r
59     try:\r
60         year = int(m.group('year'))\r
61         if year < 100:\r
62             if year < 70:\r
63                 year += 2000\r
64             else:\r
65                 year += 1900\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
73     except Exception:\r
74         raise ValueError("%r is not a valid date" % date)\r
75 \r
76 def parse_http_date_safe(date):\r
77     """\r
78     Same as parse_http_date, but returns None if the input is invalid.\r
79     """\r
80     try:\r
81         return parse_http_date(date)\r
82     except Exception:\r
83         pass\r
84 \r
85 # Metadata handling.\r
86 \r
87 def format_meta_key(k):\r
88     return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])\r
89 \r
90 def get_meta(request, prefix):\r
91     """\r
92     Get all prefix-* request headers in a dict.\r
93     All underscores are converted to dashes.\r
94     """\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
97 \r
98 # Range parsing.\r
99 \r
100 def get_range(request):\r
101     """\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
106     """\r
107     \r
108     range = request.GET.get('range')\r
109     if not range:\r
110         return None\r
111     \r
112     range = range.replace(' ', '')\r
113     if not range.startswith('bytes='):\r
114         return None\r
115     \r
116     parts = range.split('-')\r
117     if len(parts) != 2:\r
118         return None\r
119     \r
120     offset, length = parts\r
121     if offset == '' and length == '':\r
122         return None\r
123     \r
124     if offset != '':\r
125         try:\r
126             offset = int(offset)\r
127         except ValueError:\r
128             return None\r
129     else:\r
130         offset = 0\r
131     \r
132     if length != '':\r
133         try:\r
134             length = int(length)\r
135         except ValueError:\r
136             return None\r
137     else:\r
138         length = None\r
139     \r
140     return (offset, length)\r
141 \r
142 # def get_vm(server_id):\r
143 #     """Return a VirtualMachine instance or raise ItemNotFound."""\r
144 #     \r
145 #     try:\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
152\r
153 # def get_vm_meta(server_id, key):\r
154 #     """Return a VirtualMachineMetadata instance or raise ItemNotFound."""\r
155 #     \r
156 #     try:\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
161\r
162 # def get_image(image_id):\r
163 #     """Return an Image instance or raise ItemNotFound."""\r
164 #     \r
165 #     try:\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
170\r
171 # def get_image_meta(image_id, key):\r
172 #     """Return a ImageMetadata instance or raise ItemNotFound."""\r
173\r
174 #     try:\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
179\r
180\r
181 # def get_request_dict(request):\r
182 #     """Returns data sent by the client as a python dict."""\r
183 #     \r
184 #     data = request.raw_post_data\r
185 #     if request.META.get('CONTENT_TYPE').startswith('application/json'):\r
186 #         try:\r
187 #             return json.loads(data)\r
188 #         except ValueError:\r
189 #             raise BadRequest('Invalid JSON data.')\r
190 #     else:\r
191 #         raise BadRequest('Unsupported Content-Type.')\r
192 \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
198     else:\r
199         response['Content-Type'] = 'text/plain; charset=UTF-8'\r
200 \r
201     if settings.TEST:\r
202         response['Date'] = format_date_time(time())\r
203 \r
204 def render_fault(request, fault):\r
205     if settings.DEBUG or settings.TEST:\r
206         fault.details = format_exc(fault)\r
207     \r
208 #     if request.serialization == 'xml':\r
209 #         data = render_to_string('fault.xml', {'fault': fault})\r
210 #     else:\r
211 #         d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}\r
212 #         data = json.dumps(d)\r
213     \r
214 #     resp = HttpResponse(data, status=fault.code)\r
215     resp = HttpResponse(status = fault.code)\r
216     update_response_headers(request, resp)\r
217     return resp\r
218 \r
219 def request_serialization(request, format_allowed=False):\r
220     """\r
221     Return the serialization format requested.\r
222        \r
223     Valid formats are 'text' and 'json', 'xml' if `format_allowed` is True.\r
224     """\r
225     \r
226     if not format_allowed:\r
227         return 'text'\r
228     \r
229     format = request.GET.get('format')\r
230     if format == 'json':\r
231         return 'json'\r
232     elif format == 'xml':\r
233         return 'xml'\r
234     \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
239 #             return 'json'\r
240 #         elif accept == 'application/xml':\r
241 #             return 'xml'\r
242     \r
243     return 'text'\r
244 \r
245 def api_method(http_method = None, format_allowed = False):\r
246     """\r
247     Decorator function for views that implement an API method.\r
248     """\r
249     \r
250     def decorator(func):\r
251         @wraps(func)\r
252         def wrapper(request, *args, **kwargs):\r
253             try:\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
261                 \r
262                 resp = func(request, *args, **kwargs)\r
263                 update_response_headers(request, resp)\r
264                 return resp\r
265             \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
272         return wrapper\r
273     return decorator\r