Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.7 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
import ipaddr
36

    
37
from base64 import b64encode, b64decode
38
from datetime import timedelta, tzinfo
39
from functools import wraps
40
from hashlib import sha256
41
from logging import getLogger
42
from random import choice
43
from string import digits, lowercase, uppercase
44
from time import time
45
from traceback import format_exc
46
from wsgiref.handlers import format_date_time
47

    
48
import dateutil.parser
49

    
50
from Crypto.Cipher import AES
51

    
52
from django.conf import settings
53
from django.http import HttpResponse
54
from django.template.loader import render_to_string
55
from django.utils import simplejson as json
56
from django.utils.cache import add_never_cache_headers
57
from django.db.models import Q
58

    
59
from snf_django.lib.api import faults
60
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
61
                               Network, BackendNetwork, NetworkInterface,
62
                               BridgePoolTable, MacPrefixPoolTable, Backend)
63
from synnefo.db.pools import EmptyPool
64

    
65
from snf_django.lib.astakos import get_user
66
from synnefo.plankton.utils import image_backend
67
from synnefo.settings import MAX_CIDR_BLOCK
68

    
69

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

    
72

    
73
def random_password():
74
    """Generates a random password
75

76
    We generate a windows compliant password: it must contain at least
77
    one charachter from each of the groups: upper case, lower case, digits.
78
    """
79

    
80
    pool = lowercase + uppercase + digits
81
    lowerset = set(lowercase)
82
    upperset = set(uppercase)
83
    digitset = set(digits)
84
    length = 10
85

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

    
88
    # Make sure the password is compliant
89
    chars = set(password)
90
    if not chars & lowerset:
91
        password += choice(lowercase)
92
    if not chars & upperset:
93
        password += choice(uppercase)
94
    if not chars & digitset:
95
        password += choice(digits)
96

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

    
100
    return password
101

    
102

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

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

    
110

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

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

    
119

    
120
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
121
           non_suspended=False):
122
    """Find a VirtualMachine instance based on ID and owner."""
123

    
124
    try:
125
        server_id = int(server_id)
126
        servers = VirtualMachine.objects
127
        if for_update:
128
            servers = servers.select_for_update()
129
        vm = servers.get(id=server_id, userid=user_id)
130
        if non_deleted and vm.deleted:
131
            raise faults.BadRequest("Server has been deleted.")
132
        if non_suspended and vm.suspended:
133
            raise faults.Forbidden("Administratively Suspended VM")
134
        return vm
135
    except ValueError:
136
        raise faults.BadRequest('Invalid server ID.')
137
    except VirtualMachine.DoesNotExist:
138
        raise faults.ItemNotFound('Server not found.')
139

    
140

    
141
def get_vm_meta(vm, key):
142
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
143

    
144
    try:
145
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
146
    except VirtualMachineMetadata.DoesNotExist:
147
        raise faults.ItemNotFound('Metadata key not found.')
148

    
149

    
150
def get_image(image_id, user_id):
151
    """Return an Image instance or raise ItemNotFound."""
152

    
153
    with image_backend(user_id) as backend:
154
        return backend.get_image(image_id)
155

    
156

    
157
def get_image_dict(image_id, user_id):
158
    image = {}
159
    img = get_image(image_id, user_id)
160
    properties = img.get('properties', {})
161
    image["id"] = img["id"]
162
    image["name"] = img["name"]
163
    image['backend_id'] = img['location']
164
    image['format'] = img['disk_format']
165
    image['metadata'] = dict((key.upper(), val)
166
                             for key, val in properties.items())
167
    image['checksum'] = img['checksum']
168

    
169
    return image
170

    
171

    
172
def get_flavor(flavor_id, include_deleted=False):
173
    """Return a Flavor instance or raise ItemNotFound."""
174

    
175
    try:
176
        flavor_id = int(flavor_id)
177
        if include_deleted:
178
            return Flavor.objects.get(id=flavor_id)
179
        else:
180
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
181
    except (ValueError, Flavor.DoesNotExist):
182
        raise faults.ItemNotFound('Flavor not found.')
183

    
184

    
185
def get_flavor_provider(flavor):
186
    """Extract provider from disk template.
187

188
    Provider for `ext` disk_template is encoded in the disk template
189
    name, which is formed `ext_<provider_name>`. Provider is None
190
    for all other disk templates.
191

192
    """
193
    disk_template = flavor.disk_template
194
    provider = None
195
    if disk_template.startswith("ext"):
196
        disk_template, provider = disk_template.split("_", 1)
197
    return disk_template, provider
198

    
199

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

    
203
    try:
204
        network_id = int(network_id)
205
        objects = Network.objects
206
        if for_update:
207
            objects = objects.select_for_update()
208
        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
209
    except (ValueError, Network.DoesNotExist):
210
        raise faults.ItemNotFound('Network not found.')
211

    
212

    
213
def validate_network_params(subnet, gateway=None, subnet6=None, gateway6=None):
214
    try:
215
        # Use strict option to not all subnets with host bits set
216
        network = ipaddr.IPv4Network(subnet, strict=True)
217
    except ValueError:
218
        raise faults.BadRequest("Invalid network IPv4 subnet")
219

    
220
    # Check that network size is allowed!
221
    if not validate_network_size(network.prefixlen):
222
        raise faults.OverLimit(message="Unsupported network size",
223
                        details="Network mask must be in range (%s, 29]" %
224
                                MAX_CIDR_BLOCK)
225

    
226
    # Check that gateway belongs to network
227
    if gateway:
228
        try:
229
            gateway = ipaddr.IPv4Address(gateway)
230
        except ValueError:
231
            raise faults.BadRequest("Invalid network IPv4 gateway")
232
        if not gateway in network:
233
            raise faults.BadRequest("Invalid network IPv4 gateway")
234

    
235
    if subnet6:
236
        try:
237
            # Use strict option to not all subnets with host bits set
238
            network6 = ipaddr.IPv6Network(subnet6, strict=True)
239
        except ValueError:
240
            raise faults.BadRequest("Invalid network IPv6 subnet")
241
        if gateway6:
242
            try:
243
                gateway6 = ipaddr.IPv6Address(gateway6)
244
            except ValueError:
245
                raise faults.BadRequest("Invalid network IPv6 gateway")
246
            if not gateway6 in network6:
247
                raise faults.BadRequest("Invalid network IPv6 gateway")
248

    
249

    
250
def validate_network_size(cidr_block):
251
    """Return True if network size is allowed."""
252
    return cidr_block <= 29 and cidr_block > MAX_CIDR_BLOCK
253

    
254

    
255
def allocate_public_address(backend):
256
    """Allocate a public IP for a vm."""
257
    for network in backend_public_networks(backend):
258
        try:
259
            address = get_network_free_address(network)
260
            return (network, address)
261
        except EmptyPool:
262
            pass
263
    return (None, None)
264

    
265

    
266
def get_public_ip(backend):
267
    """Reserve an IP from a public network.
268

269
    This method should run inside a transaction.
270

271
    """
272

    
273
    # Guarantee exclusive access to backend, because accessing the IP pools of
274
    # the backend networks may result in a deadlock with backend allocator
275
    # which also checks that backend networks have a free IP.
276
    backend = Backend.objects.select_for_update().get(id=backend.id)
277

    
278
    address = None
279
    if settings.PUBLIC_USE_POOL:
280
        (network, address) = allocate_public_address(backend)
281
    else:
282
        for net in list(backend_public_networks(backend)):
283
            pool = net.get_pool()
284
            if not pool.empty():
285
                address = 'pool'
286
                network = net
287
                break
288
    if address is None:
289
        log.error("Public networks of backend %s are full", backend)
290
        raise faults.OverLimit("Can not allocate IP for new machine."
291
                        " Public networks are full.")
292
    return (network, address)
293

    
294

    
295
def backend_public_networks(backend):
296
    """Return available public networks of the backend.
297

298
    Iterator for non-deleted public networks that are available
299
    to the specified backend.
300

301
    """
302
    for network in Network.objects.filter(public=True, deleted=False):
303
        if BackendNetwork.objects.filter(network=network,
304
                                         backend=backend).exists():
305
            yield network
306

    
307

    
308
def get_network_free_address(network):
309
    """Reserve an IP address from the IP Pool of the network.
310

311
    Raises EmptyPool
312

313
    """
314

    
315
    pool = network.get_pool()
316
    address = pool.get()
317
    pool.save()
318
    return address
319

    
320

    
321
def get_nic(machine, network):
322
    try:
323
        return NetworkInterface.objects.get(machine=machine, network=network)
324
    except NetworkInterface.DoesNotExist:
325
        raise faults.ItemNotFound('Server not connected to this network.')
326

    
327

    
328
def get_nic_from_index(vm, nic_index):
329
    """Returns the nic_index-th nic of a vm
330
       Error Response Codes: itemNotFound (404), badMediaType (415)
331
    """
332
    matching_nics = vm.nics.filter(index=nic_index)
333
    matching_nics_len = len(matching_nics)
334
    if matching_nics_len < 1:
335
        raise faults.ItemNotFound('NIC not found on VM')
336
    elif matching_nics_len > 1:
337
        raise faults.BadMediaType('NIC index conflict on VM')
338
    nic = matching_nics[0]
339
    return nic
340

    
341

    
342
def render_metadata(request, metadata, use_values=False, status=200):
343
    if request.serialization == 'xml':
344
        data = render_to_string('metadata.xml', {'metadata': metadata})
345
    else:
346
        if use_values:
347
            d = {'metadata': {'values': metadata}}
348
        else:
349
            d = {'metadata': metadata}
350
        data = json.dumps(d)
351
    return HttpResponse(data, status=status)
352

    
353

    
354
def render_meta(request, meta, status=200):
355
    if request.serialization == 'xml':
356
        data = render_to_string('meta.xml', dict(key=key, val=val))
357
    else:
358
        data = json.dumps(dict(meta=meta))
359
    return HttpResponse(data, status=status)
360

    
361

    
362
def construct_nic_id(nic):
363
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
364

    
365

    
366
def verify_personality(personality):
367
    """Verify that a a list of personalities is well formed"""
368
    if len(personality) > settings.MAX_PERSONALITY:
369
        raise faults.OverLimit("Maximum number of personalities"
370
                        " exceeded")
371
    for p in personality:
372
        # Verify that personalities are well-formed
373
        try:
374
            assert isinstance(p, dict)
375
            keys = set(p.keys())
376
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
377
            assert keys.issubset(allowed)
378
            contents = p['contents']
379
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
380
                # No need to decode if contents already exceed limit
381
                raise faults.OverLimit("Maximum size of personality exceeded")
382
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
383
                raise faults.OverLimit("Maximum size of personality exceeded")
384
        except AssertionError:
385
            raise faults.BadRequest("Malformed personality in request")
386

    
387

    
388
def values_from_flavor(flavor):
389
    """Get Ganeti connectivity info from flavor type.
390

391
    If link or mac_prefix equals to "pool", then the resources
392
    are allocated from the corresponding Pools.
393

394
    """
395
    try:
396
        flavor = Network.FLAVORS[flavor]
397
    except KeyError:
398
        raise faults.BadRequest("Unknown network flavor")
399

    
400
    mode = flavor.get("mode")
401

    
402
    link = flavor.get("link")
403
    if link == "pool":
404
        link = allocate_resource("bridge")
405

    
406
    mac_prefix = flavor.get("mac_prefix")
407
    if mac_prefix == "pool":
408
        mac_prefix = allocate_resource("mac_prefix")
409

    
410
    tags = flavor.get("tags")
411

    
412
    return mode, link, mac_prefix, tags
413

    
414

    
415
def allocate_resource(res_type):
416
    table = get_pool_table(res_type)
417
    pool = table.get_pool()
418
    value = pool.get()
419
    pool.save()
420
    return value
421

    
422

    
423
def release_resource(res_type, value):
424
    table = get_pool_table(res_type)
425
    pool = table.get_pool()
426
    pool.put(value)
427
    pool.save()
428

    
429

    
430
def get_pool_table(res_type):
431
    if res_type == "bridge":
432
        return BridgePoolTable
433
    elif res_type == "mac_prefix":
434
        return MacPrefixPoolTable
435
    else:
436
        raise Exception("Unknown resource type")
437

    
438

    
439
def get_existing_users():
440
    """
441
    Retrieve user ids stored in cyclades user agnostic models.
442
    """
443
    # also check PublicKeys a user with no servers/networks exist
444
    from synnefo.ui.userdata.models import PublicKeyPair
445
    from synnefo.db.models import VirtualMachine, Network
446

    
447
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
448
                                                                  flat=True)
449
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
450
                                                                  flat=True)
451
    networkusernames = Network.objects.filter().values_list('userid',
452
                                                            flat=True)
453

    
454
    return set(list(keypairusernames) + list(serverusernames) +
455
               list(networkusernames))