Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / util.py @ 3165f027

History | View | Annotate | Download (11.8 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
                                BadMediaType, OverLimit)
60
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
61
                               Network, NetworkInterface, BridgePool,
62
                               MacPrefixPool, Pool)
63

    
64
from synnefo.lib.astakos import get_user
65
from synnefo.plankton.backend import ImageBackend
66

    
67

    
68
log = getLogger('synnefo.api')
69

    
70

    
71
class UTC(tzinfo):
72
    def utcoffset(self, dt):
73
        return timedelta(0)
74

    
75
    def tzname(self, dt):
76
        return 'UTC'
77

    
78
    def dst(self, dt):
79
        return timedelta(0)
80

    
81

    
82
def isoformat(d):
83
    """Return an ISO8601 date string that includes a timezone."""
84

    
85
    return d.replace(tzinfo=UTC()).isoformat()
86

    
87

    
88
def isoparse(s):
89
    """Parse an ISO8601 date string into a datetime object."""
90

    
91
    if not s:
92
        return None
93

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

    
100
    now = datetime.datetime.now()
101
    if utc_since > now:
102
        raise BadRequest('changes-since value set in the future.')
103

    
104
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
105
        raise BadRequest('Too old changes-since value.')
106

    
107
    return utc_since
108

    
109

    
110
def random_password():
111
    """Generates a random password
112

113
    We generate a windows compliant password: it must contain at least
114
    one charachter from each of the groups: upper case, lower case, digits.
115
    """
116

    
117
    pool = lowercase + uppercase + digits
118
    lowerset = set(lowercase)
119
    upperset = set(uppercase)
120
    digitset = set(digits)
121
    length = 10
122

    
123
    password = ''.join(choice(pool) for i in range(length - 2))
124

    
125
    # Make sure the password is compliant
126
    chars = set(password)
127
    if not chars & lowerset:
128
        password += choice(lowercase)
129
    if not chars & upperset:
130
        password += choice(uppercase)
131
    if not chars & digitset:
132
        password += choice(digits)
133

    
134
    # Pad if necessary to reach required length
135
    password += ''.join(choice(pool) for i in range(length - len(password)))
136

    
137
    return password
138

    
139

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

    
144
    npad = 16 - len(s) % 16
145
    return s + '\x00' * npad
146

    
147

    
148
def encrypt(plaintext):
149
    # Make sure key is 32 bytes long
150
    key = sha256(settings.SECRET_KEY).digest()
151

    
152
    aes = AES.new(key)
153
    enc = aes.encrypt(zeropad(plaintext))
154
    return b64encode(enc)
155

    
156

    
157
def get_vm(server_id, user_id):
158
    """Return a VirtualMachine instance or raise ItemNotFound."""
159

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

    
168

    
169
def get_vm_meta(vm, key):
170
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
171

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

    
177

    
178
def get_image(image_id, user_id):
179
    """Return an Image instance or raise ItemNotFound."""
180

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

    
190

    
191
def get_flavor(flavor_id):
192
    """Return a Flavor instance or raise ItemNotFound."""
193

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

    
200

    
201
def get_network(network_id, user_id):
202
    """Return a Network instance or raise ItemNotFound."""
203

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

    
213

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

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

    
233
def get_request_dict(request):
234
    """Returns data sent by the client as a python dict."""
235

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

    
245

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

    
254
    if settings.TEST:
255
        response['Date'] = format_date_time(time())
256

    
257
    add_never_cache_headers(response)
258

    
259

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

    
271

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

    
279

    
280
def render_fault(request, fault):
281
    if settings.DEBUG or settings.TEST:
282
        fault.details = format_exc(fault)
283

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

    
293
    resp = HttpResponse(data, status=fault.code)
294
    update_response_headers(request, resp)
295
    return resp
296

    
297

    
298
def request_serialization(request, atom_allowed=False):
299
    """Return the serialization format requested.
300

301
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
302
    """
303

    
304
    path = request.path
305

    
306
    if path.endswith('.json'):
307
        return 'json'
308
    elif path.endswith('.xml'):
309
        return 'xml'
310
    elif atom_allowed and path.endswith('.atom'):
311
        return 'atom'
312

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

    
322
    return 'json'
323

    
324

    
325
def api_method(http_method=None, atom_allowed=False):
326
    """Decorator function for views that implement an API method."""
327

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

    
358

    
359
def construct_nic_id(nic):
360
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
361

    
362

    
363
def network_link_from_type(network_type):
364
    if network_type == 'PRIVATE_MAC_FILTERED':
365
        link = settings.PRIVATE_MAC_FILTERED_BRIDGE
366
    elif network_type == 'PRIVATE_PHYSICAL_VLAN':
367
        link = BridgePool.get_available().value
368
    elif network_type == 'CUSTOM_ROUTED':
369
        link = settings.CUSTOM_ROUTED_ROUTING_TABLE
370
    elif network_type == 'CUSTOM_BRIDGED':
371
        link = settings.CUSTOM_BRIDGED_BRIDGE
372
    elif network_type == 'PUBLIC_ROUTED':
373
        link = settings.PUBLIC_ROUTED_ROUTING_TABLE
374
    else:
375
        raise BadRequest('Unknown network network_type')
376

    
377
    return link