util.py 16.1 KB
Newer Older
1
# Copyright 2011-2014 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.backend import PlanktonBackend
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
        if prefetch_related is not None:
141
142
143
144
            if isinstance(prefetch_related, list):
                servers = servers.prefetch_related(*prefetch_related)
            else:
                servers = servers.prefetch_related(prefetch_related)
145
        vm = servers.get(id=server_id, userid=user_id)
146
        if non_deleted and vm.deleted:
147
            raise faults.BadRequest("Server has been deleted.")
148
        if non_suspended and vm.suspended:
149
            raise faults.Forbidden("Administratively Suspended VM")
Christos Stavrakakis's avatar
Christos Stavrakakis committed
150
        return vm
151
    except (ValueError, TypeError):
152
        raise faults.BadRequest('Invalid server ID.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
153
    except VirtualMachine.DoesNotExist:
154
        raise faults.ItemNotFound('Server not found.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
155

156

Giorgos Verigakis's avatar
Giorgos Verigakis committed
157
def get_vm_meta(vm, key):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
158
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
159

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

165

166
def get_image(image_id, user_id):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
167
    """Return an Image instance or raise ItemNotFound."""
168

169
    with PlanktonBackend(user_id) as backend:
170
        return backend.get_image(image_id)
171

172

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

184
185
186
187
    mapfile = img["mapfile"]
    if mapfile.startswith("archip:"):
        _, unprefixed_mapfile, = mapfile.split("archip:")
        mapfile = unprefixed_mapfile
188
    else:
189
190
        unprefixed_mapfile = mapfile
        mapfile = "pithos:" + mapfile
191

192
    image["backend_id"] = PITHOSMAP_PREFIX + "/".join([unprefixed_mapfile,
193
                                                       str(size)])
194
    image["mapfile"] = mapfile
195
196
197

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

200
201
202
    return image


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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
206
207
    try:
        flavor_id = int(flavor_id)
208
209
210
211
        if include_deleted:
            return Flavor.objects.get(id=flavor_id)
        else:
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
212
213
214
    except (ValueError, TypeError):
        raise faults.BadRequest("Invalid flavor ID '%s'" % flavor_id)
    except Flavor.DoesNotExist:
215
        raise faults.ItemNotFound('Flavor not found.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
216

217

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

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


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

253

254
255
256
257
258
259
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")
260

261

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


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


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
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)
306
307


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

318

319
320
321
322
323
324
325
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)


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

337

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

346

347
def verify_personality(personality):
348
    """Verify that a a list of personalities is well formed"""
Christos Stavrakakis's avatar
Christos Stavrakakis committed
349
    if len(personality) > settings.MAX_PERSONALITY:
350
        raise faults.OverLimit("Maximum number of personalities"
Christos Stavrakakis's avatar
Christos Stavrakakis committed
351
                               " exceeded")
352
353
354
355
356
357
358
359
360
361
    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
362
                raise faults.OverLimit("Maximum size of personality exceeded")
363
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
364
                raise faults.OverLimit("Maximum size of personality exceeded")
365
        except (AssertionError, TypeError):
366
            raise faults.BadRequest("Malformed personality in request")
367
368


369
370
371
372
373
374
375
376
377
378
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:
379
        raise faults.BadRequest("Unknown network flavor")
380
381
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

    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")
418
419
420
421
422
423
424


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

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

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


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


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


449
450
451
452
453
454
455
456
457
458
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")]


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


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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
471

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