Statistics
| Branch: | Tag: | Revision:

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

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

    
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.settings import MAX_CIDR_BLOCK
67

    
68

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

    
71

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

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

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

    
82

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

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

    
88

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

    
92
    if not s:
93
        return None
94

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

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

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

    
108
    return utc_since
109

    
110

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

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

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

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

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

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

    
138
    return password
139

    
140

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

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

    
148

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

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

    
157

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

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

    
169

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

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

    
178

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

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

    
191

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

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

    
201

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

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

    
214

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

    
219

    
220
def backend_public_networks(backend):
221
    """Return available public networks of the backend.
222

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

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

    
232

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

236
    Raises EmptyPool
237

238
    """
239

    
240
    pool = network.get_pool()
241
    address = pool.get()
242
    pool.save()
243
    return address
244

    
245

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

    
252

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

    
266

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

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

    
279

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

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

    
291
    add_never_cache_headers(response)
292

    
293

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

    
305

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

    
313

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

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

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

    
331

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

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

    
338
    path = request.path
339

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

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

    
356
    return 'json'
357

    
358

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

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

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

    
392

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

    
396

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

    
417
    return link, mac_prefix