Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.9 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

    
64
from synnefo.lib.astakos import get_user
65
from synnefo.plankton.backend import ImageBackend
66
from synnefo.db.pools import IPPool
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):
204
    """Return a Network instance or raise ItemNotFound."""
205

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

    
212

    
213
def validate_network_size(cidr_block):
214
    """Return True if network size is allowed."""
215
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
216

    
217

    
218
def backend_public_networks(backend):
219
    """Return available public networks of the backend.
220

221
    Iterator for non-deleted public networks that are available
222
    to the specified backend.
223

224
    """
225
    for network in Network.objects.filter(public=True, deleted=False):
226
        if BackendNetwork.objects.filter(network=network,
227
                                         backend=backend).exists():
228
            yield network
229

    
230

    
231
def get_network_free_address(network):
232
    """Reserve an IP address from the IP Pool of the network.
233

234
    Raises Network.DoesNotExist , IPPool.IPPoolExhausted
235

236
    """
237

    
238
    # Get the Network object in exclusive mode in order to
239
    # safely (isolated) reserve an IP address
240
    network = Network.objects.select_for_update().get(id=network.id)
241
    pool = IPPool(network)
242
    address = pool.get_free_address()
243
    pool.save()
244
    return address
245

    
246

    
247
def get_nic(machine, network):
248
    try:
249
        return NetworkInterface.objects.get(machine=machine, network=network)
250
    except NetworkInterface.DoesNotExist:
251
        raise ItemNotFound('Server not connected to this network.')
252

    
253

    
254
def get_nic_from_index(vm, nic_index):
255
    """Returns the nic_index-th nic of a vm
256
       Error Response Codes: itemNotFound (404), badMediaType (415)
257
    """
258
    matching_nics = vm.nics.filter(index=nic_index)
259
    matching_nics_len = len(matching_nics)
260
    if matching_nics_len < 1:
261
        raise  ItemNotFound('NIC not found on VM')
262
    elif matching_nics_len > 1:
263
        raise BadMediaType('NIC index conflict on VM')
264
    nic = matching_nics[0]
265
    return nic
266

    
267

    
268
def get_request_dict(request):
269
    """Returns data sent by the client as a python dict."""
270

    
271
    data = request.raw_post_data
272
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
273
        try:
274
            return json.loads(data)
275
        except ValueError:
276
            raise BadRequest('Invalid JSON data.')
277
    else:
278
        raise BadRequest('Unsupported Content-Type.')
279

    
280

    
281
def update_response_headers(request, response):
282
    if request.serialization == 'xml':
283
        response['Content-Type'] = 'application/xml'
284
    elif request.serialization == 'atom':
285
        response['Content-Type'] = 'application/atom+xml'
286
    else:
287
        response['Content-Type'] = 'application/json'
288

    
289
    if settings.TEST:
290
        response['Date'] = format_date_time(time())
291

    
292
    add_never_cache_headers(response)
293

    
294

    
295
def render_metadata(request, metadata, use_values=False, status=200):
296
    if request.serialization == 'xml':
297
        data = render_to_string('metadata.xml', {'metadata': metadata})
298
    else:
299
        if use_values:
300
            d = {'metadata': {'values': metadata}}
301
        else:
302
            d = {'metadata': metadata}
303
        data = json.dumps(d)
304
    return HttpResponse(data, status=status)
305

    
306

    
307
def render_meta(request, meta, status=200):
308
    if request.serialization == 'xml':
309
        data = render_to_string('meta.xml', dict(key=key, val=val))
310
    else:
311
        data = json.dumps(dict(meta=meta))
312
    return HttpResponse(data, status=status)
313

    
314

    
315
def render_fault(request, fault):
316
    if settings.DEBUG or settings.TEST:
317
        fault.details = format_exc(fault)
318

    
319
    if request.serialization == 'xml':
320
        data = render_to_string('fault.xml', {'fault': fault})
321
    else:
322
        d = {fault.name: {
323
                'code': fault.code,
324
                'message': fault.message,
325
                'details': fault.details}}
326
        data = json.dumps(d)
327

    
328
    resp = HttpResponse(data, status=fault.code)
329
    update_response_headers(request, resp)
330
    return resp
331

    
332

    
333
def request_serialization(request, atom_allowed=False):
334
    """Return the serialization format requested.
335

336
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
337
    """
338

    
339
    path = request.path
340

    
341
    if path.endswith('.json'):
342
        return 'json'
343
    elif path.endswith('.xml'):
344
        return 'xml'
345
    elif atom_allowed and path.endswith('.atom'):
346
        return 'atom'
347

    
348
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
349
        accept, sep, rest = item.strip().partition(';')
350
        if accept == 'application/json':
351
            return 'json'
352
        elif accept == 'application/xml':
353
            return 'xml'
354
        elif atom_allowed and accept == 'application/atom+xml':
355
            return 'atom'
356

    
357
    return 'json'
358

    
359

    
360
def api_method(http_method=None, atom_allowed=False):
361
    """Decorator function for views that implement an API method."""
362

    
363
    def decorator(func):
364
        @wraps(func)
365
        def wrapper(request, *args, **kwargs):
366
            try:
367
                request.serialization = request_serialization(request,
368
                                                              atom_allowed)
369
                get_user(request, settings.ASTAKOS_URL)
370
                if not request.user_uniq:
371
                    raise Unauthorized('No user found.')
372
                if http_method and request.method != http_method:
373
                    raise BadRequest('Method not allowed.')
374

    
375
                resp = func(request, *args, **kwargs)
376
                update_response_headers(request, resp)
377
                return resp
378
            except VirtualMachine.DeletedError:
379
                fault = BadRequest('Server has been deleted.')
380
                return render_fault(request, fault)
381
            except VirtualMachine.BuildingError:
382
                fault = BuildInProgress('Server is being built.')
383
                return render_fault(request, fault)
384
            except Fault, fault:
385
                return render_fault(request, fault)
386
            except BaseException:
387
                log.exception('Unexpected error')
388
                fault = ServiceUnavailable('Unexpected error.')
389
                return render_fault(request, fault)
390
        return wrapper
391
    return decorator
392

    
393

    
394
def construct_nic_id(nic):
395
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
396

    
397

    
398
def net_resources(net_type):
399
    mac_prefix = settings.MAC_POOL_BASE
400
    if net_type == 'PRIVATE_MAC_FILTERED':
401
        link = settings.PRIVATE_MAC_FILTERED_BRIDGE
402
        mac_pool = MacPrefixPoolTable.get_pool()
403
        mac_prefix = mac_pool.get()
404
        mac_pool.save()
405
    elif net_type == 'PRIVATE_PHYSICAL_VLAN':
406
        pool = BridgePoolTable.get_pool()
407
        link = pool.get()
408
        pool.save()
409
    elif net_type == 'CUSTOM_ROUTED':
410
        link = settings.CUSTOM_ROUTED_ROUTING_TABLE
411
    elif net_type == 'CUSTOM_BRIDGED':
412
        link = settings.CUSTOM_BRIDGED_BRIDGE
413
    elif net_type == 'PUBLIC_ROUTED':
414
        link = settings.PUBLIC_ROUTED_ROUTING_TABLE
415
    else:
416
        raise BadRequest('Unknown network type')
417

    
418
    return link, mac_prefix