Statistics
| Branch: | Tag: | Revision:

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

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
        # Ensure that request if for active flavor
200
        return Flavor.objects.get(id=flavor_id, deleted=False)
201
    except (ValueError, Flavor.DoesNotExist):
202
        raise ItemNotFound('Flavor not found.')
203

    
204

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

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

    
217

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

    
222

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

    
233

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

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

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

    
246

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

250
    Raises EmptyPool
251

252
    """
253

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

    
259

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

    
266

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

    
280

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

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

    
293

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

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

    
305
    add_never_cache_headers(response)
306

    
307

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

    
319

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

    
327

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

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

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

    
345

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

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

    
352
    path = request.path
353

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

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

    
370
    return 'json'
371

    
372

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

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

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

    
408

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

    
412

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

    
433
    return link, mac_prefix