Statistics
| Branch: | Tag: | Revision:

root / api / util.py @ 75768d0e

History | View | Annotate | Download (9.6 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
# 
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
# 
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
# 
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
# 
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from datetime import timedelta, tzinfo
35
from functools import wraps
36
from random import choice
37
from string import ascii_letters, digits
38
from time import time
39
from traceback import format_exc
40
from wsgiref.handlers import format_date_time
41

    
42
import datetime
43
import dateutil.parser
44
import logging
45

    
46
from django.conf import settings
47
from django.http import HttpResponse
48
from django.template.loader import render_to_string
49
from django.utils import simplejson as json
50

    
51
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress,
52
                                ItemNotFound, ServiceUnavailable, Unauthorized)
53
from synnefo.db.models import (SynnefoUser, Flavor, Image, ImageMetadata,
54
                                VirtualMachine, VirtualMachineMetadata,
55
                                Network, NetworkInterface)
56

    
57

    
58
class UTC(tzinfo):
59
    def utcoffset(self, dt):
60
        return timedelta(0)
61

    
62
    def tzname(self, dt):
63
        return 'UTC'
64

    
65
    def dst(self, dt):
66
        return timedelta(0)
67

    
68

    
69
def isoformat(d):
70
    """Return an ISO8601 date string that includes a timezone."""
71

    
72
    return d.replace(tzinfo=UTC()).isoformat()
73

    
74
def isoparse(s):
75
    """Parse an ISO8601 date string into a datetime object."""
76

    
77
    if not s:
78
        return None
79

    
80
    try:
81
        since = dateutil.parser.parse(s)
82
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
83
    except ValueError:
84
        raise BadRequest('Invalid changes-since parameter.')
85

    
86
    now = datetime.datetime.now()
87
    if utc_since > now:
88
        raise BadRequest('changes-since value set in the future.')
89

    
90
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
91
        raise BadRequest('Too old changes-since value.')
92

    
93
    return utc_since
94

    
95
def random_password(length=8):
96
    pool = ascii_letters + digits
97
    return ''.join(choice(pool) for i in range(length))
98

    
99

    
100
def get_vm(server_id, owner):
101
    """Return a VirtualMachine instance or raise ItemNotFound."""
102

    
103
    try:
104
        server_id = int(server_id)
105
        return VirtualMachine.objects.get(id=server_id, owner=owner)
106
    except ValueError:
107
        raise BadRequest('Invalid server ID.')
108
    except VirtualMachine.DoesNotExist:
109
        raise ItemNotFound('Server not found.')
110

    
111
def get_vm_meta(vm, key):
112
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
113

    
114
    try:
115
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
116
    except VirtualMachineMetadata.DoesNotExist:
117
        raise ItemNotFound('Metadata key not found.')
118

    
119
def get_image(image_id, owner):
120
    """Return an Image instance or raise ItemNotFound."""
121

    
122
    try:
123
        image_id = int(image_id)
124
        image = Image.objects.get(id=image_id)
125
        if not image.public and image.owner != owner:
126
            raise ItemNotFound('Image not found.')
127
        return image
128
    except ValueError:
129
        raise BadRequest('Invalid image ID.')
130
    except Image.DoesNotExist:
131
        raise ItemNotFound('Image not found.')
132

    
133
def get_image_meta(image, key):
134
    """Return a ImageMetadata instance or raise ItemNotFound."""
135

    
136
    try:
137
        return ImageMetadata.objects.get(meta_key=key, image=image)
138
    except ImageMetadata.DoesNotExist:
139
        raise ItemNotFound('Metadata key not found.')
140

    
141
def get_flavor(flavor_id):
142
    """Return a Flavor instance or raise ItemNotFound."""
143

    
144
    try:
145
        flavor_id = int(flavor_id)
146
        return Flavor.objects.get(id=flavor_id)
147
    except ValueError:
148
        raise BadRequest('Invalid flavor ID.')
149
    except Flavor.DoesNotExist:
150
        raise ItemNotFound('Flavor not found.')
151

    
152
def get_network(network_id, owner):
153
    """Return a Network instance or raise ItemNotFound."""
154

    
155
    try:
156
        if network_id == 'public':
157
            return Network.objects.get(public=True)
158
        else:
159
            network_id = int(network_id)
160
            return Network.objects.get(id=network_id, owner=owner)
161
    except ValueError:
162
        raise BadRequest('Invalid network ID.')
163
    except Network.DoesNotExist:
164
        raise ItemNotFound('Network not found.')
165

    
166
def get_nic(machine, network):
167
    try:
168
        return NetworkInterface.objects.get(machine=machine, network=network)
169
    except NetworkInterface.DoesNotExist:
170
        raise ItemNotFound('Server not connected to this network.')
171

    
172

    
173
def get_request_dict(request):
174
    """Returns data sent by the client as a python dict."""
175

    
176
    data = request.raw_post_data
177
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
178
        try:
179
            return json.loads(data)
180
        except ValueError:
181
            raise BadRequest('Invalid JSON data.')
182
    else:
183
        raise BadRequest('Unsupported Content-Type.')
184

    
185
def update_response_headers(request, response):
186
    if request.serialization == 'xml':
187
        response['Content-Type'] = 'application/xml'
188
    elif request.serialization == 'atom':
189
        response['Content-Type'] = 'application/atom+xml'
190
    else:
191
        response['Content-Type'] = 'application/json'
192

    
193
    if settings.TEST:
194
        response['Date'] = format_date_time(time())
195

    
196
def render_metadata(request, metadata, use_values=False, status=200):
197
    if request.serialization == 'xml':
198
        data = render_to_string('metadata.xml', {'metadata': metadata})
199
    else:
200
        if use_values:
201
            d = {'metadata': {'values': metadata}}
202
        else:
203
            d = {'metadata': metadata}
204
        data = json.dumps(d)
205
    return HttpResponse(data, status=status)
206

    
207
def render_meta(request, meta, status=200):
208
    if request.serialization == 'xml':
209
        data = render_to_string('meta.xml', {'meta': meta})
210
    else:
211
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
212
    return HttpResponse(data, status=status)
213

    
214
def render_fault(request, fault):
215
    if settings.DEBUG or settings.TEST:
216
        fault.details = format_exc(fault)
217

    
218
    if request.serialization == 'xml':
219
        data = render_to_string('fault.xml', {'fault': fault})
220
    else:
221
        d = {fault.name: {
222
                'code': fault.code,
223
                'message': fault.message,
224
                'details': fault.details}}
225
        data = json.dumps(d)
226

    
227
    resp = HttpResponse(data, status=fault.code)
228
    update_response_headers(request, resp)
229
    return resp
230

    
231

    
232
def request_serialization(request, atom_allowed=False):
233
    """Return the serialization format requested.
234

235
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
236
    """
237

    
238
    path = request.path
239

    
240
    if path.endswith('.json'):
241
        return 'json'
242
    elif path.endswith('.xml'):
243
        return 'xml'
244
    elif atom_allowed and path.endswith('.atom'):
245
        return 'atom'
246

    
247
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
248
        accept, sep, rest = item.strip().partition(';')
249
        if accept == 'application/json':
250
            return 'json'
251
        elif accept == 'application/xml':
252
            return 'xml'
253
        elif atom_allowed and accept == 'application/atom+xml':
254
            return 'atom'
255

    
256
    return 'json'
257

    
258
def api_method(http_method=None, atom_allowed=False):
259
    """Decorator function for views that implement an API method."""
260

    
261
    def decorator(func):
262
        @wraps(func)
263
        def wrapper(request, *args, **kwargs):
264
            try:
265
                request.serialization = request_serialization(
266
                    request,
267
                    atom_allowed)
268
                if not request.user:
269
                    raise Unauthorized('No user found.')
270
                if http_method and request.method != http_method:
271
                    raise BadRequest('Method not allowed.')
272

    
273
                resp = func(request, *args, **kwargs)
274
                update_response_headers(request, resp)
275
                return resp
276
            except VirtualMachine.DeletedError:
277
                fault = BadRequest('Server has been deleted.')
278
                return render_fault(request, fault)
279
            except VirtualMachine.BuildingError:
280
                fault = BuildInProgress('Server is being built.')
281
                return render_fault(request, fault)
282
            except Fault, fault:
283
                return render_fault(request, fault)
284
            except BaseException, e:
285
                logging.exception('Unexpected error: %s', e)
286
                fault = ServiceUnavailable('Unexpected error.')
287
                return render_fault(request, fault)
288
        return wrapper
289
    return decorator