Statistics
| Branch: | Tag: | Revision:

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

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
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

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

    
76

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

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

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

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

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

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

    
104
    return password
105

    
106

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

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

    
114

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

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

    
123

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

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

    
144

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

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

    
153

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

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

    
160

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

    
173
    return image
174

    
175

    
176
def get_flavor(flavor_id, include_deleted=False):
177
    """Return a Flavor instance or raise ItemNotFound."""
178

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

    
188

    
189
def get_flavor_provider(flavor):
190
    """Extract provider from disk template.
191

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

196
    """
197
    disk_template = flavor.disk_template
198
    provider = None
199
    if disk_template.startswith("ext"):
200
        disk_template, provider = disk_template.split("_", 1)
201
    return disk_template, provider
202

    
203

    
204
def get_network(network_id, user_id, for_update=False, non_deleted=False):
205
    """Return a Network instance or raise ItemNotFound."""
206

    
207
    try:
208
        network_id = int(network_id)
209
        objects = Network.objects
210
        if for_update:
211
            objects = objects.select_for_update()
212
        network = objects.get(Q(userid=user_id) | Q(public=True),
213
                              id=network_id)
214
        if non_deleted and network.deleted:
215
            raise faults.BadRequest("Network has been deleted.")
216
        return network
217
    except (ValueError, Network.DoesNotExist):
218
        raise faults.ItemNotFound('Network not found.')
219

    
220

    
221
def get_floating_ip(user_id, ipv4, for_update=False):
222
    try:
223
        objects = FloatingIP.objects
224
        if for_update:
225
            objects = objects.select_for_update()
226
        return objects.get(userid=user_id, ipv4=ipv4, deleted=False)
227
    except FloatingIP.DoesNotExist:
228
        raise faults.ItemNotFound("Floating IP does not exist.")
229

    
230

    
231
def allocate_public_address(backend):
232
    """Get a public IP for any available network of a backend."""
233
    # Guarantee exclusive access to backend, because accessing the IP pools of
234
    # the backend networks may result in a deadlock with backend allocator
235
    # which also checks that backend networks have a free IP.
236
    backend = Backend.objects.select_for_update().get(id=backend.id)
237
    public_networks = backend_public_networks(backend)
238
    return get_free_ip(public_networks)
239

    
240

    
241
def backend_public_networks(backend):
242
    """Return available public networks of the backend.
243

244
    Iterator for non-deleted public networks that are available
245
    to the specified backend.
246

247
    """
248
    bnets = BackendNetwork.objects.filter(backend=backend,
249
                                          network__public=True,
250
                                          network__deleted=False,
251
                                          network__floating_ip_pool=False,
252
                                          network__subnet__isnull=False,
253
                                          network__drained=False)
254
    return [b.network for b in bnets]
255

    
256

    
257
def get_free_ip(networks):
258
    for network in networks:
259
        try:
260
            address = get_network_free_address(network)
261
            return network, address
262
        except faults.OverLimit:
263
            pass
264
    msg = "Can not allocate public IP. Public networks are full."
265
    log.error(msg)
266
    raise faults.OverLimit(msg)
267

    
268

    
269
def get_network_free_address(network):
270
    """Reserve an IP address from the IP Pool of the network."""
271

    
272
    pool = network.get_pool()
273
    try:
274
        address = pool.get()
275
    except EmptyPool:
276
        raise faults.OverLimit("Network %s is full." % network.backend_id)
277
    pool.save()
278
    return address
279

    
280

    
281
def get_nic(machine, network):
282
    try:
283
        return NetworkInterface.objects.get(machine=machine, network=network)
284
    except NetworkInterface.DoesNotExist:
285
        raise faults.ItemNotFound('Server not connected to this network.')
286

    
287

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

    
301

    
302
def render_metadata(request, metadata, use_values=False, status=200):
303
    if request.serialization == 'xml':
304
        data = render_to_string('metadata.xml', {'metadata': metadata})
305
    else:
306
        if use_values:
307
            d = {'metadata': {'values': metadata}}
308
        else:
309
            d = {'metadata': metadata}
310
        data = json.dumps(d)
311
    return HttpResponse(data, status=status)
312

    
313

    
314
def render_meta(request, meta, status=200):
315
    if request.serialization == 'xml':
316
        key, val = meta.items()[0]
317
        data = render_to_string('meta.xml', dict(key=key, val=val))
318
    else:
319
        data = json.dumps(dict(meta=meta))
320
    return HttpResponse(data, status=status)
321

    
322

    
323
def construct_nic_id(nic):
324
    return "-".join(["nic", unicode(nic.machine.id), unicode(nic.index)])
325

    
326

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

    
348

    
349
def values_from_flavor(flavor):
350
    """Get Ganeti connectivity info from flavor type.
351

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

355
    """
356
    try:
357
        flavor = Network.FLAVORS[flavor]
358
    except KeyError:
359
        raise faults.BadRequest("Unknown network flavor")
360

    
361
    mode = flavor.get("mode")
362

    
363
    link = flavor.get("link")
364
    if link == "pool":
365
        link = allocate_resource("bridge")
366

    
367
    mac_prefix = flavor.get("mac_prefix")
368
    if mac_prefix == "pool":
369
        mac_prefix = allocate_resource("mac_prefix")
370

    
371
    tags = flavor.get("tags")
372

    
373
    return mode, link, mac_prefix, tags
374

    
375

    
376
def allocate_resource(res_type):
377
    table = get_pool_table(res_type)
378
    pool = table.get_pool()
379
    value = pool.get()
380
    pool.save()
381
    return value
382

    
383

    
384
def release_resource(res_type, value):
385
    table = get_pool_table(res_type)
386
    pool = table.get_pool()
387
    pool.put(value)
388
    pool.save()
389

    
390

    
391
def get_pool_table(res_type):
392
    if res_type == "bridge":
393
        return BridgePoolTable
394
    elif res_type == "mac_prefix":
395
        return MacPrefixPoolTable
396
    else:
397
        raise Exception("Unknown resource type")
398

    
399

    
400
def get_existing_users():
401
    """
402
    Retrieve user ids stored in cyclades user agnostic models.
403
    """
404
    # also check PublicKeys a user with no servers/networks exist
405
    from synnefo.userdata.models import PublicKeyPair
406
    from synnefo.db.models import VirtualMachine, Network
407

    
408
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
409
                                                                  flat=True)
410
    serverusernames = VirtualMachine.objects.filter().values_list('userid',
411
                                                                  flat=True)
412
    networkusernames = Network.objects.filter().values_list('userid',
413
                                                            flat=True)
414

    
415
    return set(list(keypairusernames) + list(serverusernames) +
416
               list(networkusernames))
417

    
418

    
419
def vm_to_links(vm_id):
420
    href = join_urls(SERVERS_URL, str(vm_id))
421
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
422

    
423

    
424
def network_to_links(network_id):
425
    href = join_urls(NETWORKS_URL, str(network_id))
426
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
427

    
428

    
429
def flavor_to_links(flavor_id):
430
    href = join_urls(FLAVORS_URL, str(flavor_id))
431
    return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
432

    
433

    
434
def image_to_links(image_id):
435
    href = join_urls(IMAGES_URL, str(image_id))
436
    links = [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
437
    links.append({"rel": "alternate",
438
                  "href": join_urls(IMAGES_PLANKTON_URL, str(image_id))})
439
    return links
440

    
441

    
442
def start_action(vm, action, jobId):
443
    vm.action = action
444
    vm.backendjobid = jobId
445
    vm.backendopcode = None
446
    vm.backendjobstatus = None
447
    vm.backendlogmsg = None
448
    vm.save()