Statistics
| Branch: | Tag: | Revision:

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

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
        return vm
171
    except ValueError:
172
        raise BadRequest('Invalid server ID.')
173
    except VirtualMachine.DoesNotExist:
174
        raise ItemNotFound('Server not found.')
175

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

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

    
184

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

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

    
197

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

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

    
210

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

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

    
223

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

    
228

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

    
239

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

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

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

    
252

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

256
    Raises EmptyPool
257

258
    """
259

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

    
265

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

    
272

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

    
286

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

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

    
299

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

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

    
311
    add_never_cache_headers(response)
312

    
313

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

    
325

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

    
333

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

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

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

    
351

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

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

    
358
    path = request.path
359

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

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

    
376
    return 'json'
377

    
378

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

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

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

    
417

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

    
421

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

    
442
    return link, mac_prefix