Statistics
| Branch: | Tag: | Revision:

root / api / util.py @ adee02b8

History | View | Annotate | Download (9.5 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
        return Image.objects.get(id=image_id, owner=owner)
125
    except ValueError:
126
        raise BadRequest('Invalid image ID.')
127
    except Image.DoesNotExist:
128
        raise ItemNotFound('Image not found.')
129

    
130
def get_image_meta(image, key):
131
    """Return a ImageMetadata instance or raise ItemNotFound."""
132

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

    
138
def get_flavor(flavor_id):
139
    """Return a Flavor instance or raise ItemNotFound."""
140

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

    
149
def get_network(network_id, owner):
150
    """Return a Network instance or raise ItemNotFound."""
151

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

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

    
169

    
170
def get_request_dict(request):
171
    """Returns data sent by the client as a python dict."""
172

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

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

    
190
    if settings.TEST:
191
        response['Date'] = format_date_time(time())
192

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

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

    
211
def render_fault(request, fault):
212
    if settings.DEBUG or settings.TEST:
213
        fault.details = format_exc(fault)
214

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

    
224
    resp = HttpResponse(data, status=fault.code)
225
    update_response_headers(request, resp)
226
    return resp
227

    
228

    
229
def request_serialization(request, atom_allowed=False):
230
    """Return the serialization format requested.
231

232
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
233
    """
234

    
235
    path = request.path
236

    
237
    if path.endswith('.json'):
238
        return 'json'
239
    elif path.endswith('.xml'):
240
        return 'xml'
241
    elif atom_allowed and path.endswith('.atom'):
242
        return 'atom'
243

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

    
253
    return 'json'
254

    
255
def api_method(http_method=None, atom_allowed=False):
256
    """Decorator function for views that implement an API method."""
257

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

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