Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 4b3b8688

History | View | Annotate | Download (10.6 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.lib.astakos import get_user
62
from synnefo.plankton.backend import ImageBackend
63

    
64

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

    
67

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

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

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

    
78

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

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

    
84

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

    
88
    if not s:
89
        return None
90

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

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

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

    
104
    return utc_since
105

    
106

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

    
136

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

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

    
144

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

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

    
153

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

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

    
165

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

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

    
174

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

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

    
187

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

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

    
197

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

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

    
210

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

    
217

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

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

    
230

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

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

    
244

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

    
256

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

    
264

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

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

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

    
282

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

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

    
289
    path = request.path
290

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

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

    
307
    return 'json'
308

    
309

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

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

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