Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 2035039b

History | View | Annotate | Download (10.5 kB)

1
# Copyright 2011-2012 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
import datetime
35

    
36
from base64 import b64encode
37
from datetime import timedelta, tzinfo
38
from functools import wraps
39
from hashlib import sha256
40
from logging import getLogger
41
from random import choice
42
from string import digits, lowercase, uppercase
43
from time import time
44
from traceback import format_exc
45
from wsgiref.handlers import format_date_time
46

    
47
import dateutil.parser
48

    
49
from Crypto.Cipher import AES
50

    
51
from django.conf import settings
52
from django.http import HttpResponse
53
from django.template.loader import render_to_string
54
from django.utils import simplejson as json
55
from django.utils.cache import add_never_cache_headers
56

    
57
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress,
58
                                ItemNotFound, ServiceUnavailable, Unauthorized)
59
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
60
                               Network, NetworkInterface)
61
from synnefo.plankton.backend import ImageBackend
62

    
63

    
64
log = getLogger('synnefo.api')
65

    
66

    
67
class UTC(tzinfo):
68
    def utcoffset(self, dt):
69
        return timedelta(0)
70

    
71
    def tzname(self, dt):
72
        return 'UTC'
73

    
74
    def dst(self, dt):
75
        return timedelta(0)
76

    
77

    
78
def isoformat(d):
79
    """Return an ISO8601 date string that includes a timezone."""
80

    
81
    return d.replace(tzinfo=UTC()).isoformat()
82

    
83

    
84
def isoparse(s):
85
    """Parse an ISO8601 date string into a datetime object."""
86

    
87
    if not s:
88
        return None
89

    
90
    try:
91
        since = dateutil.parser.parse(s)
92
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
93
    except ValueError:
94
        raise BadRequest('Invalid changes-since parameter.')
95

    
96
    now = datetime.datetime.now()
97
    if utc_since > now:
98
        raise BadRequest('changes-since value set in the future.')
99

    
100
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
101
        raise BadRequest('Too old changes-since value.')
102

    
103
    return utc_since
104

    
105

    
106
def random_password():
107
    """Generates a random password
108
    
109
    We generate a windows compliant password: it must contain at least
110
    one charachter from each of the groups: upper case, lower case, digits.
111
    """
112
    
113
    pool = lowercase + uppercase + digits
114
    lowerset = set(lowercase)
115
    upperset = set(uppercase)
116
    digitset = set(digits)
117
    length = 10
118
    
119
    password = ''.join(choice(pool) for i in range(length - 2))
120
    
121
    # Make sure the password is compliant
122
    chars = set(password)
123
    if not chars & lowerset:
124
        password += choice(lowercase)
125
    if not chars & upperset:
126
        password += choice(uppercase)
127
    if not chars & digitset:
128
        password += choice(digits)
129
    
130
    # Pad if necessary to reach required length
131
    password += ''.join(choice(pool) for i in range(length - len(password)))
132
    
133
    return password
134

    
135

    
136
def zeropad(s):
137
    """Add zeros at the end of a string in order to make its length
138
       a multiple of 16."""
139

    
140
    npad = 16 - len(s) % 16
141
    return s + '\x00' * npad
142

    
143

    
144
def encrypt(plaintext):
145
    # Make sure key is 32 bytes long
146
    key = sha256(settings.SECRET_KEY).digest()
147

    
148
    aes = AES.new(key)
149
    enc = aes.encrypt(zeropad(plaintext))
150
    return b64encode(enc)
151

    
152

    
153
def get_vm(server_id, user_id):
154
    """Return a VirtualMachine instance or raise ItemNotFound."""
155

    
156
    try:
157
        server_id = int(server_id)
158
        return VirtualMachine.objects.get(id=server_id, userid=user_id)
159
    except ValueError:
160
        raise BadRequest('Invalid server ID.')
161
    except VirtualMachine.DoesNotExist:
162
        raise ItemNotFound('Server not found.')
163

    
164

    
165
def get_vm_meta(vm, key):
166
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
167

    
168
    try:
169
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
170
    except VirtualMachineMetadata.DoesNotExist:
171
        raise ItemNotFound('Metadata key not found.')
172

    
173

    
174
def get_image(image_id, user_id):
175
    """Return an Image instance or raise ItemNotFound."""
176

    
177
    backend = ImageBackend(user_id)
178
    try:
179
        image = backend.get_image(image_id)
180
        if not image:
181
            raise ItemNotFound('Image not found.')
182
        return image
183
    finally:
184
        backend.close()
185

    
186

    
187
def get_flavor(flavor_id):
188
    """Return a Flavor instance or raise ItemNotFound."""
189

    
190
    try:
191
        flavor_id = int(flavor_id)
192
        return Flavor.objects.get(id=flavor_id)
193
    except (ValueError, Flavor.DoesNotExist):
194
        raise ItemNotFound('Flavor not found.')
195

    
196

    
197
def get_network(network_id, user_id):
198
    """Return a Network instance or raise ItemNotFound."""
199

    
200
    try:
201
        if network_id == 'public':
202
            return Network.objects.get(public=True)
203
        else:
204
            network_id = int(network_id)
205
            return Network.objects.get(id=network_id, userid=user_id)
206
    except (ValueError, Network.DoesNotExist):
207
        raise ItemNotFound('Network not found.')
208

    
209

    
210
def get_nic(machine, network):
211
    try:
212
        return NetworkInterface.objects.get(machine=machine, network=network)
213
    except NetworkInterface.DoesNotExist:
214
        raise ItemNotFound('Server not connected to this network.')
215

    
216

    
217
def get_request_dict(request):
218
    """Returns data sent by the client as a python dict."""
219

    
220
    data = request.raw_post_data
221
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
222
        try:
223
            return json.loads(data)
224
        except ValueError:
225
            raise BadRequest('Invalid JSON data.')
226
    else:
227
        raise BadRequest('Unsupported Content-Type.')
228

    
229

    
230
def update_response_headers(request, response):
231
    if request.serialization == 'xml':
232
        response['Content-Type'] = 'application/xml'
233
    elif request.serialization == 'atom':
234
        response['Content-Type'] = 'application/atom+xml'
235
    else:
236
        response['Content-Type'] = 'application/json'
237

    
238
    if settings.TEST:
239
        response['Date'] = format_date_time(time())
240
    
241
    add_never_cache_headers(response)
242

    
243

    
244
def render_metadata(request, metadata, use_values=False, status=200):
245
    if request.serialization == 'xml':
246
        data = render_to_string('metadata.xml', {'metadata': metadata})
247
    else:
248
        if use_values:
249
            d = {'metadata': {'values': metadata}}
250
        else:
251
            d = {'metadata': metadata}
252
        data = json.dumps(d)
253
    return HttpResponse(data, status=status)
254

    
255

    
256
def render_meta(request, meta, status=200):
257
    if request.serialization == 'xml':
258
        data = render_to_string('meta.xml', dict(key=key, val=val))
259
    else:
260
        data = json.dumps(dict(meta=meta))
261
    return HttpResponse(data, status=status)
262

    
263

    
264
def render_fault(request, fault):
265
    if settings.DEBUG or settings.TEST:
266
        fault.details = format_exc(fault)
267

    
268
    if request.serialization == 'xml':
269
        data = render_to_string('fault.xml', {'fault': fault})
270
    else:
271
        d = {fault.name: {
272
                'code': fault.code,
273
                'message': fault.message,
274
                'details': fault.details}}
275
        data = json.dumps(d)
276

    
277
    resp = HttpResponse(data, status=fault.code)
278
    update_response_headers(request, resp)
279
    return resp
280

    
281

    
282
def request_serialization(request, atom_allowed=False):
283
    """Return the serialization format requested.
284

285
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
286
    """
287

    
288
    path = request.path
289

    
290
    if path.endswith('.json'):
291
        return 'json'
292
    elif path.endswith('.xml'):
293
        return 'xml'
294
    elif atom_allowed and path.endswith('.atom'):
295
        return 'atom'
296

    
297
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
298
        accept, sep, rest = item.strip().partition(';')
299
        if accept == 'application/json':
300
            return 'json'
301
        elif accept == 'application/xml':
302
            return 'xml'
303
        elif atom_allowed and accept == 'application/atom+xml':
304
            return 'atom'
305

    
306
    return 'json'
307

    
308

    
309
def api_method(http_method=None, atom_allowed=False):
310
    """Decorator function for views that implement an API method."""
311

    
312
    def decorator(func):
313
        @wraps(func)
314
        def wrapper(request, *args, **kwargs):
315
            try:
316
                request.serialization = request_serialization(request,
317
                                                              atom_allowed)
318
                if not request.user:
319
                    raise Unauthorized('No user found.')
320
                if http_method and request.method != http_method:
321
                    raise BadRequest('Method not allowed.')
322

    
323
                resp = func(request, *args, **kwargs)
324
                update_response_headers(request, resp)
325
                return resp
326
            except VirtualMachine.DeletedError:
327
                fault = BadRequest('Server has been deleted.')
328
                return render_fault(request, fault)
329
            except VirtualMachine.BuildingError:
330
                fault = BuildInProgress('Server is being built.')
331
                return render_fault(request, fault)
332
            except Fault, fault:
333
                return render_fault(request, fault)
334
            except BaseException, e:
335
                log.exception('Unexpected error')
336
                fault = ServiceUnavailable('Unexpected error.')
337
                return render_fault(request, fault)
338
        return wrapper
339
    return decorator