Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (13.2 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)
60
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
61
                               Network, BackendNetwork, NetworkInterface,
62
                               BridgePoolTable, MacPrefixPoolTable)
63
from synnefo.db.pools import EmptyPool
64

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

    
69

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

    
72

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

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

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

    
83

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

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

    
89

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

    
93
    if not s:
94
        return None
95

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

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

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

    
109
    return utc_since
110

    
111

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

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

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

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

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

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

    
139
    return password
140

    
141

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

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

    
149

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

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

    
158

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

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

    
170

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

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

    
179

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

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

    
192

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

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

    
202

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

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

    
215

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

    
220

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

    
231

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

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

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

    
244

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

248
    Raises EmptyPool
249

250
    """
251

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

    
257

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

    
264

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

    
278

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

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

    
291

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

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

    
303
    add_never_cache_headers(response)
304

    
305

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

    
317

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

    
325

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

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

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

    
343

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

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

    
350
    path = request.path
351

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

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

    
368
    return 'json'
369

    
370

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

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

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

    
404

    
405
def construct_nic_id(nic):
406
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
407

    
408

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

    
429
    return link, mac_prefix