util.py 16.1 KB
Newer Older
1
# Copyright 2011-2012 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
from base64 import urlsafe_b64encode, b64decode
from urllib import quote
36
from hashlib import sha256
37
from logging import getLogger
Giorgos Verigakis's avatar
Giorgos Verigakis committed
38
from random import choice
39
from string import digits, lowercase, uppercase
40

41
from Crypto.Cipher import AES
Giorgos Verigakis's avatar
Giorgos Verigakis committed
42

Giorgos Verigakis's avatar
Giorgos Verigakis committed
43
from django.conf import settings
44
45
from django.http import HttpResponse
from django.template.loader import render_to_string
46
from django.utils import simplejson as json
47
from django.db.models import Q
48

49
from snf_django.lib.api import faults
50
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
51
52
                               Network, NetworkInterface, SecurityGroup,
                               BridgePoolTable, MacPrefixPoolTable, IPAddress,
53
                               IPPoolTable)
54
from synnefo.plankton.utils import image_backend
55

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
from synnefo.lib.services import get_service_path
from synnefo.lib import join_urls

COMPUTE_URL = \
    join_urls(BASE_HOST,
              get_service_path(cyclades_services, "compute", version="v2.0"))
SERVERS_URL = join_urls(COMPUTE_URL, "servers/")
FLAVORS_URL = join_urls(COMPUTE_URL, "flavors/")
IMAGES_URL = join_urls(COMPUTE_URL, "images/")
PLANKTON_URL = \
    join_urls(BASE_HOST,
              get_service_path(cyclades_services, "image", version="v1.0"))
IMAGES_PLANKTON_URL = join_urls(PLANKTON_URL, "images/")

71
72
73
74
75
76
77
78
NETWORK_URL = \
    join_urls(BASE_HOST,
              get_service_path(cyclades_services, "network", version="v2.0"))
NETWORKS_URL = join_urls(NETWORK_URL, "networks/")
PORTS_URL = join_urls(NETWORK_URL, "ports/")
SUBNETS_URL = join_urls(NETWORK_URL, "subnets/")
FLOATING_IPS_URL = join_urls(NETWORK_URL, "floatingips/")

79
PITHOSMAP_PREFIX = "pithosmap://"
80
81
82

log = getLogger('synnefo.api')

83

84
85
def random_password():
    """Generates a random password
86

87
    We generate a windows compliant password: it must contain at least
88
89
    one charachter from each of the groups: upper case, lower case, digits.
    """
90

91
92
93
94
95
    pool = lowercase + uppercase + digits
    lowerset = set(lowercase)
    upperset = set(uppercase)
    digitset = set(digits)
    length = 10
96

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

99
100
101
102
103
104
105
106
    # Make sure the password is compliant
    chars = set(password)
    if not chars & lowerset:
        password += choice(lowercase)
    if not chars & upperset:
        password += choice(uppercase)
    if not chars & digitset:
        password += choice(digits)
107

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

111
112
    return password

Giorgos Verigakis's avatar
Giorgos Verigakis committed
113

114
115
116
def zeropad(s):
    """Add zeros at the end of a string in order to make its length
       a multiple of 16."""
117

118
119
120
    npad = 16 - len(s) % 16
    return s + '\x00' * npad

121

122
def stats_encrypt(plaintext):
123
    # Make sure key is 32 bytes long
124
    key = sha256(settings.CYCLADES_STATS_SECRET_KEY).digest()
125

126
127
    aes = AES.new(key)
    enc = aes.encrypt(zeropad(plaintext))
128
    return quote(urlsafe_b64encode(enc))
129

130

131
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
132
           non_suspended=False, prefetch_related=None):
133
    """Find a VirtualMachine instance based on ID and owner."""
134

Giorgos Verigakis's avatar
Giorgos Verigakis committed
135
136
    try:
        server_id = int(server_id)
137
138
139
        servers = VirtualMachine.objects
        if for_update:
            servers = servers.select_for_update()
140
141
        if prefetch_related is not None:
            servers = servers.prefetch_related(prefetch_related)
142
        vm = servers.get(id=server_id, userid=user_id)
143
        if non_deleted and vm.deleted:
144
            raise faults.BadRequest("Server has been deleted.")
145
        if non_suspended and vm.suspended:
146
            raise faults.Forbidden("Administratively Suspended VM")
Christos Stavrakakis's avatar
Christos Stavrakakis committed
147
        return vm
148
    except (ValueError, TypeError):
149
        raise faults.BadRequest('Invalid server ID.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
150
    except VirtualMachine.DoesNotExist:
151
        raise faults.ItemNotFound('Server not found.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
152

153

Giorgos Verigakis's avatar
Giorgos Verigakis committed
154
def get_vm_meta(vm, key):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
155
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
156

Giorgos Verigakis's avatar
Giorgos Verigakis committed
157
    try:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
158
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
159
    except VirtualMachineMetadata.DoesNotExist:
160
        raise faults.ItemNotFound('Metadata key not found.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
161

162

163
def get_image(image_id, user_id):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
164
    """Return an Image instance or raise ItemNotFound."""
165

166
    with image_backend(user_id) as backend:
167
        return backend.get_image(image_id)
168

169

170
171
172
def get_image_dict(image_id, user_id):
    image = {}
    img = get_image(image_id, user_id)
173
174
    image["id"] = img["id"]
    image["name"] = img["name"]
175
176
177
178
179
180
181
182
183
184
    image["format"] = img["disk_format"]
    image["checksum"] = img["checksum"]
    image["location"] = img["location"]

    checksum = image["checksum"] = img["checksum"]
    size = image["size"] = img["size"]
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([checksum, str(size)])

    properties = img.get("properties", {})
    image["metadata"] = dict((key.upper(), val)
185
                             for key, val in properties.items())
186

187
188
189
    return image


190
def get_flavor(flavor_id, include_deleted=False):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
191
    """Return a Flavor instance or raise ItemNotFound."""
192

Giorgos Verigakis's avatar
Giorgos Verigakis committed
193
194
    try:
        flavor_id = int(flavor_id)
195
196
197
198
        if include_deleted:
            return Flavor.objects.get(id=flavor_id)
        else:
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
199
200
201
    except (ValueError, TypeError):
        raise faults.BadRequest("Invalid flavor ID '%s'" % flavor_id)
    except Flavor.DoesNotExist:
202
        raise faults.ItemNotFound('Flavor not found.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
203

204

Christos Stavrakakis's avatar
Christos Stavrakakis committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def get_flavor_provider(flavor):
    """Extract provider from disk template.

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

    """
    disk_template = flavor.disk_template
    provider = None
    if disk_template.startswith("ext"):
        disk_template, provider = disk_template.split("_", 1)
    return disk_template, provider

219

220
def get_network(network_id, user_id, for_update=False, non_deleted=False):
221
    """Return a Network instance or raise ItemNotFound."""
222

223
    try:
224
        network_id = int(network_id)
225
        objects = Network.objects
226
        if for_update:
227
            objects = objects.select_for_update()
228
229
230
        network = objects.get(Q(userid=user_id) | Q(public=True),
                              id=network_id)
        if non_deleted and network.deleted:
231
            raise faults.BadRequest("Network has been deleted.")
232
        return network
233
234
235
    except (ValueError, TypeError):
        raise faults.BadRequest("Invalid network ID '%s'" % network_id)
    except Network.DoesNotExist:
236
        raise faults.ItemNotFound('Network %s not found.' % network_id)
237
238
239


def get_port(port_id, user_id, for_update=False):
Marios Kogias's avatar
Marios Kogias committed
240
    """
241
    Return a NetworkInteface instance or raise ItemNotFound.
Marios Kogias's avatar
Marios Kogias committed
242
243
    """
    try:
244
        objects = NetworkInterface.objects.filter(userid=user_id)
Marios Kogias's avatar
Marios Kogias committed
245
246
        if for_update:
            objects = objects.select_for_update()
247
        # if (port.device_owner != "vm") and for_update:
248
        #     raise faults.BadRequest('Cannot update non vm port')
249
        return objects.get(id=port_id)
250
251
252
    except (ValueError, TypeError):
        raise faults.BadRequest("Invalid port ID '%s'" % port_id)
    except NetworkInterface.DoesNotExist:
253
        raise faults.ItemNotFound("Port '%s' not found." % port_id)
254

255

256
257
258
259
260
261
def get_security_group(sg_id):
    try:
        sg = SecurityGroup.objects.get(id=sg_id)
        return sg
    except (ValueError, SecurityGroup.DoesNotExist):
        raise faults.ItemNotFound("Not valid security group")
262

263

264
def get_floating_ip_by_address(userid, address, for_update=False):
265
    try:
266
        objects = IPAddress.objects
267
268
        if for_update:
            objects = objects.select_for_update()
269
270
        return objects.get(userid=userid, floating_ip=True,
                           address=address, deleted=False)
271
    except IPAddress.DoesNotExist:
272
273
274
        raise faults.ItemNotFound("Floating IP does not exist.")


275
276
def get_floating_ip_by_id(userid, floating_ip_id, for_update=False):
    try:
277
        floating_ip_id = int(floating_ip_id)
278
279
280
        objects = IPAddress.objects
        if for_update:
            objects = objects.select_for_update()
281
        return objects.get(id=floating_ip_id, floating_ip=True,
282
                           userid=userid, deleted=False)
283
    except IPAddress.DoesNotExist:
284
        raise faults.ItemNotFound("Floating IP with ID %s does not exist." %
285
                                  floating_ip_id)
286
287
    except (ValueError, TypeError):
        raise faults.BadRequest("Invalid Floating IP ID %s" % floating_ip_id)
288
289


290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def backend_has_free_public_ip(backend):
    """Check if a backend has a free public IPv4 address."""
    ip_pool_rows = IPPoolTable.objects.select_for_update()\
        .filter(subnet__network__public=True)\
        .filter(subnet__network__drained=False)\
        .filter(subnet__deleted=False)\
        .filter(subnet__network__backend_networks__backend=backend)
    for pool_row in ip_pool_rows:
        pool = pool_row.pool
        if pool.empty():
            continue
        else:
            return True


def backend_public_networks(backend):
    return Network.objects.filter(deleted=False, public=True,
                                  backend_networks__backend=backend)
308
309


310
def get_vm_nic(vm, nic_id):
311
    """Get a VMs NIC by its ID."""
Giorgos Verigakis's avatar
Giorgos Verigakis committed
312
    try:
313
        nic_id = int(nic_id)
314
        return vm.nics.get(id=nic_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
315
    except NetworkInterface.DoesNotExist:
316
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)
317
318
    except (ValueError, TypeError):
        raise faults.BadRequest("Invalid NIC ID '%s'" % nic_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
319

320

321
322
323
324
325
326
327
def get_nic(nic_id):
    try:
        return NetworkInterface.objects.get(id=nic_id)
    except NetworkInterface.DoesNotExist:
        raise faults.ItemNotFound("NIC '%s' not found" % nic_id)


328
329
330
331
def render_metadata(request, metadata, use_values=False, status=200):
    if request.serialization == 'xml':
        data = render_to_string('metadata.xml', {'metadata': metadata})
    else:
332
333
334
335
        if use_values:
            d = {'metadata': {'values': metadata}}
        else:
            d = {'metadata': metadata}
336
337
338
        data = json.dumps(d)
    return HttpResponse(data, status=status)

339

340
341
def render_meta(request, meta, status=200):
    if request.serialization == 'xml':
Christos Stavrakakis's avatar
Christos Stavrakakis committed
342
        key, val = meta.items()[0]
343
        data = render_to_string('meta.xml', dict(key=key, val=val))
344
    else:
345
        data = json.dumps(dict(meta=meta))
346
347
    return HttpResponse(data, status=status)

348

349
def verify_personality(personality):
350
    """Verify that a a list of personalities is well formed"""
Christos Stavrakakis's avatar
Christos Stavrakakis committed
351
    if len(personality) > settings.MAX_PERSONALITY:
352
        raise faults.OverLimit("Maximum number of personalities"
Christos Stavrakakis's avatar
Christos Stavrakakis committed
353
                               " exceeded")
354
355
356
357
358
359
360
361
362
363
    for p in personality:
        # Verify that personalities are well-formed
        try:
            assert isinstance(p, dict)
            keys = set(p.keys())
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
            assert keys.issubset(allowed)
            contents = p['contents']
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
                # No need to decode if contents already exceed limit
364
                raise faults.OverLimit("Maximum size of personality exceeded")
365
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
366
                raise faults.OverLimit("Maximum size of personality exceeded")
367
        except (AssertionError, TypeError):
368
            raise faults.BadRequest("Malformed personality in request")
369
370


371
372
373
374
375
376
377
378
379
380
def values_from_flavor(flavor):
    """Get Ganeti connectivity info from flavor type.

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

    """
    try:
        flavor = Network.FLAVORS[flavor]
    except KeyError:
381
        raise faults.BadRequest("Unknown network flavor")
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419

    mode = flavor.get("mode")

    link = flavor.get("link")
    if link == "pool":
        link = allocate_resource("bridge")

    mac_prefix = flavor.get("mac_prefix")
    if mac_prefix == "pool":
        mac_prefix = allocate_resource("mac_prefix")

    tags = flavor.get("tags")

    return mode, link, mac_prefix, tags


def allocate_resource(res_type):
    table = get_pool_table(res_type)
    pool = table.get_pool()
    value = pool.get()
    pool.save()
    return value


def release_resource(res_type, value):
    table = get_pool_table(res_type)
    pool = table.get_pool()
    pool.put(value)
    pool.save()


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


def get_existing_users():
    """
    Retrieve user ids stored in cyclades user agnostic models.
    """
    # also check PublicKeys a user with no servers/networks exist
427
    from synnefo.userdata.models import PublicKeyPair
428
429
430
    from synnefo.db.models import VirtualMachine, Network

    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
Christos Stavrakakis's avatar
Christos Stavrakakis committed
431
                                                                  flat=True)
432
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
Christos Stavrakakis's avatar
Christos Stavrakakis committed
433
                                                                  flat=True)
434
435
436
    networkusernames = Network.objects.filter().values_list('userid',
                                                            flat=True)

Christos Stavrakakis's avatar
Christos Stavrakakis committed
437
438
    return set(list(keypairusernames) + list(serverusernames) +
               list(networkusernames))
439
440
441


def vm_to_links(vm_id):
442
443
    href = join_urls(SERVERS_URL, str(vm_id))
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
444
445
446


def network_to_links(network_id):
447
448
    href = join_urls(NETWORKS_URL, str(network_id))
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
449
450


451
452
453
454
455
456
457
458
459
460
def subnet_to_links(subnet_id):
    href = join_urls(SUBNETS_URL, str(subnet_id))
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]


def port_to_links(port_id):
    href = join_urls(PORTS_URL, str(port_id))
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]


461
def flavor_to_links(flavor_id):
462
463
    href = join_urls(FLAVORS_URL, str(flavor_id))
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
464
465
466


def image_to_links(image_id):
467
468
    href = join_urls(IMAGES_URL, str(image_id))
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
469
    links.append({"rel": "alternate",
470
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
471
    return links
472

Christos Stavrakakis's avatar
Christos Stavrakakis committed
473

474
475
476
477
478
479
480
def start_action(vm, action, jobId):
    vm.action = action
    vm.backendjobid = jobId
    vm.backendopcode = None
    vm.backendjobstatus = None
    vm.backendlogmsg = None
    vm.save()