Statistics
| Branch: | Tag: | Revision:

root / api / util.py @ f4fe8796

History | View | Annotate | Download (10.5 kB)

1 adee02b8 Giorgos Verigakis
# Copyright 2011 GRNET S.A. All rights reserved.
2 d1387ed7 Christodoulos Psaltis
#
3 adee02b8 Giorgos Verigakis
# Redistribution and use in source and binary forms, with or
4 adee02b8 Giorgos Verigakis
# without modification, are permitted provided that the following
5 adee02b8 Giorgos Verigakis
# conditions are met:
6 d1387ed7 Christodoulos Psaltis
#
7 adee02b8 Giorgos Verigakis
#   1. Redistributions of source code must retain the above
8 adee02b8 Giorgos Verigakis
#      copyright notice, this list of conditions and the following
9 adee02b8 Giorgos Verigakis
#      disclaimer.
10 d1387ed7 Christodoulos Psaltis
#
11 adee02b8 Giorgos Verigakis
#   2. Redistributions in binary form must reproduce the above
12 adee02b8 Giorgos Verigakis
#      copyright notice, this list of conditions and the following
13 adee02b8 Giorgos Verigakis
#      disclaimer in the documentation and/or other materials
14 adee02b8 Giorgos Verigakis
#      provided with the distribution.
15 d1387ed7 Christodoulos Psaltis
#
16 adee02b8 Giorgos Verigakis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 adee02b8 Giorgos Verigakis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 adee02b8 Giorgos Verigakis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 adee02b8 Giorgos Verigakis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 adee02b8 Giorgos Verigakis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 adee02b8 Giorgos Verigakis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 adee02b8 Giorgos Verigakis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 adee02b8 Giorgos Verigakis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 adee02b8 Giorgos Verigakis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 adee02b8 Giorgos Verigakis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 adee02b8 Giorgos Verigakis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 adee02b8 Giorgos Verigakis
# POSSIBILITY OF SUCH DAMAGE.
28 d1387ed7 Christodoulos Psaltis
#
29 adee02b8 Giorgos Verigakis
# The views and conclusions contained in the software and
30 adee02b8 Giorgos Verigakis
# documentation are those of the authors and should not be
31 adee02b8 Giorgos Verigakis
# interpreted as representing official policies, either expressed
32 adee02b8 Giorgos Verigakis
# or implied, of GRNET S.A.
33 7e2f9d4b Giorgos Verigakis
34 c738c935 Giorgos Verigakis
import datetime
35 c738c935 Giorgos Verigakis
import dateutil.parser
36 c738c935 Giorgos Verigakis
37 c738c935 Giorgos Verigakis
from base64 import b64encode
38 d8e50a39 Giorgos Verigakis
from datetime import timedelta, tzinfo
39 d8e50a39 Giorgos Verigakis
from functools import wraps
40 c738c935 Giorgos Verigakis
from hashlib import sha256
41 d8e50a39 Giorgos Verigakis
from random import choice
42 d8e50a39 Giorgos Verigakis
from string import ascii_letters, digits
43 8b01f7f3 Giorgos Verigakis
from time import time
44 d8e50a39 Giorgos Verigakis
from traceback import format_exc
45 8b01f7f3 Giorgos Verigakis
from wsgiref.handlers import format_date_time
46 7e2f9d4b Giorgos Verigakis
47 c738c935 Giorgos Verigakis
from Crypto.Cipher import AES
48 529178b1 Giorgos Verigakis
49 d8e50a39 Giorgos Verigakis
from django.conf import settings
50 7e2f9d4b Giorgos Verigakis
from django.http import HttpResponse
51 7e2f9d4b Giorgos Verigakis
from django.template.loader import render_to_string
52 29a59bc1 Giorgos Verigakis
from django.utils import simplejson as json
53 7e2f9d4b Giorgos Verigakis
54 b36f78fa Giorgos Verigakis
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress,
55 b36f78fa Giorgos Verigakis
                                ItemNotFound, ServiceUnavailable, Unauthorized)
56 529178b1 Giorgos Verigakis
from synnefo.db.models import (SynnefoUser, Flavor, Image, ImageMetadata,
57 d44c236b Giorgos Verigakis
                                VirtualMachine, VirtualMachineMetadata,
58 d44c236b Giorgos Verigakis
                                Network, NetworkInterface)
59 b9b28a61 Georgios Gousios
from synnefo.logic import log
60 7e2f9d4b Giorgos Verigakis
61 d8e50a39 Giorgos Verigakis
class UTC(tzinfo):
62 d8e50a39 Giorgos Verigakis
    def utcoffset(self, dt):
63 d8e50a39 Giorgos Verigakis
        return timedelta(0)
64 aa197ee4 Vangelis Koukis
65 d8e50a39 Giorgos Verigakis
    def tzname(self, dt):
66 d8e50a39 Giorgos Verigakis
        return 'UTC'
67 aa197ee4 Vangelis Koukis
68 d8e50a39 Giorgos Verigakis
    def dst(self, dt):
69 d8e50a39 Giorgos Verigakis
        return timedelta(0)
70 7e2f9d4b Giorgos Verigakis
71 7e2f9d4b Giorgos Verigakis
72 d8e50a39 Giorgos Verigakis
def isoformat(d):
73 529178b1 Giorgos Verigakis
    """Return an ISO8601 date string that includes a timezone."""
74 aa197ee4 Vangelis Koukis
75 d8e50a39 Giorgos Verigakis
    return d.replace(tzinfo=UTC()).isoformat()
76 d8e50a39 Giorgos Verigakis
77 d8e50a39 Giorgos Verigakis
def isoparse(s):
78 d8e50a39 Giorgos Verigakis
    """Parse an ISO8601 date string into a datetime object."""
79 aa197ee4 Vangelis Koukis
80 d8e50a39 Giorgos Verigakis
    if not s:
81 d8e50a39 Giorgos Verigakis
        return None
82 aa197ee4 Vangelis Koukis
83 d8e50a39 Giorgos Verigakis
    try:
84 d8e50a39 Giorgos Verigakis
        since = dateutil.parser.parse(s)
85 e87d30f3 Giorgos Verigakis
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
86 d8e50a39 Giorgos Verigakis
    except ValueError:
87 d8e50a39 Giorgos Verigakis
        raise BadRequest('Invalid changes-since parameter.')
88 aa197ee4 Vangelis Koukis
89 e87d30f3 Giorgos Verigakis
    now = datetime.datetime.now()
90 e87d30f3 Giorgos Verigakis
    if utc_since > now:
91 d8e50a39 Giorgos Verigakis
        raise BadRequest('changes-since value set in the future.')
92 aa197ee4 Vangelis Koukis
93 e87d30f3 Giorgos Verigakis
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
94 d8e50a39 Giorgos Verigakis
        raise BadRequest('Too old changes-since value.')
95 aa197ee4 Vangelis Koukis
96 0140e54b Vangelis Koukis
    return utc_since
97 aa197ee4 Vangelis Koukis
98 d8e50a39 Giorgos Verigakis
def random_password(length=8):
99 d8e50a39 Giorgos Verigakis
    pool = ascii_letters + digits
100 d8e50a39 Giorgos Verigakis
    return ''.join(choice(pool) for i in range(length))
101 d8e50a39 Giorgos Verigakis
102 c738c935 Giorgos Verigakis
def zeropad(s):
103 c738c935 Giorgos Verigakis
    """Add zeros at the end of a string in order to make its length
104 c738c935 Giorgos Verigakis
       a multiple of 16."""
105 d1387ed7 Christodoulos Psaltis
106 c738c935 Giorgos Verigakis
    npad = 16 - len(s) % 16
107 c738c935 Giorgos Verigakis
    return s + '\x00' * npad
108 c738c935 Giorgos Verigakis
109 c738c935 Giorgos Verigakis
def encrypt(plaintext):
110 c738c935 Giorgos Verigakis
    # Make sure key is 32 bytes long
111 c738c935 Giorgos Verigakis
    key = sha256(settings.SECRET_KEY).digest()
112 d1387ed7 Christodoulos Psaltis
113 c738c935 Giorgos Verigakis
    aes = AES.new(key)
114 c738c935 Giorgos Verigakis
    enc = aes.encrypt(zeropad(plaintext))
115 c738c935 Giorgos Verigakis
    return b64encode(enc)
116 c738c935 Giorgos Verigakis
117 7e2f9d4b Giorgos Verigakis
118 40777cc8 Giorgos Verigakis
def get_vm(server_id, owner):
119 d8e50a39 Giorgos Verigakis
    """Return a VirtualMachine instance or raise ItemNotFound."""
120 aa197ee4 Vangelis Koukis
121 d8e50a39 Giorgos Verigakis
    try:
122 d8e50a39 Giorgos Verigakis
        server_id = int(server_id)
123 40777cc8 Giorgos Verigakis
        return VirtualMachine.objects.get(id=server_id, owner=owner)
124 d8e50a39 Giorgos Verigakis
    except ValueError:
125 d8e50a39 Giorgos Verigakis
        raise BadRequest('Invalid server ID.')
126 d8e50a39 Giorgos Verigakis
    except VirtualMachine.DoesNotExist:
127 d8e50a39 Giorgos Verigakis
        raise ItemNotFound('Server not found.')
128 d8e50a39 Giorgos Verigakis
129 40777cc8 Giorgos Verigakis
def get_vm_meta(vm, key):
130 d8e50a39 Giorgos Verigakis
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
131 aa197ee4 Vangelis Koukis
132 d8e50a39 Giorgos Verigakis
    try:
133 40777cc8 Giorgos Verigakis
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
134 d8e50a39 Giorgos Verigakis
    except VirtualMachineMetadata.DoesNotExist:
135 d8e50a39 Giorgos Verigakis
        raise ItemNotFound('Metadata key not found.')
136 d8e50a39 Giorgos Verigakis
137 40777cc8 Giorgos Verigakis
def get_image(image_id, owner):
138 d8e50a39 Giorgos Verigakis
    """Return an Image instance or raise ItemNotFound."""
139 aa197ee4 Vangelis Koukis
140 d8e50a39 Giorgos Verigakis
    try:
141 d8e50a39 Giorgos Verigakis
        image_id = int(image_id)
142 75768d0e Giorgos Verigakis
        image = Image.objects.get(id=image_id)
143 75768d0e Giorgos Verigakis
        if not image.public and image.owner != owner:
144 75768d0e Giorgos Verigakis
            raise ItemNotFound('Image not found.')
145 75768d0e Giorgos Verigakis
        return image
146 40777cc8 Giorgos Verigakis
    except ValueError:
147 40777cc8 Giorgos Verigakis
        raise BadRequest('Invalid image ID.')
148 d8e50a39 Giorgos Verigakis
    except Image.DoesNotExist:
149 d8e50a39 Giorgos Verigakis
        raise ItemNotFound('Image not found.')
150 d8e50a39 Giorgos Verigakis
151 40777cc8 Giorgos Verigakis
def get_image_meta(image, key):
152 432fc8c3 Giorgos Verigakis
    """Return a ImageMetadata instance or raise ItemNotFound."""
153 432fc8c3 Giorgos Verigakis
154 432fc8c3 Giorgos Verigakis
    try:
155 40777cc8 Giorgos Verigakis
        return ImageMetadata.objects.get(meta_key=key, image=image)
156 432fc8c3 Giorgos Verigakis
    except ImageMetadata.DoesNotExist:
157 432fc8c3 Giorgos Verigakis
        raise ItemNotFound('Metadata key not found.')
158 432fc8c3 Giorgos Verigakis
159 529178b1 Giorgos Verigakis
def get_flavor(flavor_id):
160 529178b1 Giorgos Verigakis
    """Return a Flavor instance or raise ItemNotFound."""
161 aa197ee4 Vangelis Koukis
162 529178b1 Giorgos Verigakis
    try:
163 529178b1 Giorgos Verigakis
        flavor_id = int(flavor_id)
164 529178b1 Giorgos Verigakis
        return Flavor.objects.get(id=flavor_id)
165 40777cc8 Giorgos Verigakis
    except ValueError:
166 40777cc8 Giorgos Verigakis
        raise BadRequest('Invalid flavor ID.')
167 529178b1 Giorgos Verigakis
    except Flavor.DoesNotExist:
168 529178b1 Giorgos Verigakis
        raise ItemNotFound('Flavor not found.')
169 d8e50a39 Giorgos Verigakis
170 b19653d1 Giorgos Verigakis
def get_network(network_id, owner):
171 e2ee7808 Giorgos Verigakis
    """Return a Network instance or raise ItemNotFound."""
172 aa197ee4 Vangelis Koukis
173 e2ee7808 Giorgos Verigakis
    try:
174 d44c236b Giorgos Verigakis
        if network_id == 'public':
175 207b70d5 Giorgos Verigakis
            return Network.objects.get(public=True)
176 207b70d5 Giorgos Verigakis
        else:
177 207b70d5 Giorgos Verigakis
            network_id = int(network_id)
178 207b70d5 Giorgos Verigakis
            return Network.objects.get(id=network_id, owner=owner)
179 e2ee7808 Giorgos Verigakis
    except ValueError:
180 b19653d1 Giorgos Verigakis
        raise BadRequest('Invalid network ID.')
181 13b954b0 Giorgos Verigakis
    except Network.DoesNotExist:
182 e2ee7808 Giorgos Verigakis
        raise ItemNotFound('Network not found.')
183 e2ee7808 Giorgos Verigakis
184 d44c236b Giorgos Verigakis
def get_nic(machine, network):
185 d44c236b Giorgos Verigakis
    try:
186 d44c236b Giorgos Verigakis
        return NetworkInterface.objects.get(machine=machine, network=network)
187 d44c236b Giorgos Verigakis
    except NetworkInterface.DoesNotExist:
188 d44c236b Giorgos Verigakis
        raise ItemNotFound('Server not connected to this network.')
189 d44c236b Giorgos Verigakis
190 e2ee7808 Giorgos Verigakis
191 7e2f9d4b Giorgos Verigakis
def get_request_dict(request):
192 d8e50a39 Giorgos Verigakis
    """Returns data sent by the client as a python dict."""
193 aa197ee4 Vangelis Koukis
194 7e2f9d4b Giorgos Verigakis
    data = request.raw_post_data
195 7e45ddef Dimitris Moraitis
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
196 7e2f9d4b Giorgos Verigakis
        try:
197 7e2f9d4b Giorgos Verigakis
            return json.loads(data)
198 7e2f9d4b Giorgos Verigakis
        except ValueError:
199 d8e50a39 Giorgos Verigakis
            raise BadRequest('Invalid JSON data.')
200 d8e50a39 Giorgos Verigakis
    else:
201 d8e50a39 Giorgos Verigakis
        raise BadRequest('Unsupported Content-Type.')
202 7e2f9d4b Giorgos Verigakis
203 8b01f7f3 Giorgos Verigakis
def update_response_headers(request, response):
204 8b01f7f3 Giorgos Verigakis
    if request.serialization == 'xml':
205 8b01f7f3 Giorgos Verigakis
        response['Content-Type'] = 'application/xml'
206 8b01f7f3 Giorgos Verigakis
    elif request.serialization == 'atom':
207 8b01f7f3 Giorgos Verigakis
        response['Content-Type'] = 'application/atom+xml'
208 8b01f7f3 Giorgos Verigakis
    else:
209 8b01f7f3 Giorgos Verigakis
        response['Content-Type'] = 'application/json'
210 aa197ee4 Vangelis Koukis
211 4cf8adf8 Vangelis Koukis
    if settings.TEST:
212 cdb65551 Giorgos Verigakis
        response['Date'] = format_date_time(time())
213 8b01f7f3 Giorgos Verigakis
214 432fc8c3 Giorgos Verigakis
def render_metadata(request, metadata, use_values=False, status=200):
215 432fc8c3 Giorgos Verigakis
    if request.serialization == 'xml':
216 432fc8c3 Giorgos Verigakis
        data = render_to_string('metadata.xml', {'metadata': metadata})
217 432fc8c3 Giorgos Verigakis
    else:
218 b36f78fa Giorgos Verigakis
        if use_values:
219 b36f78fa Giorgos Verigakis
            d = {'metadata': {'values': metadata}}
220 b36f78fa Giorgos Verigakis
        else:
221 b36f78fa Giorgos Verigakis
            d = {'metadata': metadata}
222 432fc8c3 Giorgos Verigakis
        data = json.dumps(d)
223 432fc8c3 Giorgos Verigakis
    return HttpResponse(data, status=status)
224 432fc8c3 Giorgos Verigakis
225 432fc8c3 Giorgos Verigakis
def render_meta(request, meta, status=200):
226 432fc8c3 Giorgos Verigakis
    if request.serialization == 'xml':
227 432fc8c3 Giorgos Verigakis
        data = render_to_string('meta.xml', {'meta': meta})
228 432fc8c3 Giorgos Verigakis
    else:
229 432fc8c3 Giorgos Verigakis
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
230 432fc8c3 Giorgos Verigakis
    return HttpResponse(data, status=status)
231 432fc8c3 Giorgos Verigakis
232 c36934a7 Giorgos Verigakis
def render_fault(request, fault):
233 4cf8adf8 Vangelis Koukis
    if settings.DEBUG or settings.TEST:
234 7e2f9d4b Giorgos Verigakis
        fault.details = format_exc(fault)
235 aa197ee4 Vangelis Koukis
236 d8e50a39 Giorgos Verigakis
    if request.serialization == 'xml':
237 d8e50a39 Giorgos Verigakis
        data = render_to_string('fault.xml', {'fault': fault})
238 7e2f9d4b Giorgos Verigakis
    else:
239 b36f78fa Giorgos Verigakis
        d = {fault.name: {
240 b36f78fa Giorgos Verigakis
                'code': fault.code,
241 b36f78fa Giorgos Verigakis
                'message': fault.message,
242 b36f78fa Giorgos Verigakis
                'details': fault.details}}
243 7e2f9d4b Giorgos Verigakis
        data = json.dumps(d)
244 aa197ee4 Vangelis Koukis
245 f0656db1 Giorgos Verigakis
    resp = HttpResponse(data, status=fault.code)
246 8b01f7f3 Giorgos Verigakis
    update_response_headers(request, resp)
247 f0656db1 Giorgos Verigakis
    return resp
248 d8e50a39 Giorgos Verigakis
249 432fc8c3 Giorgos Verigakis
250 d8e50a39 Giorgos Verigakis
def request_serialization(request, atom_allowed=False):
251 d8e50a39 Giorgos Verigakis
    """Return the serialization format requested.
252 aa197ee4 Vangelis Koukis

253 529178b1 Giorgos Verigakis
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
254 d8e50a39 Giorgos Verigakis
    """
255 aa197ee4 Vangelis Koukis
256 d8e50a39 Giorgos Verigakis
    path = request.path
257 aa197ee4 Vangelis Koukis
258 d8e50a39 Giorgos Verigakis
    if path.endswith('.json'):
259 d8e50a39 Giorgos Verigakis
        return 'json'
260 d8e50a39 Giorgos Verigakis
    elif path.endswith('.xml'):
261 d8e50a39 Giorgos Verigakis
        return 'xml'
262 d8e50a39 Giorgos Verigakis
    elif atom_allowed and path.endswith('.atom'):
263 d8e50a39 Giorgos Verigakis
        return 'atom'
264 aa197ee4 Vangelis Koukis
265 d8e50a39 Giorgos Verigakis
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
266 d8e50a39 Giorgos Verigakis
        accept, sep, rest = item.strip().partition(';')
267 d8e50a39 Giorgos Verigakis
        if accept == 'application/json':
268 d8e50a39 Giorgos Verigakis
            return 'json'
269 d8e50a39 Giorgos Verigakis
        elif accept == 'application/xml':
270 d8e50a39 Giorgos Verigakis
            return 'xml'
271 d8e50a39 Giorgos Verigakis
        elif atom_allowed and accept == 'application/atom+xml':
272 d8e50a39 Giorgos Verigakis
            return 'atom'
273 aa197ee4 Vangelis Koukis
274 d8e50a39 Giorgos Verigakis
    return 'json'
275 7e2f9d4b Giorgos Verigakis
276 d8e50a39 Giorgos Verigakis
def api_method(http_method=None, atom_allowed=False):
277 d8e50a39 Giorgos Verigakis
    """Decorator function for views that implement an API method."""
278 aa197ee4 Vangelis Koukis
279 c36934a7 Giorgos Verigakis
    def decorator(func):
280 c36934a7 Giorgos Verigakis
        @wraps(func)
281 c36934a7 Giorgos Verigakis
        def wrapper(request, *args, **kwargs):
282 bc923fb7 Giorgos Verigakis
            u = request.user.uniq if request.user else ''
283 b9b28a61 Georgios Gousios
            logger = log.get_logger("synnefo.api")
284 b9b28a61 Georgios Gousios
            logger.debug("%s <%s>" % (request.path, u))
285 c36934a7 Giorgos Verigakis
            try:
286 b9b28a61 Georgios Gousios
287 b36f78fa Giorgos Verigakis
                request.serialization = request_serialization(
288 b36f78fa Giorgos Verigakis
                    request,
289 b36f78fa Giorgos Verigakis
                    atom_allowed)
290 27f0e60e Georgios Gousios
                if not request.method == 'GET':
291 27f0e60e Georgios Gousios
                    if 'readonly' in request.__dict__ and \
292 27f0e60e Georgios Gousios
                       request.readonly == True:
293 27f0e60e Georgios Gousios
                        raise BadRequest('Method not allowed')
294 40777cc8 Giorgos Verigakis
                if not request.user:
295 40777cc8 Giorgos Verigakis
                    raise Unauthorized('No user found.')
296 d8e50a39 Giorgos Verigakis
                if http_method and request.method != http_method:
297 d8e50a39 Giorgos Verigakis
                    raise BadRequest('Method not allowed.')
298 aa197ee4 Vangelis Koukis
299 c36934a7 Giorgos Verigakis
                resp = func(request, *args, **kwargs)
300 8b01f7f3 Giorgos Verigakis
                update_response_headers(request, resp)
301 c36934a7 Giorgos Verigakis
                return resp
302 5231a38a Giorgos Verigakis
            except VirtualMachine.DeletedError:
303 5231a38a Giorgos Verigakis
                fault = BadRequest('Server has been deleted.')
304 5231a38a Giorgos Verigakis
                return render_fault(request, fault)
305 5231a38a Giorgos Verigakis
            except VirtualMachine.BuildingError:
306 5231a38a Giorgos Verigakis
                fault = BuildInProgress('Server is being built.')
307 5231a38a Giorgos Verigakis
                return render_fault(request, fault)
308 c36934a7 Giorgos Verigakis
            except Fault, fault:
309 c36934a7 Giorgos Verigakis
                return render_fault(request, fault)
310 a62a4d20 Giorgos Verigakis
            except BaseException, e:
311 f2bdb9ab Georgios Gousios
                logger.exception('Unexpected error')
312 529178b1 Giorgos Verigakis
                fault = ServiceUnavailable('Unexpected error.')
313 c36934a7 Giorgos Verigakis
                return render_fault(request, fault)
314 c36934a7 Giorgos Verigakis
        return wrapper
315 c36934a7 Giorgos Verigakis
    return decorator