Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (13.3 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
from django.db.models import Q
57

    
58
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress,
59
                                ItemNotFound, ServiceUnavailable, Unauthorized,
60
                                BadMediaType)
61
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
62
                               Network, BackendNetwork, NetworkInterface,
63
                               BridgePoolTable, MacPrefixPoolTable)
64
from synnefo.db.pools import EmptyPool
65

    
66
from synnefo.lib.astakos import get_user
67
from synnefo.plankton.backend import ImageBackend
68
from synnefo.settings import MAX_CIDR_BLOCK
69

    
70

    
71
log = getLogger('synnefo.api')
72

    
73

    
74
class UTC(tzinfo):
75
    def utcoffset(self, dt):
76
        return timedelta(0)
77

    
78
    def tzname(self, dt):
79
        return 'UTC'
80

    
81
    def dst(self, dt):
82
        return timedelta(0)
83

    
84

    
85
def isoformat(d):
86
    """Return an ISO8601 date string that includes a timezone."""
87

    
88
    return d.replace(tzinfo=UTC()).isoformat()
89

    
90

    
91
def isoparse(s):
92
    """Parse an ISO8601 date string into a datetime object."""
93

    
94
    if not s:
95
        return None
96

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

    
103
    now = datetime.datetime.now()
104
    if utc_since > now:
105
        raise BadRequest('changes-since value set in the future.')
106

    
107
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
108
        raise BadRequest('Too old changes-since value.')
109

    
110
    return utc_since
111

    
112

    
113
def random_password():
114
    """Generates a random password
115

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

    
120
    pool = lowercase + uppercase + digits
121
    lowerset = set(lowercase)
122
    upperset = set(uppercase)
123
    digitset = set(digits)
124
    length = 10
125

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

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

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

    
140
    return password
141

    
142

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

    
147
    npad = 16 - len(s) % 16
148
    return s + '\x00' * npad
149

    
150

    
151
def encrypt(plaintext):
152
    # Make sure key is 32 bytes long
153
    key = sha256(settings.SECRET_KEY).digest()
154

    
155
    aes = AES.new(key)
156
    enc = aes.encrypt(zeropad(plaintext))
157
    return b64encode(enc)
158

    
159

    
160
def get_vm(server_id, user_id):
161
    """Return a VirtualMachine instance or raise ItemNotFound."""
162

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

    
171

    
172
def get_vm_meta(vm, key):
173
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
174

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

    
180

    
181
def get_image(image_id, user_id):
182
    """Return an Image instance or raise ItemNotFound."""
183

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

    
193

    
194
def get_flavor(flavor_id):
195
    """Return a Flavor instance or raise ItemNotFound."""
196

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

    
203

    
204
def get_network(network_id, user_id, for_update=False):
205
    """Return a Network instance or raise ItemNotFound."""
206

    
207
    try:
208
        network_id = int(network_id)
209
        objects = Network.objects
210
        if for_update:
211
            objects = objects.select_for_update()
212
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
213
    except (ValueError, Network.DoesNotExist):
214
        raise ItemNotFound('Network not found.')
215

    
216

    
217
def validate_network_size(cidr_block):
218
    """Return True if network size is allowed."""
219
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
220

    
221

    
222
def allocate_public_address(backend):
223
    """Allocate a public IP for a vm."""
224
    for network in backend_public_networks(backend):
225
        try:
226
            address = get_network_free_address(network)
227
            return (network, address)
228
        except EmptyPool:
229
            pass
230
    return (None, None)
231

    
232

    
233
def backend_public_networks(backend):
234
    """Return available public networks of the backend.
235

236
    Iterator for non-deleted public networks that are available
237
    to the specified backend.
238

239
    """
240
    for network in Network.objects.filter(public=True, deleted=False):
241
        if BackendNetwork.objects.filter(network=network,
242
                                         backend=backend).exists():
243
            yield network
244

    
245

    
246
def get_network_free_address(network):
247
    """Reserve an IP address from the IP Pool of the network.
248

249
    Raises EmptyPool
250

251
    """
252

    
253
    pool = network.get_pool()
254
    address = pool.get()
255
    pool.save()
256
    return address
257

    
258

    
259
def get_nic(machine, network):
260
    try:
261
        return NetworkInterface.objects.get(machine=machine, network=network)
262
    except NetworkInterface.DoesNotExist:
263
        raise ItemNotFound('Server not connected to this network.')
264

    
265

    
266
def get_nic_from_index(vm, nic_index):
267
    """Returns the nic_index-th nic of a vm
268
       Error Response Codes: itemNotFound (404), badMediaType (415)
269
    """
270
    matching_nics = vm.nics.filter(index=nic_index)
271
    matching_nics_len = len(matching_nics)
272
    if matching_nics_len < 1:
273
        raise  ItemNotFound('NIC not found on VM')
274
    elif matching_nics_len > 1:
275
        raise BadMediaType('NIC index conflict on VM')
276
    nic = matching_nics[0]
277
    return nic
278

    
279

    
280
def get_request_dict(request):
281
    """Returns data sent by the client as a python dict."""
282

    
283
    data = request.raw_post_data
284
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
285
        try:
286
            return json.loads(data)
287
        except ValueError:
288
            raise BadRequest('Invalid JSON data.')
289
    else:
290
        raise BadRequest('Unsupported Content-Type.')
291

    
292

    
293
def update_response_headers(request, response):
294
    if request.serialization == 'xml':
295
        response['Content-Type'] = 'application/xml'
296
    elif request.serialization == 'atom':
297
        response['Content-Type'] = 'application/atom+xml'
298
    else:
299
        response['Content-Type'] = 'application/json'
300

    
301
    if settings.TEST:
302
        response['Date'] = format_date_time(time())
303

    
304
    add_never_cache_headers(response)
305

    
306

    
307
def render_metadata(request, metadata, use_values=False, status=200):
308
    if request.serialization == 'xml':
309
        data = render_to_string('metadata.xml', {'metadata': metadata})
310
    else:
311
        if use_values:
312
            d = {'metadata': {'values': metadata}}
313
        else:
314
            d = {'metadata': metadata}
315
        data = json.dumps(d)
316
    return HttpResponse(data, status=status)
317

    
318

    
319
def render_meta(request, meta, status=200):
320
    if request.serialization == 'xml':
321
        data = render_to_string('meta.xml', dict(key=key, val=val))
322
    else:
323
        data = json.dumps(dict(meta=meta))
324
    return HttpResponse(data, status=status)
325

    
326

    
327
def render_fault(request, fault):
328
    if settings.DEBUG or settings.TEST:
329
        fault.details = format_exc(fault)
330

    
331
    if request.serialization == 'xml':
332
        data = render_to_string('fault.xml', {'fault': fault})
333
    else:
334
        d = {fault.name: {
335
                'code': fault.code,
336
                'message': fault.message,
337
                'details': fault.details}}
338
        data = json.dumps(d)
339

    
340
    resp = HttpResponse(data, status=fault.code)
341
    update_response_headers(request, resp)
342
    return resp
343

    
344

    
345
def request_serialization(request, atom_allowed=False):
346
    """Return the serialization format requested.
347

348
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
349
    """
350

    
351
    path = request.path
352

    
353
    if path.endswith('.json'):
354
        return 'json'
355
    elif path.endswith('.xml'):
356
        return 'xml'
357
    elif atom_allowed and path.endswith('.atom'):
358
        return 'atom'
359

    
360
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
361
        accept, sep, rest = item.strip().partition(';')
362
        if accept == 'application/json':
363
            return 'json'
364
        elif accept == 'application/xml':
365
            return 'xml'
366
        elif atom_allowed and accept == 'application/atom+xml':
367
            return 'atom'
368

    
369
    return 'json'
370

    
371

    
372
def api_method(http_method=None, atom_allowed=False):
373
    """Decorator function for views that implement an API method."""
374

    
375
    def decorator(func):
376
        @wraps(func)
377
        def wrapper(request, *args, **kwargs):
378
            try:
379
                request.serialization = request_serialization(request,
380
                                                              atom_allowed)
381
                get_user(request, settings.ASTAKOS_URL)
382
                if not request.user_uniq:
383
                    raise Unauthorized('No user found.')
384
                if http_method and request.method != http_method:
385
                    raise BadRequest('Method not allowed.')
386

    
387
                resp = func(request, *args, **kwargs)
388
                update_response_headers(request, resp)
389
                return resp
390
            except VirtualMachine.DeletedError:
391
                fault = BadRequest('Server has been deleted.')
392
                return render_fault(request, fault)
393
            except VirtualMachine.BuildingError:
394
                fault = BuildInProgress('Server is being built.')
395
                return render_fault(request, fault)
396
            except Fault, fault:
397
                if fault.code >= 500:
398
                    log.exception('API fault')
399
                return render_fault(request, fault)
400
            except BaseException:
401
                log.exception('Unexpected error')
402
                fault = ServiceUnavailable('Unexpected error.')
403
                return render_fault(request, fault)
404
        return wrapper
405
    return decorator
406

    
407

    
408
def construct_nic_id(nic):
409
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
410

    
411

    
412
def net_resources(net_type):
413
    mac_prefix = settings.MAC_POOL_BASE
414
    if net_type == 'PRIVATE_MAC_FILTERED':
415
        link = settings.PRIVATE_MAC_FILTERED_BRIDGE
416
        mac_pool = MacPrefixPoolTable.get_pool()
417
        mac_prefix = mac_pool.get()
418
        mac_pool.save()
419
    elif net_type == 'PRIVATE_PHYSICAL_VLAN':
420
        pool = BridgePoolTable.get_pool()
421
        link = pool.get()
422
        pool.save()
423
    elif net_type == 'CUSTOM_ROUTED':
424
        link = settings.CUSTOM_ROUTED_ROUTING_TABLE
425
    elif net_type == 'CUSTOM_BRIDGED':
426
        link = settings.CUSTOM_BRIDGED_BRIDGE
427
    elif net_type == 'PUBLIC_ROUTED':
428
        link = settings.PUBLIC_ROUTED_ROUTING_TABLE
429
    else:
430
        raise BadRequest('Unknown network type')
431

    
432
    return link, mac_prefix