Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.9 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
from base64 import b64encode, b64decode
35
from hashlib import sha256
36
from logging import getLogger
37
from random import choice
38
from string import digits, lowercase, uppercase
39

    
40
from Crypto.Cipher import AES
41

    
42
from django.conf import settings
43
from django.http import HttpResponse
44
from django.template.loader import render_to_string
45
from django.utils import simplejson as json
46
from django.db.models import Q
47

    
48
from snf_django.lib.api import faults
49
from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata,
50
                               Network, BackendNetwork, NetworkInterface,
51
                               BridgePoolTable, MacPrefixPoolTable, Backend,
52
                               FloatingIP)
53
from synnefo.db.pools import EmptyPool
54

    
55
from synnefo.plankton.utils import image_backend
56

    
57
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
58
from synnefo.lib.services import get_service_path
59
from synnefo.lib import join_urls
60

    
61
COMPUTE_URL = \
62
    join_urls(BASE_HOST,
63
              get_service_path(cyclades_services, "compute", version="v2.0"))
64
SERVERS_URL = join_urls(COMPUTE_URL, "servers/")
65
NETWORKS_URL = join_urls(COMPUTE_URL, "networks/")
66
FLAVORS_URL = join_urls(COMPUTE_URL, "flavors/")
67
IMAGES_URL = join_urls(COMPUTE_URL, "images/")
68
PLANKTON_URL = \
69
    join_urls(BASE_HOST,
70
              get_service_path(cyclades_services, "image", version="v1.0"))
71
IMAGES_PLANKTON_URL = join_urls(PLANKTON_URL, "images/")
72

    
73
PITHOSMAP_PREFIX = "pithosmap://"
74

    
75
log = getLogger('synnefo.api')
76

    
77

    
78
def random_password():
79
    """Generates a random password
80

81
    We generate a windows compliant password: it must contain at least
82
    one charachter from each of the groups: upper case, lower case, digits.
83
    """
84

    
85
    pool = lowercase + uppercase + digits
86
    lowerset = set(lowercase)
87
    upperset = set(uppercase)
88
    digitset = set(digits)
89
    length = 10
90

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

    
93
    # Make sure the password is compliant
94
    chars = set(password)
95
    if not chars & lowerset:
96
        password += choice(lowercase)
97
    if not chars & upperset:
98
        password += choice(uppercase)
99
    if not chars & digitset:
100
        password += choice(digits)
101

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

    
105
    return password
106

    
107

    
108
def zeropad(s):
109
    """Add zeros at the end of a string in order to make its length
110
       a multiple of 16."""
111

    
112
    npad = 16 - len(s) % 16
113
    return s + '\x00' * npad
114

    
115

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

    
120
    aes = AES.new(key)
121
    enc = aes.encrypt(zeropad(plaintext))
122
    return b64encode(enc)
123

    
124

    
125
def get_vm(server_id, user_id, for_update=False, non_deleted=False,
126
           non_suspended=False):
127
    """Find a VirtualMachine instance based on ID and owner."""
128

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

    
145

    
146
def get_vm_meta(vm, key):
147
    """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
148

    
149
    try:
150
        return VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
151
    except VirtualMachineMetadata.DoesNotExist:
152
        raise faults.ItemNotFound('Metadata key not found.')
153

    
154

    
155
def get_image(image_id, user_id):
156
    """Return an Image instance or raise ItemNotFound."""
157

    
158
    with image_backend(user_id) as backend:
159
        return backend.get_image(image_id)
160

    
161

    
162
def get_image_dict(image_id, user_id):
163
    image = {}
164
    img = get_image(image_id, user_id)
165
    image["id"] = img["id"]
166
    image["name"] = img["name"]
167
    image["format"] = img["disk_format"]
168
    image["checksum"] = img["checksum"]
169
    image["location"] = img["location"]
170

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

    
175
    properties = img.get("properties", {})
176
    image["metadata"] = dict((key.upper(), val)
177
                             for key, val in properties.items())
178

    
179

    
180
    return image
181

    
182

    
183
def get_flavor(flavor_id, include_deleted=False):
184
    """Return a Flavor instance or raise ItemNotFound."""
185

    
186
    try:
187
        flavor_id = int(flavor_id)
188
        if include_deleted:
189
            return Flavor.objects.get(id=flavor_id)
190
        else:
191
            return Flavor.objects.get(id=flavor_id, deleted=include_deleted)
192
    except (ValueError, Flavor.DoesNotExist):
193
        raise faults.ItemNotFound('Flavor not found.')
194

    
195

    
196
def get_flavor_provider(flavor):
197
    """Extract provider from disk template.
198

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

203
    """
204
    disk_template = flavor.disk_template
205
    provider = None
206
    if disk_template.startswith("ext"):
207
        disk_template, provider = disk_template.split("_", 1)
208
    return disk_template, provider
209

    
210

    
211
def get_network(network_id, user_id, for_update=False, non_deleted=False):
212
    """Return a Network instance or raise ItemNotFound."""
213

    
214
    try:
215
        network_id = int(network_id)
216
        objects = Network.objects
217
        if for_update:
218
            objects = objects.select_for_update()
219
        network = objects.get(Q(userid=user_id) | Q(public=True),
220
                              id=network_id)
221
        if non_deleted and network.deleted:
222
            raise faults.BadRequest("Network has been deleted.")
223
        return network
224
    except (ValueError, Network.DoesNotExist):
225
        raise faults.ItemNotFound('Network not found.')
226

    
227

    
228
def get_floating_ip(user_id, ipv4, for_update=False):
229
    try:
230
        objects = FloatingIP.objects
231
        if for_update:
232
            objects = objects.select_for_update()
233
        return objects.get(userid=user_id, ipv4=ipv4, deleted=False)
234
    except FloatingIP.DoesNotExist:
235
        raise faults.ItemNotFound("Floating IP does not exist.")
236

    
237

    
238
def allocate_public_address(backend):
239
    """Get a public IP for any available network of a backend."""
240
    # Guarantee exclusive access to backend, because accessing the IP pools of
241
    # the backend networks may result in a deadlock with backend allocator
242
    # which also checks that backend networks have a free IP.
243
    backend = Backend.objects.select_for_update().get(id=backend.id)
244
    public_networks = backend_public_networks(backend)
245
    return get_free_ip(public_networks)
246

    
247

    
248
def backend_public_networks(backend):
249
    """Return available public networks of the backend.
250

251
    Iterator for non-deleted public networks that are available
252
    to the specified backend.
253

254
    """
255
    bnets = BackendNetwork.objects.filter(backend=backend,
256
                                          network__public=True,
257
                                          network__deleted=False,
258
                                          network__floating_ip_pool=False,
259
                                          network__subnet__isnull=False,
260
                                          network__drained=False)
261
    return [b.network for b in bnets]
262

    
263

    
264
def get_free_ip(networks):
265
    for network in networks:
266
        try:
267
            address = get_network_free_address(network)
268
            return network, address
269
        except faults.OverLimit:
270
            pass
271
    msg = "Can not allocate public IP. Public networks are full."
272
    log.error(msg)
273
    raise faults.OverLimit(msg)
274

    
275

    
276
def get_network_free_address(network):
277
    """Reserve an IP address from the IP Pool of the network."""
278

    
279
    pool = network.get_pool()
280
    try:
281
        address = pool.get()
282
    except EmptyPool:
283
        raise faults.OverLimit("Network %s is full." % network.backend_id)
284
    pool.save()
285
    return address
286

    
287

    
288
def get_nic(machine, network):
289
    try:
290
        return NetworkInterface.objects.get(machine=machine, network=network)
291
    except NetworkInterface.DoesNotExist:
292
        raise faults.ItemNotFound('Server not connected to this network.')
293

    
294

    
295
def get_nic_from_index(vm, nic_index):
296
    """Returns the nic_index-th nic of a vm
297
       Error Response Codes: itemNotFound (404), badMediaType (415)
298
    """
299
    matching_nics = vm.nics.filter(index=nic_index)
300
    matching_nics_len = len(matching_nics)
301
    if matching_nics_len < 1:
302
        raise faults.ItemNotFound('NIC not found on VM')
303
    elif matching_nics_len > 1:
304
        raise faults.BadMediaType('NIC index conflict on VM')
305
    nic = matching_nics[0]
306
    return nic
307

    
308

    
309
def render_metadata(request, metadata, use_values=False, status=200):
310
    if request.serialization == 'xml':
311
        data = render_to_string('metadata.xml', {'metadata': metadata})
312
    else:
313
        if use_values:
314
            d = {'metadata': {'values': metadata}}
315
        else:
316
            d = {'metadata': metadata}
317
        data = json.dumps(d)
318
    return HttpResponse(data, status=status)
319

    
320

    
321
def render_meta(request, meta, status=200):
322
    if request.serialization == 'xml':
323
        key, val = meta.items()[0]
324
        data = render_to_string('meta.xml', dict(key=key, val=val))
325
    else:
326
        data = json.dumps(dict(meta=meta))
327
    return HttpResponse(data, status=status)
328

    
329

    
330
def construct_nic_id(nic):
331
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
332

    
333

    
334
def verify_personality(personality):
335
    """Verify that a a list of personalities is well formed"""
336
    if len(personality) > settings.MAX_PERSONALITY:
337
        raise faults.OverLimit("Maximum number of personalities"
338
                               " exceeded")
339
    for p in personality:
340
        # Verify that personalities are well-formed
341
        try:
342
            assert isinstance(p, dict)
343
            keys = set(p.keys())
344
            allowed = set(['contents', 'group', 'mode', 'owner', 'path'])
345
            assert keys.issubset(allowed)
346
            contents = p['contents']
347
            if len(contents) > settings.MAX_PERSONALITY_SIZE:
348
                # No need to decode if contents already exceed limit
349
                raise faults.OverLimit("Maximum size of personality exceeded")
350
            if len(b64decode(contents)) > settings.MAX_PERSONALITY_SIZE:
351
                raise faults.OverLimit("Maximum size of personality exceeded")
352
        except AssertionError:
353
            raise faults.BadRequest("Malformed personality in request")
354

    
355

    
356
def values_from_flavor(flavor):
357
    """Get Ganeti connectivity info from flavor type.
358

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

362
    """
363
    try:
364
        flavor = Network.FLAVORS[flavor]
365
    except KeyError:
366
        raise faults.BadRequest("Unknown network flavor")
367

    
368
    mode = flavor.get("mode")
369

    
370
    link = flavor.get("link")
371
    if link == "pool":
372
        link = allocate_resource("bridge")
373

    
374
    mac_prefix = flavor.get("mac_prefix")
375
    if mac_prefix == "pool":
376
        mac_prefix = allocate_resource("mac_prefix")
377

    
378
    tags = flavor.get("tags")
379

    
380
    return mode, link, mac_prefix, tags
381

    
382

    
383
def allocate_resource(res_type):
384
    table = get_pool_table(res_type)
385
    pool = table.get_pool()
386
    value = pool.get()
387
    pool.save()
388
    return value
389

    
390

    
391
def release_resource(res_type, value):
392
    table = get_pool_table(res_type)
393
    pool = table.get_pool()
394
    pool.put(value)
395
    pool.save()
396

    
397

    
398
def get_pool_table(res_type):
399
    if res_type == "bridge":
400
        return BridgePoolTable
401
    elif res_type == "mac_prefix":
402
        return MacPrefixPoolTable
403
    else:
404
        raise Exception("Unknown resource type")
405

    
406

    
407
def get_existing_users():
408
    """
409
    Retrieve user ids stored in cyclades user agnostic models.
410
    """
411
    # also check PublicKeys a user with no servers/networks exist
412
    from synnefo.userdata.models import PublicKeyPair
413
    from synnefo.db.models import VirtualMachine, Network
414

    
415
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
416
                                                                  flat=True)
417
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
418
                                                                  flat=True)
419
    networkusernames = Network.objects.filter().values_list('userid',
420
                                                            flat=True)
421

    
422
    return set(list(keypairusernames) + list(serverusernames) +
423
               list(networkusernames))
424

    
425

    
426
def vm_to_links(vm_id):
427
    href = join_urls(SERVERS_URL, str(vm_id))
428
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
429

    
430

    
431
def network_to_links(network_id):
432
    href = join_urls(NETWORKS_URL, str(network_id))
433
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
434

    
435

    
436
def flavor_to_links(flavor_id):
437
    href = join_urls(FLAVORS_URL, str(flavor_id))
438
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
439

    
440

    
441
def image_to_links(image_id):
442
    href = join_urls(IMAGES_URL, str(image_id))
443
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
444
    links.append({"rel": "alternate",
445
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
446
    return links
447

    
448

    
449
def start_action(vm, action, jobId):
450
    vm.action = action
451
    vm.backendjobid = jobId
452
    vm.backendopcode = None
453
    vm.backendjobstatus = None
454
    vm.backendlogmsg = None
455
    vm.save()