Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.4 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)
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 VirtualMachine.DeletedError
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['backend_id'] = img['location']
162
    image['format'] = img['disk_format']
163
    image['metadata'] = dict((key.upper(), val)
164
                             for key, val in properties.items())
165
    image['checksum'] = img['checksum']
166

    
167
    return image
168

    
169

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

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

    
182

    
183
def get_flavor_provider(flavor):
184
    """Extract provider from disk template.
185

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

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

    
197

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

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

    
210

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

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

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

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

    
247

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

    
252

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

    
263

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

267
    This method should run inside a transaction.
268

269
    """
270
    address = None
271
    if settings.PUBLIC_USE_POOL:
272
        (network, address) = allocate_public_address(backend)
273
    else:
274
        for net in list(backend_public_networks(backend)):
275
            pool = net.get_pool()
276
            if not pool.empty():
277
                address = 'pool'
278
                network = net
279
                break
280
    if address is None:
281
        log.error("Public networks of backend %s are full", backend)
282
        raise faults.OverLimit("Can not allocate IP for new machine."
283
                        " Public networks are full.")
284
    return (network, address)
285

    
286

    
287
def backend_public_networks(backend):
288
    """Return available public networks of the backend.
289

290
    Iterator for non-deleted public networks that are available
291
    to the specified backend.
292

293
    """
294
    for network in Network.objects.filter(public=True, deleted=False):
295
        if BackendNetwork.objects.filter(network=network,
296
                                         backend=backend).exists():
297
            yield network
298

    
299

    
300
def get_network_free_address(network):
301
    """Reserve an IP address from the IP Pool of the network.
302

303
    Raises EmptyPool
304

305
    """
306

    
307
    pool = network.get_pool()
308
    address = pool.get()
309
    pool.save()
310
    return address
311

    
312

    
313
def get_nic(machine, network):
314
    try:
315
        return NetworkInterface.objects.get(machine=machine, network=network)
316
    except NetworkInterface.DoesNotExist:
317
        raise faults.ItemNotFound('Server not connected to this network.')
318

    
319

    
320
def get_nic_from_index(vm, nic_index):
321
    """Returns the nic_index-th nic of a vm
322
       Error Response Codes: itemNotFound (404), badMediaType (415)
323
    """
324
    matching_nics = vm.nics.filter(index=nic_index)
325
    matching_nics_len = len(matching_nics)
326
    if matching_nics_len < 1:
327
        raise faults.ItemNotFound('NIC not found on VM')
328
    elif matching_nics_len > 1:
329
        raise faults.BadMediaType('NIC index conflict on VM')
330
    nic = matching_nics[0]
331
    return nic
332

    
333

    
334
def render_metadata(request, metadata, use_values=False, status=200):
335
    if request.serialization == 'xml':
336
        data = render_to_string('metadata.xml', {'metadata': metadata})
337
    else:
338
        if use_values:
339
            d = {'metadata': {'values': metadata}}
340
        else:
341
            d = {'metadata': metadata}
342
        data = json.dumps(d)
343
    return HttpResponse(data, status=status)
344

    
345

    
346
def render_meta(request, meta, status=200):
347
    if request.serialization == 'xml':
348
        data = render_to_string('meta.xml', dict(key=key, val=val))
349
    else:
350
        data = json.dumps(dict(meta=meta))
351
    return HttpResponse(data, status=status)
352

    
353

    
354
def construct_nic_id(nic):
355
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
356

    
357

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

    
379

    
380
def values_from_flavor(flavor):
381
    """Get Ganeti connectivity info from flavor type.
382

383
    If link or mac_prefix equals to "pool", then the resources
384
    are allocated from the corresponding Pools.
385

386
    """
387
    try:
388
        flavor = Network.FLAVORS[flavor]
389
    except KeyError:
390
        raise faults.BadRequest("Unknown network flavor")
391

    
392
    mode = flavor.get("mode")
393

    
394
    link = flavor.get("link")
395
    if link == "pool":
396
        link = allocate_resource("bridge")
397

    
398
    mac_prefix = flavor.get("mac_prefix")
399
    if mac_prefix == "pool":
400
        mac_prefix = allocate_resource("mac_prefix")
401

    
402
    tags = flavor.get("tags")
403

    
404
    return mode, link, mac_prefix, tags
405

    
406

    
407
def allocate_resource(res_type):
408
    table = get_pool_table(res_type)
409
    pool = table.get_pool()
410
    value = pool.get()
411
    pool.save()
412
    return value
413

    
414

    
415
def release_resource(res_type, value):
416
    table = get_pool_table(res_type)
417
    pool = table.get_pool()
418
    pool.put(value)
419
    pool.save()
420

    
421

    
422
def get_pool_table(res_type):
423
    if res_type == "bridge":
424
        return BridgePoolTable
425
    elif res_type == "mac_prefix":
426
        return MacPrefixPoolTable
427
    else:
428
        raise Exception("Unknown resource type")
429

    
430

    
431
def get_existing_users():
432
    """
433
    Retrieve user ids stored in cyclades user agnostic models.
434
    """
435
    # also check PublicKeys a user with no servers/networks exist
436
    from synnefo.ui.userdata.models import PublicKeyPair
437
    from synnefo.db.models import VirtualMachine, Network
438

    
439
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
440
                                                                  flat=True)
441
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
442
                                                                  flat=True)
443
    networkusernames = Network.objects.filter().values_list('userid',
444
                                                            flat=True)
445

    
446
    return set(list(keypairusernames) + list(serverusernames) +
447
               list(networkusernames))