util.py 10.6 KB
Newer Older
Giorgos Verigakis's avatar
Giorgos Verigakis committed
1
# Copyright 2011 GRNET S.A. All rights reserved.
2
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
3
4
5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
6
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
7
8
9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
10
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
11
12
13
14
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
15
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
16
17
18
19
20
21
22
23
24
25
26
27
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
28
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
29
30
31
32
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
33

34
35
36
37
import datetime
import dateutil.parser

from base64 import b64encode
Giorgos Verigakis's avatar
Giorgos Verigakis committed
38
39
from datetime import timedelta, tzinfo
from functools import wraps
40
from hashlib import sha256
Giorgos Verigakis's avatar
Giorgos Verigakis committed
41
42
from random import choice
from string import ascii_letters, digits
43
from time import time
Giorgos Verigakis's avatar
Giorgos Verigakis committed
44
from traceback import format_exc
45
from wsgiref.handlers import format_date_time
46

47
from Crypto.Cipher import AES
Giorgos Verigakis's avatar
Giorgos Verigakis committed
48

Giorgos Verigakis's avatar
Giorgos Verigakis committed
49
from django.conf import settings
50
51
from django.http import HttpResponse
from django.template.loader import render_to_string
52
from django.utils import simplejson as json
53
from django.utils.cache import add_never_cache_headers
54

55
56
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress,
                                ItemNotFound, ServiceUnavailable, Unauthorized)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
57
from synnefo.db.models import (SynnefoUser, Flavor, Image, ImageMetadata,
Giorgos Verigakis's avatar
Giorgos Verigakis committed
58
59
                                VirtualMachine, VirtualMachineMetadata,
                                Network, NetworkInterface)
60
from synnefo.logic import log
61

Giorgos Verigakis's avatar
Giorgos Verigakis committed
62
63
64
class UTC(tzinfo):
    def utcoffset(self, dt):
        return timedelta(0)
65

Giorgos Verigakis's avatar
Giorgos Verigakis committed
66
67
    def tzname(self, dt):
        return 'UTC'
68

Giorgos Verigakis's avatar
Giorgos Verigakis committed
69
70
    def dst(self, dt):
        return timedelta(0)
71
72


Giorgos Verigakis's avatar
Giorgos Verigakis committed
73
def isoformat(d):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
74
    """Return an ISO8601 date string that includes a timezone."""
75

Giorgos Verigakis's avatar
Giorgos Verigakis committed
76
77
78
79
    return d.replace(tzinfo=UTC()).isoformat()

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
81
82
    if not s:
        return None
83

Giorgos Verigakis's avatar
Giorgos Verigakis committed
84
85
    try:
        since = dateutil.parser.parse(s)
86
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
87
88
    except ValueError:
        raise BadRequest('Invalid changes-since parameter.')
89

90
91
    now = datetime.datetime.now()
    if utc_since > now:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
92
        raise BadRequest('changes-since value set in the future.')
93

94
    if now - utc_since > timedelta(seconds=settings.POLL_LIMIT):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
95
        raise BadRequest('Too old changes-since value.')
96

97
    return utc_since
98

Giorgos Verigakis's avatar
Giorgos Verigakis committed
99
100
101
102
def random_password(length=8):
    pool = ascii_letters + digits
    return ''.join(choice(pool) for i in range(length))

103
104
105
def zeropad(s):
    """Add zeros at the end of a string in order to make its length
       a multiple of 16."""
106

107
108
109
110
111
112
    npad = 16 - len(s) % 16
    return s + '\x00' * npad

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

114
115
116
117
    aes = AES.new(key)
    enc = aes.encrypt(zeropad(plaintext))
    return b64encode(enc)

118

Giorgos Verigakis's avatar
Giorgos Verigakis committed
119
def get_vm(server_id, owner):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
120
    """Return a VirtualMachine instance or raise ItemNotFound."""
121

Giorgos Verigakis's avatar
Giorgos Verigakis committed
122
123
    try:
        server_id = int(server_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
124
        return VirtualMachine.objects.get(id=server_id, owner=owner)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
125
126
127
128
129
    except ValueError:
        raise BadRequest('Invalid server ID.')
    except VirtualMachine.DoesNotExist:
        raise ItemNotFound('Server not found.')

Giorgos Verigakis's avatar
Giorgos Verigakis committed
130
def get_vm_meta(vm, key):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
131
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
132

Giorgos Verigakis's avatar
Giorgos Verigakis committed
133
    try:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
134
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
135
136
137
    except VirtualMachineMetadata.DoesNotExist:
        raise ItemNotFound('Metadata key not found.')

Giorgos Verigakis's avatar
Giorgos Verigakis committed
138
def get_image(image_id, owner):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
139
    """Return an Image instance or raise ItemNotFound."""
140

Giorgos Verigakis's avatar
Giorgos Verigakis committed
141
142
    try:
        image_id = int(image_id)
143
144
145
146
        image = Image.objects.get(id=image_id)
        if not image.public and image.owner != owner:
            raise ItemNotFound('Image not found.')
        return image
Giorgos Verigakis's avatar
Giorgos Verigakis committed
147
148
    except ValueError:
        raise BadRequest('Invalid image ID.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
149
150
151
    except Image.DoesNotExist:
        raise ItemNotFound('Image not found.')

Giorgos Verigakis's avatar
Giorgos Verigakis committed
152
def get_image_meta(image, key):
153
154
155
    """Return a ImageMetadata instance or raise ItemNotFound."""

    try:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
156
        return ImageMetadata.objects.get(meta_key=key, image=image)
157
158
159
    except ImageMetadata.DoesNotExist:
        raise ItemNotFound('Metadata key not found.')

Giorgos Verigakis's avatar
Giorgos Verigakis committed
160
161
def get_flavor(flavor_id):
    """Return a Flavor instance or raise ItemNotFound."""
162

Giorgos Verigakis's avatar
Giorgos Verigakis committed
163
164
165
    try:
        flavor_id = int(flavor_id)
        return Flavor.objects.get(id=flavor_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
166
167
    except ValueError:
        raise BadRequest('Invalid flavor ID.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
168
169
    except Flavor.DoesNotExist:
        raise ItemNotFound('Flavor not found.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
170

171
def get_network(network_id, owner):
172
    """Return a Network instance or raise ItemNotFound."""
173

174
    try:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
175
        if network_id == 'public':
Giorgos Verigakis's avatar
Giorgos Verigakis committed
176
177
178
179
            return Network.objects.get(public=True)
        else:
            network_id = int(network_id)
            return Network.objects.get(id=network_id, owner=owner)
180
    except ValueError:
181
        raise BadRequest('Invalid network ID.')
182
    except Network.DoesNotExist:
183
184
        raise ItemNotFound('Network not found.')

Giorgos Verigakis's avatar
Giorgos Verigakis committed
185
186
187
188
189
190
def get_nic(machine, network):
    try:
        return NetworkInterface.objects.get(machine=machine, network=network)
    except NetworkInterface.DoesNotExist:
        raise ItemNotFound('Server not connected to this network.')

191

192
def get_request_dict(request):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
193
    """Returns data sent by the client as a python dict."""
194

195
    data = request.raw_post_data
196
    if request.META.get('CONTENT_TYPE').startswith('application/json'):
197
198
199
        try:
            return json.loads(data)
        except ValueError:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
200
201
202
            raise BadRequest('Invalid JSON data.')
    else:
        raise BadRequest('Unsupported Content-Type.')
203

204
205
206
207
208
209
210
def update_response_headers(request, response):
    if request.serialization == 'xml':
        response['Content-Type'] = 'application/xml'
    elif request.serialization == 'atom':
        response['Content-Type'] = 'application/atom+xml'
    else:
        response['Content-Type'] = 'application/json'
211

212
    if settings.TEST:
213
        response['Date'] = format_date_time(time())
214
215
216
    
    add_never_cache_headers(response)

217

218
219
220
221
def render_metadata(request, metadata, use_values=False, status=200):
    if request.serialization == 'xml':
        data = render_to_string('metadata.xml', {'metadata': metadata})
    else:
222
223
224
225
        if use_values:
            d = {'metadata': {'values': metadata}}
        else:
            d = {'metadata': metadata}
226
227
228
229
230
231
232
233
234
235
        data = json.dumps(d)
    return HttpResponse(data, status=status)

def render_meta(request, meta, status=200):
    if request.serialization == 'xml':
        data = render_to_string('meta.xml', {'meta': meta})
    else:
        data = json.dumps({'meta': {meta.meta_key: meta.meta_value}})
    return HttpResponse(data, status=status)

236
def render_fault(request, fault):
237
    if settings.DEBUG or settings.TEST:
238
        fault.details = format_exc(fault)
239

Giorgos Verigakis's avatar
Giorgos Verigakis committed
240
241
    if request.serialization == 'xml':
        data = render_to_string('fault.xml', {'fault': fault})
242
    else:
243
244
245
246
        d = {fault.name: {
                'code': fault.code,
                'message': fault.message,
                'details': fault.details}}
247
        data = json.dumps(d)
248

249
    resp = HttpResponse(data, status=fault.code)
250
    update_response_headers(request, resp)
251
    return resp
Giorgos Verigakis's avatar
Giorgos Verigakis committed
252

253

Giorgos Verigakis's avatar
Giorgos Verigakis committed
254
255
def request_serialization(request, atom_allowed=False):
    """Return the serialization format requested.
256

Giorgos Verigakis's avatar
Giorgos Verigakis committed
257
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
Giorgos Verigakis's avatar
Giorgos Verigakis committed
258
    """
259

Giorgos Verigakis's avatar
Giorgos Verigakis committed
260
    path = request.path
261

Giorgos Verigakis's avatar
Giorgos Verigakis committed
262
263
264
265
266
267
    if path.endswith('.json'):
        return 'json'
    elif path.endswith('.xml'):
        return 'xml'
    elif atom_allowed and path.endswith('.atom'):
        return 'atom'
268

Giorgos Verigakis's avatar
Giorgos Verigakis committed
269
270
271
272
273
274
275
276
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
        accept, sep, rest = item.strip().partition(';')
        if accept == 'application/json':
            return 'json'
        elif accept == 'application/xml':
            return 'xml'
        elif atom_allowed and accept == 'application/atom+xml':
            return 'atom'
277

Giorgos Verigakis's avatar
Giorgos Verigakis committed
278
    return 'json'
279

Giorgos Verigakis's avatar
Giorgos Verigakis committed
280
281
def api_method(http_method=None, atom_allowed=False):
    """Decorator function for views that implement an API method."""
282

283
284
285
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
Giorgos Verigakis's avatar
Bugfix    
Giorgos Verigakis committed
286
            u = request.user.uniq if request.user else ''
287
288
            logger = log.get_logger("synnefo.api")
            logger.debug("%s <%s>" % (request.path, u))
289
            try:
290

291
292
293
                request.serialization = request_serialization(
                    request,
                    atom_allowed)
294
295
296
297
                if not request.method == 'GET':
                    if 'readonly' in request.__dict__ and \
                       request.readonly == True:
                        raise BadRequest('Method not allowed')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
298
299
                if not request.user:
                    raise Unauthorized('No user found.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
300
301
                if http_method and request.method != http_method:
                    raise BadRequest('Method not allowed.')
302

303
                resp = func(request, *args, **kwargs)
304
                update_response_headers(request, resp)
305
                return resp
306
307
308
309
310
311
            except VirtualMachine.DeletedError:
                fault = BadRequest('Server has been deleted.')
                return render_fault(request, fault)
            except VirtualMachine.BuildingError:
                fault = BuildInProgress('Server is being built.')
                return render_fault(request, fault)
312
313
            except Fault, fault:
                return render_fault(request, fault)
314
            except BaseException, e:
315
                logger.exception('Unexpected error')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
316
                fault = ServiceUnavailable('Unexpected error.')
317
318
319
                return render_fault(request, fault)
        return wrapper
    return decorator