Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (13.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
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, non_deleted=False, non_suspended=False):
161
    """Find a VirtualMachine instance based on ID and owner."""
162

    
163
    try:
164
        server_id = int(server_id)
165
        vm = VirtualMachine.objects.get(id=server_id, userid=user_id)
166
        if non_deleted and vm.deleted:
167
            raise VirtualMachine.DeletedError
168
        if non_suspended and vm.suspended:
169
            raise Unauthorized("Administratively Suspended VM")
170
    except ValueError:
171
        raise BadRequest('Invalid server ID.')
172
    except VirtualMachine.DoesNotExist:
173
        raise ItemNotFound('Server not found.')
174

    
175
def get_vm_meta(vm, key):
176
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
177

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

    
183

    
184
def get_image(image_id, user_id):
185
    """Return an Image instance or raise ItemNotFound."""
186

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

    
196

    
197
def get_flavor(flavor_id, include_deleted=False):
198
    """Return a Flavor instance or raise ItemNotFound."""
199

    
200
    try:
201
        flavor_id = int(flavor_id)
202
        if include_deleted:
203
            return Flavor.objects.get(id=flavor_id)
204
        else:
205
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
206
    except (ValueError, Flavor.DoesNotExist):
207
        raise ItemNotFound('Flavor not found.')
208

    
209

    
210
def get_network(network_id, user_id, for_update=False):
211
    """Return a Network instance or raise ItemNotFound."""
212

    
213
    try:
214
        network_id = int(network_id)
215
        objects = Network.objects
216
        if for_update:
217
            objects = objects.select_for_update()
218
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
219
    except (ValueError, Network.DoesNotExist):
220
        raise ItemNotFound('Network not found.')
221

    
222

    
223
def validate_network_size(cidr_block):
224
    """Return True if network size is allowed."""
225
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
226

    
227

    
228
def allocate_public_address(backend):
229
    """Allocate a public IP for a vm."""
230
    for network in backend_public_networks(backend):
231
        try:
232
            address = get_network_free_address(network)
233
            return (network, address)
234
        except EmptyPool:
235
            pass
236
    return (None, None)
237

    
238

    
239
def backend_public_networks(backend):
240
    """Return available public networks of the backend.
241

242
    Iterator for non-deleted public networks that are available
243
    to the specified backend.
244

245
    """
246
    for network in Network.objects.filter(public=True, deleted=False):
247
        if BackendNetwork.objects.filter(network=network,
248
                                         backend=backend).exists():
249
            yield network
250

    
251

    
252
def get_network_free_address(network):
253
    """Reserve an IP address from the IP Pool of the network.
254

255
    Raises EmptyPool
256

257
    """
258

    
259
    pool = network.get_pool()
260
    address = pool.get()
261
    pool.save()
262
    return address
263

    
264

    
265
def get_nic(machine, network):
266
    try:
267
        return NetworkInterface.objects.get(machine=machine, network=network)
268
    except NetworkInterface.DoesNotExist:
269
        raise ItemNotFound('Server not connected to this network.')
270

    
271

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

    
285

    
286
def get_request_dict(request):
287
    """Returns data sent by the client as a python dict."""
288

    
289
    data = request.raw_post_data
290
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
291
        try:
292
            return json.loads(data)
293
        except ValueError:
294
            raise BadRequest('Invalid JSON data.')
295
    else:
296
        raise BadRequest('Unsupported Content-Type.')
297

    
298

    
299
def update_response_headers(request, response):
300
    if request.serialization == 'xml':
301
        response['Content-Type'] = 'application/xml'
302
    elif request.serialization == 'atom':
303
        response['Content-Type'] = 'application/atom+xml'
304
    else:
305
        response['Content-Type'] = 'application/json'
306

    
307
    if settings.TEST:
308
        response['Date'] = format_date_time(time())
309

    
310
    add_never_cache_headers(response)
311

    
312

    
313
def render_metadata(request, metadata, use_values=False, status=200):
314
    if request.serialization == 'xml':
315
        data = render_to_string('metadata.xml', {'metadata': metadata})
316
    else:
317
        if use_values:
318
            d = {'metadata': {'values': metadata}}
319
        else:
320
            d = {'metadata': metadata}
321
        data = json.dumps(d)
322
    return HttpResponse(data, status=status)
323

    
324

    
325
def render_meta(request, meta, status=200):
326
    if request.serialization == 'xml':
327
        data = render_to_string('meta.xml', dict(key=key, val=val))
328
    else:
329
        data = json.dumps(dict(meta=meta))
330
    return HttpResponse(data, status=status)
331

    
332

    
333
def render_fault(request, fault):
334
    if settings.DEBUG or settings.TEST:
335
        fault.details = format_exc(fault)
336

    
337
    if request.serialization == 'xml':
338
        data = render_to_string('fault.xml', {'fault': fault})
339
    else:
340
        d = {fault.name: {
341
                'code': fault.code,
342
                'message': fault.message,
343
                'details': fault.details}}
344
        data = json.dumps(d)
345

    
346
    resp = HttpResponse(data, status=fault.code)
347
    update_response_headers(request, resp)
348
    return resp
349

    
350

    
351
def request_serialization(request, atom_allowed=False):
352
    """Return the serialization format requested.
353

354
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
355
    """
356

    
357
    path = request.path
358

    
359
    if path.endswith('.json'):
360
        return 'json'
361
    elif path.endswith('.xml'):
362
        return 'xml'
363
    elif atom_allowed and path.endswith('.atom'):
364
        return 'atom'
365

    
366
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
367
        accept, sep, rest = item.strip().partition(';')
368
        if accept == 'application/json':
369
            return 'json'
370
        elif accept == 'application/xml':
371
            return 'xml'
372
        elif atom_allowed and accept == 'application/atom+xml':
373
            return 'atom'
374

    
375
    return 'json'
376

    
377

    
378
def api_method(http_method=None, atom_allowed=False):
379
    """Decorator function for views that implement an API method."""
380

    
381
    def decorator(func):
382
        @wraps(func)
383
        def wrapper(request, *args, **kwargs):
384
            try:
385
                request.serialization = request_serialization(request,
386
                                                              atom_allowed)
387
                get_user(request, settings.ASTAKOS_URL)
388
                if not request.user_uniq:
389
                    raise Unauthorized('No user found.')
390
                if http_method and request.method != http_method:
391
                    raise BadRequest('Method not allowed.')
392

    
393
                resp = func(request, *args, **kwargs)
394
                update_response_headers(request, resp)
395
                return resp
396
            except VirtualMachine.DeletedError:
397
                fault = BadRequest('Server has been deleted.')
398
                return render_fault(request, fault)
399
            except Network.DeletedError:
400
                fault = BadRequest('Network has been deleted.')
401
                return render_fault(request, fault)
402
            except VirtualMachine.BuildingError:
403
                fault = BuildInProgress('Server is being built.')
404
                return render_fault(request, fault)
405
            except Fault, fault:
406
                if fault.code >= 500:
407
                    log.exception('API fault')
408
                return render_fault(request, fault)
409
            except BaseException:
410
                log.exception('Unexpected error')
411
                fault = ServiceUnavailable('Unexpected error.')
412
                return render_fault(request, fault)
413
        return wrapper
414
    return decorator
415

    
416

    
417
def construct_nic_id(nic):
418
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
419

    
420

    
421
def net_resources(net_type):
422
    mac_prefix = settings.MAC_POOL_BASE
423
    if net_type == 'PRIVATE_MAC_FILTERED':
424
        link = settings.PRIVATE_MAC_FILTERED_BRIDGE
425
        mac_pool = MacPrefixPoolTable.get_pool()
426
        mac_prefix = mac_pool.get()
427
        mac_pool.save()
428
    elif net_type == 'PRIVATE_PHYSICAL_VLAN':
429
        pool = BridgePoolTable.get_pool()
430
        link = pool.get()
431
        pool.save()
432
    elif net_type == 'CUSTOM_ROUTED':
433
        link = settings.CUSTOM_ROUTED_ROUTING_TABLE
434
    elif net_type == 'CUSTOM_BRIDGED':
435
        link = settings.CUSTOM_BRIDGED_BRIDGE
436
    elif net_type == 'PUBLIC_ROUTED':
437
        link = settings.PUBLIC_ROUTED_ROUTING_TABLE
438
    else:
439
        raise BadRequest('Unknown network type')
440

    
441
    return link, mac_prefix