Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 08b079e2

History | View | Annotate | Download (11.1 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, BadMediaType)
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
def get_nic_from_index(vm, nic_index):
218
    """Returns the nic_index-th nic of a vm
219
       Error Response Codes: itemNotFound (404), badMediaType (415)
220
    """
221
    matching_nics = vm.nics.filter(index=nic_index)
222
    matching_nics_len = len(matching_nics)
223
    if matching_nics_len < 1:
224
        raise  ItemNotFound('NIC not found on VM')
225
    elif matching_nics_len > 1:
226
        raise BadMediaType('NIC index conflict on VM')
227
    nic = matching_nics[0]
228
    return nic
229

    
230
def get_request_dict(request):
231
    """Returns data sent by the client as a python dict."""
232

    
233
    data = request.raw_post_data
234
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
235
        try:
236
            return json.loads(data)
237
        except ValueError:
238
            raise BadRequest('Invalid JSON data.')
239
    else:
240
        raise BadRequest('Unsupported Content-Type.')
241

    
242

    
243
def update_response_headers(request, response):
244
    if request.serialization == 'xml':
245
        response['Content-Type'] = 'application/xml'
246
    elif request.serialization == 'atom':
247
        response['Content-Type'] = 'application/atom+xml'
248
    else:
249
        response['Content-Type'] = 'application/json'
250

    
251
    if settings.TEST:
252
        response['Date'] = format_date_time(time())
253

    
254
    add_never_cache_headers(response)
255

    
256

    
257
def render_metadata(request, metadata, use_values=False, status=200):
258
    if request.serialization == 'xml':
259
        data = render_to_string('metadata.xml', {'metadata': metadata})
260
    else:
261
        if use_values:
262
            d = {'metadata': {'values': metadata}}
263
        else:
264
            d = {'metadata': metadata}
265
        data = json.dumps(d)
266
    return HttpResponse(data, status=status)
267

    
268

    
269
def render_meta(request, meta, status=200):
270
    if request.serialization == 'xml':
271
        data = render_to_string('meta.xml', dict(key=key, val=val))
272
    else:
273
        data = json.dumps(dict(meta=meta))
274
    return HttpResponse(data, status=status)
275

    
276

    
277
def render_fault(request, fault):
278
    if settings.DEBUG or settings.TEST:
279
        fault.details = format_exc(fault)
280

    
281
    if request.serialization == 'xml':
282
        data = render_to_string('fault.xml', {'fault': fault})
283
    else:
284
        d = {fault.name: {
285
                'code': fault.code,
286
                'message': fault.message,
287
                'details': fault.details}}
288
        data = json.dumps(d)
289

    
290
    resp = HttpResponse(data, status=fault.code)
291
    update_response_headers(request, resp)
292
    return resp
293

    
294

    
295
def request_serialization(request, atom_allowed=False):
296
    """Return the serialization format requested.
297

298
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
299
    """
300

    
301
    path = request.path
302

    
303
    if path.endswith('.json'):
304
        return 'json'
305
    elif path.endswith('.xml'):
306
        return 'xml'
307
    elif atom_allowed and path.endswith('.atom'):
308
        return 'atom'
309

    
310
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
311
        accept, sep, rest = item.strip().partition(';')
312
        if accept == 'application/json':
313
            return 'json'
314
        elif accept == 'application/xml':
315
            return 'xml'
316
        elif atom_allowed and accept == 'application/atom+xml':
317
            return 'atom'
318

    
319
    return 'json'
320

    
321

    
322
def api_method(http_method=None, atom_allowed=False):
323
    """Decorator function for views that implement an API method."""
324

    
325
    def decorator(func):
326
        @wraps(func)
327
        def wrapper(request, *args, **kwargs):
328
            try:
329
                request.serialization = request_serialization(request,
330
                                                              atom_allowed)
331
                get_user(request, settings.ASTAKOS_URL)
332
                if not request.user_uniq:
333
                    raise Unauthorized('No user found.')
334
                if http_method and request.method != http_method:
335
                    raise BadRequest('Method not allowed.')
336
                
337
                resp = func(request, *args, **kwargs)
338
                update_response_headers(request, resp)
339
                return resp
340
            except VirtualMachine.DeletedError:
341
                fault = BadRequest('Server has been deleted.')
342
                return render_fault(request, fault)
343
            except VirtualMachine.BuildingError:
344
                fault = BuildInProgress('Server is being built.')
345
                return render_fault(request, fault)
346
            except Fault, fault:
347
                return render_fault(request, fault)
348
            except BaseException, e:
349
                log.exception('Unexpected error')
350
                fault = ServiceUnavailable('Unexpected error.')
351
                return render_fault(request, fault)
352
        return wrapper
353
    return decorator
354

    
355
def construct_nic_id(nic):
356
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])