Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / backend.py @ 6ec4694f

History | View | Annotate | Download (25.7 kB)

1
# Copyright 2011 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 json
35

    
36
from django.conf import settings
37
from django.db import transaction
38
from datetime import datetime
39

    
40
from synnefo.db.models import (Backend, VirtualMachine, Network,
41
                               BackendNetwork, BACKEND_STATUSES,
42
                               pooled_rapi_client, BridgePoolTable,
43
                               MacPrefixPoolTable, VirtualMachineDiagnostic)
44
from synnefo.logic import utils
45
from synnefo import quotas
46

    
47
from logging import getLogger
48
log = getLogger(__name__)
49

    
50

    
51
_firewall_tags = {
52
    'ENABLED': settings.GANETI_FIREWALL_ENABLED_TAG,
53
    'DISABLED': settings.GANETI_FIREWALL_DISABLED_TAG,
54
    'PROTECTED': settings.GANETI_FIREWALL_PROTECTED_TAG}
55

    
56
_reverse_tags = dict((v.split(':')[3], k) for k, v in _firewall_tags.items())
57

    
58

    
59
@quotas.uses_commission
60
@transaction.commit_on_success
61
def process_op_status(serials, vm, etime, jobid, opcode, status, logmsg):
62
    """Process a job progress notification from the backend
63

64
    Process an incoming message from the backend (currently Ganeti).
65
    Job notifications with a terminating status (sucess, error, or canceled),
66
    also update the operating state of the VM.
67

68
    """
69
    # See #1492, #1031, #1111 why this line has been removed
70
    #if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
71
    if status not in [x[0] for x in BACKEND_STATUSES]:
72
        raise VirtualMachine.InvalidBackendMsgError(opcode, status)
73

    
74
    vm.backendjobid = jobid
75
    vm.backendjobstatus = status
76
    vm.backendopcode = opcode
77
    vm.backendlogmsg = logmsg
78

    
79
    # Notifications of success change the operating state
80
    state_for_success = VirtualMachine.OPER_STATE_FROM_OPCODE.get(opcode, None)
81
    if status == 'success' and state_for_success is not None:
82
        vm.operstate = state_for_success
83

    
84
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
85
    if opcode == 'OP_INSTANCE_CREATE' and status in ('canceled', 'error'):
86
        vm.operstate = 'ERROR'
87
        vm.backendtime = etime
88
    elif opcode == 'OP_INSTANCE_REMOVE':
89
        # Set the deleted flag explicitly, cater for admin-initiated removals
90
        # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR,
91
        # when no instance exists at the Ganeti backend.
92
        # See ticket #799 for all the details.
93
        #
94
        if status == 'success' or (status == 'error' and
95
                                   vm.operstate == 'ERROR'):
96
            # Issue commission
97
            serial = quotas.issue_vm_commission(vm.userid, vm.flavor,
98
                                                delete=True)
99
            serials.append(serial)
100
            vm.serial = serial
101
            serial.accepted = True
102
            serial.save()
103
            release_instance_nics(vm)
104
            vm.nics.all().delete()
105
            vm.deleted = True
106
            vm.operstate = state_for_success
107
            vm.backendtime = etime
108

    
109
    # Update backendtime only for jobs that have been successfully completed,
110
    # since only these jobs update the state of the VM. Else a "race condition"
111
    # may occur when a successful job (e.g. OP_INSTANCE_REMOVE) completes
112
    # before an error job and messages arrive in reversed order.
113
    if status == 'success':
114
        vm.backendtime = etime
115

    
116
    vm.save()
117

    
118

    
119
@transaction.commit_on_success
120
def process_net_status(vm, etime, nics):
121
    """Process a net status notification from the backend
122

123
    Process an incoming message from the Ganeti backend,
124
    detailing the NIC configuration of a VM instance.
125

126
    Update the state of the VM in the DB accordingly.
127
    """
128

    
129
    ganeti_nics = process_ganeti_nics(nics)
130
    if not nics_changed(vm.nics.order_by('index'), ganeti_nics):
131
        log.debug("NICs for VM %s have not changed", vm)
132

    
133
    release_instance_nics(vm)
134

    
135
    for nic in ganeti_nics:
136
        ipv4 = nic.get('ipv4', '')
137
        if ipv4:
138
            net = nic['network']
139
            net.reserve_address(ipv4)
140

    
141
        nic['dirty'] = False
142
        vm.nics.create(**nic)
143
        # Dummy save the network, because UI uses changed-since for VMs
144
        # and Networks in order to show the VM NICs
145
        net.save()
146

    
147
    vm.backendtime = etime
148
    vm.save()
149

    
150

    
151
def process_ganeti_nics(ganeti_nics):
152
    """Process NIC dict from ganeti hooks."""
153
    new_nics = []
154
    for i, new_nic in enumerate(ganeti_nics):
155
        network = new_nic.get('network', '')
156
        n = str(network)
157
        pk = utils.id_from_network_name(n)
158

    
159
        net = Network.objects.get(pk=pk)
160

    
161
        # Get the new nic info
162
        mac = new_nic.get('mac', '')
163
        ipv4 = new_nic.get('ip', '')
164
        ipv6 = new_nic.get('ipv6', '')
165

    
166
        firewall = new_nic.get('firewall', '')
167
        firewall_profile = _reverse_tags.get(firewall, '')
168
        if not firewall_profile and net.public:
169
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
170

    
171
        nic = {
172
               'index': i,
173
               'network': net,
174
               'mac': mac,
175
               'ipv4': ipv4,
176
               'ipv6': ipv6,
177
               'firewall_profile': firewall_profile}
178

    
179
        new_nics.append(nic)
180
    return new_nics
181

    
182

    
183
def nics_changed(old_nics, new_nics):
184
    """Return True if NICs have changed in any way."""
185
    if len(old_nics) != len(new_nics):
186
        return True
187
    for old_nic, new_nic in zip(old_nics, new_nics):
188
        if not (old_nic.ipv4 == new_nic['ipv4'] and\
189
                old_nic.ipv6 == new_nic['ipv6'] and\
190
                old_nic.mac == new_nic['mac'] and\
191
                old_nic.firewall_profile == new_nic['firewall_profile'] and\
192
                old_nic.index == new_nic['index'] and\
193
                old_nic.network == new_nic['network']):
194
            return True
195
    return False
196

    
197

    
198
def release_instance_nics(vm):
199
    for nic in vm.nics.all():
200
        net = nic.network
201
        if nic.ipv4:
202
            net.release_address(nic.ipv4)
203
        nic.delete()
204
        net.save()
205

    
206

    
207
@transaction.commit_on_success
208
def process_network_status(back_network, etime, jobid, opcode, status, logmsg):
209
    if status not in [x[0] for x in BACKEND_STATUSES]:
210
        raise Network.InvalidBackendMsgError(opcode, status)
211

    
212
    back_network.backendjobid = jobid
213
    back_network.backendjobstatus = status
214
    back_network.backendopcode = opcode
215
    back_network.backendlogmsg = logmsg
216

    
217
    # Notifications of success change the operating state
218
    state_for_success = BackendNetwork.OPER_STATE_FROM_OPCODE.get(opcode, None)
219
    if status == 'success' and state_for_success is not None:
220
        back_network.operstate = state_for_success
221

    
222
    if status in ('canceled', 'error') and opcode == 'OP_NETWORK_CREATE':
223
        utils.update_state(back_network, 'ERROR')
224
        back_network.backendtime = etime
225

    
226
    if opcode == 'OP_NETWORK_REMOVE':
227
        if status == 'success' or (status == 'error' and
228
                                   back_network.operstate == 'ERROR'):
229
            back_network.operstate = state_for_success
230
            back_network.deleted = True
231
            back_network.backendtime = etime
232

    
233
    if status == 'success':
234
        back_network.backendtime = etime
235
    back_network.save()
236
    # Also you must update the state of the Network!!
237
    update_network_state(back_network.network)
238

    
239

    
240
@quotas.uses_commission
241
def update_network_state(serials, network):
242
    old_state = network.state
243

    
244
    backend_states = [s.operstate for s in network.backend_networks.all()]
245
    if not backend_states:
246
        network.state = 'PENDING'
247
        network.save()
248
        return
249

    
250
    all_equal = len(set(backend_states)) <= 1
251
    network.state = all_equal and backend_states[0] or 'PENDING'
252

    
253
    # Release the resources on the deletion of the Network
254
    if old_state != 'DELETED' and network.state == 'DELETED':
255
        log.info("Network %r deleted. Releasing link %r mac_prefix %r",
256
                 network.id, network.mac_prefix, network.link)
257
        network.deleted = True
258
        if network.mac_prefix and network.type == 'PRIVATE_MAC_FILTERED':
259
            mac_pool = MacPrefixPoolTable.get_pool()
260
            mac_pool.put(network.mac_prefix)
261
            mac_pool.save()
262

    
263
        if network.link and network.type == 'PRIVATE_VLAN':
264
            bridge_pool = BridgePoolTable.get_pool()
265
            bridge_pool.put(network.link)
266
            bridge_pool.save()
267

    
268
        # Issue commission
269
        serial = quotas.issue_network_commission(network.userid, delete=True)
270
        serials.append(serial)
271
        network.serial = serial
272
        serial.accepted = True
273
        serial.save()
274

    
275
    network.save()
276

    
277

    
278
@transaction.commit_on_success
279
def process_network_modify(back_network, etime, jobid, opcode, status,
280
                           add_reserved_ips, remove_reserved_ips):
281
    assert (opcode == "OP_NETWORK_SET_PARAMS")
282
    if status not in [x[0] for x in BACKEND_STATUSES]:
283
        raise Network.InvalidBackendMsgError(opcode, status)
284

    
285
    back_network.backendjobid = jobid
286
    back_network.backendjobstatus = status
287
    back_network.opcode = opcode
288

    
289
    if add_reserved_ips or remove_reserved_ips:
290
        net = back_network.network
291
        pool = net.get_pool()
292
        if add_reserved_ips:
293
            for ip in add_reserved_ips:
294
                pool.reserve(ip, external=True)
295
        if remove_reserved_ips:
296
            for ip in remove_reserved_ips:
297
                pool.put(ip, external=True)
298
        pool.save()
299

    
300
    if status == 'success':
301
        back_network.backendtime = etime
302
    back_network.save()
303

    
304

    
305
@transaction.commit_on_success
306
def process_create_progress(vm, etime, progress):
307

    
308
    percentage = int(progress)
309

    
310
    # The percentage may exceed 100%, due to the way
311
    # snf-image:copy-progress tracks bytes read by image handling processes
312
    percentage = 100 if percentage > 100 else percentage
313
    if percentage < 0:
314
        raise ValueError("Percentage cannot be negative")
315

    
316
    # FIXME: log a warning here, see #1033
317
#   if last_update > percentage:
318
#       raise ValueError("Build percentage should increase monotonically " \
319
#                        "(old = %d, new = %d)" % (last_update, percentage))
320

    
321
    # This assumes that no message of type 'ganeti-create-progress' is going to
322
    # arrive once OP_INSTANCE_CREATE has succeeded for a Ganeti instance and
323
    # the instance is STARTED.  What if the two messages are processed by two
324
    # separate dispatcher threads, and the 'ganeti-op-status' message for
325
    # successful creation gets processed before the 'ganeti-create-progress'
326
    # message? [vkoukis]
327
    #
328
    #if not vm.operstate == 'BUILD':
329
    #    raise VirtualMachine.IllegalState("VM is not in building state")
330

    
331
    vm.buildpercentage = percentage
332
    vm.backendtime = etime
333
    vm.save()
334

    
335

    
336
def create_instance_diagnostic(vm, message, source, level="DEBUG", etime=None,
337
    details=None):
338
    """
339
    Create virtual machine instance diagnostic entry.
340

341
    :param vm: VirtualMachine instance to create diagnostic for.
342
    :param message: Diagnostic message.
343
    :param source: Diagnostic source identifier (e.g. image-helper).
344
    :param level: Diagnostic level (`DEBUG`, `INFO`, `WARNING`, `ERROR`).
345
    :param etime: The time the message occured (if available).
346
    :param details: Additional details or debug information.
347
    """
348
    VirtualMachineDiagnostic.objects.create_for_vm(vm, level, source=source,
349
            source_date=etime, message=message, details=details)
350

    
351

    
352
def create_instance(vm, public_nic, flavor, image, password=None):
353
    """`image` is a dictionary which should contain the keys:
354
            'backend_id', 'format' and 'metadata'
355

356
        metadata value should be a dictionary.
357
    """
358

    
359
    # Handle arguments to CreateInstance() as a dictionary,
360
    # initialize it based on a deployment-specific value.
361
    # This enables the administrator to override deployment-specific
362
    # arguments, such as the disk template to use, name of os provider
363
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
364
    #
365
    kw = settings.GANETI_CREATEINSTANCE_KWARGS
366
    kw['mode'] = 'create'
367
    kw['name'] = vm.backend_vm_id
368
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
369

    
370
    # Identify if provider parameter should be set in disk options.
371
    # Current implementation support providers only fo ext template.
372
    # To select specific provider for an ext template, template name
373
    # should be formated as `ext_<provider_name>`.
374
    provider = None
375
    disk_template = flavor.disk_template
376
    if flavor.disk_template.startswith("ext"):
377
        disk_template, provider = flavor.disk_template.split("_", 1)
378

    
379
    kw['disk_template'] = disk_template
380
    kw['disks'] = [{"size": flavor.disk * 1024}]
381
    if provider:
382
        kw['disks'][0]['provider'] = provider
383

    
384
        if provider == 'vlmc':
385
            kw['disks'][0]['origin'] = image['backend_id']
386

    
387
    kw['nics'] = [public_nic]
388
    if settings.GANETI_USE_HOTPLUG:
389
        kw['hotplug'] = True
390
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
391
    # kw['os'] = settings.GANETI_OS_PROVIDER
392
    kw['ip_check'] = False
393
    kw['name_check'] = False
394

    
395
    # Do not specific a node explicitly, have
396
    # Ganeti use an iallocator instead
397
    #kw['pnode'] = rapi.GetNodes()[0]
398

    
399
    kw['dry_run'] = settings.TEST
400

    
401
    kw['beparams'] = {
402
       'auto_balance': True,
403
       'vcpus': flavor.cpu,
404
       'memory': flavor.ram}
405

    
406
    if provider == 'vlmc':
407
        image_id = 'null'
408
    else:
409
        image_id = image['backend_id']
410

    
411
    kw['osparams'] = {
412
        'config_url': vm.config_url,
413
        # Store image id and format to Ganeti
414
        'img_id': image_id,
415
        'img_format': image['format']}
416

    
417
    if password:
418
        # Only for admin created VMs !!
419
        kw['osparams']['img_passwd'] = password
420

    
421
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
422
    # kw['hvparams'] = dict(serial_console=False)
423

    
424
    log.debug("Creating instance %s", utils.hide_pass(kw))
425
    with pooled_rapi_client(vm) as client:
426
        return client.CreateInstance(**kw)
427

    
428

    
429
def delete_instance(vm):
430
    with pooled_rapi_client(vm) as client:
431
        return client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
432

    
433

    
434
def reboot_instance(vm, reboot_type):
435
    assert reboot_type in ('soft', 'hard')
436
    with pooled_rapi_client(vm) as client:
437
        return client.RebootInstance(vm.backend_vm_id, reboot_type,
438
                                     dry_run=settings.TEST)
439

    
440

    
441
def startup_instance(vm):
442
    with pooled_rapi_client(vm) as client:
443
        return client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
444

    
445

    
446
def shutdown_instance(vm):
447
    with pooled_rapi_client(vm) as client:
448
        return client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
449

    
450

    
451
def get_instance_console(vm):
452
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
453
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
454
    # useless (see #783).
455
    #
456
    # Until this is fixed on the Ganeti side, construct a console info reply
457
    # directly.
458
    #
459
    # WARNING: This assumes that VNC runs on port network_port on
460
    #          the instance's primary node, and is probably
461
    #          hypervisor-specific.
462
    #
463
    log.debug("Getting console for vm %s", vm)
464

    
465
    console = {}
466
    console['kind'] = 'vnc'
467

    
468
    with pooled_rapi_client(vm) as client:
469
        i = client.GetInstance(vm.backend_vm_id)
470

    
471
    if i['hvparams']['serial_console']:
472
        raise Exception("hv parameter serial_console cannot be true")
473
    console['host'] = i['pnode']
474
    console['port'] = i['network_port']
475

    
476
    return console
477

    
478

    
479
def get_instance_info(vm):
480
    with pooled_rapi_client(vm) as client:
481
        return client.GetInstanceInfo(vm.backend_vm_id)
482

    
483

    
484
def create_network(network, backends=None, connect=True):
485
    """Create and connect a network."""
486
    if not backends:
487
        backends = Backend.objects.exclude(offline=True)
488

    
489
    log.debug("Creating network %s in backends %s", network, backends)
490

    
491
    for backend in backends:
492
        create_jobID = _create_network(network, backend)
493
        if connect:
494
            connect_network(network, backend, create_jobID)
495

    
496

    
497
def _create_network(network, backend):
498
    """Create a network."""
499

    
500
    network_type = network.public and 'public' or 'private'
501

    
502
    tags = network.backend_tag
503
    if network.dhcp:
504
        tags.append('nfdhcpd')
505

    
506
    if network.public:
507
        conflicts_check = True
508
    else:
509
        conflicts_check = False
510

    
511
    try:
512
        bn = BackendNetwork.objects.get(network=network, backend=backend)
513
        mac_prefix = bn.mac_prefix
514
    except BackendNetwork.DoesNotExist:
515
        raise Exception("BackendNetwork for network '%s' in backend '%s'"\
516
                        " does not exist" % (network.id, backend.id))
517

    
518
    with pooled_rapi_client(backend) as client:
519
        return client.CreateNetwork(network_name=network.backend_id,
520
                                    network=network.subnet,
521
                                    network6=network.subnet6,
522
                                    gateway=network.gateway,
523
                                    gateway6=network.gateway6,
524
                                    network_type=network_type,
525
                                    mac_prefix=mac_prefix,
526
                                    conflicts_check=conflicts_check,
527
                                    tags=tags)
528

    
529

    
530
def connect_network(network, backend, depend_job=None, group=None):
531
    """Connect a network to nodegroups."""
532
    log.debug("Connecting network %s to backend %s", network, backend)
533

    
534
    mode = "routed" if "ROUTED" in network.type else "bridged"
535

    
536
    if network.public:
537
        conflicts_check = True
538
    else:
539
        conflicts_check = False
540

    
541
    depend_jobs = [depend_job] if depend_job else []
542
    with pooled_rapi_client(backend) as client:
543
        if group:
544
            client.ConnectNetwork(network.backend_id, group, mode,
545
                                  network.link, conflicts_check, depend_jobs)
546
        else:
547
            for group in client.GetGroups():
548
                client.ConnectNetwork(network.backend_id, group, mode,
549
                                      network.link, conflicts_check,
550
                                      depend_jobs)
551

    
552

    
553
def delete_network(network, backends=None, disconnect=True):
554
    if not backends:
555
        backends = Backend.objects.exclude(offline=True)
556

    
557
    log.debug("Deleting network %s from backends %s", network, backends)
558

    
559
    for backend in backends:
560
        disconnect_jobIDs = []
561
        if disconnect:
562
            disconnect_jobIDs = disconnect_network(network, backend)
563
        _delete_network(network, backend, disconnect_jobIDs)
564

    
565

    
566
def _delete_network(network, backend, depend_jobs=[]):
567
    with pooled_rapi_client(backend) as client:
568
        return client.DeleteNetwork(network.backend_id, depend_jobs)
569

    
570

    
571
def disconnect_network(network, backend, group=None):
572
    log.debug("Disconnecting network %s to backend %s", network, backend)
573

    
574
    with pooled_rapi_client(backend) as client:
575
        if group:
576
            return [client.DisconnectNetwork(network.backend_id, group)]
577
        else:
578
            jobs = []
579
            for group in client.GetGroups():
580
                job = client.DisconnectNetwork(network.backend_id, group)
581
                jobs.append(job)
582
            return jobs
583

    
584

    
585
def connect_to_network(vm, network, address=None):
586
    nic = {'ip': address, 'network': network.backend_id}
587

    
588
    log.debug("Connecting vm %s to network %s(%s)", vm, network, address)
589

    
590
    with pooled_rapi_client(vm) as client:
591
        return client.ModifyInstance(vm.backend_vm_id, nics=[('add',  nic)],
592
                                     hotplug=settings.GANETI_USE_HOTPLUG,
593
                                     dry_run=settings.TEST)
594

    
595

    
596
def disconnect_from_network(vm, nic):
597
    op = [('remove', nic.index, {})]
598

    
599
    log.debug("Removing nic of VM %s, with index %s", vm, str(nic.index))
600

    
601
    with pooled_rapi_client(vm) as client:
602
        return client.ModifyInstance(vm.backend_vm_id, nics=op,
603
                                     hotplug=settings.GANETI_USE_HOTPLUG,
604
                                     dry_run=settings.TEST)
605

    
606

    
607
def set_firewall_profile(vm, profile):
608
    try:
609
        tag = _firewall_tags[profile]
610
    except KeyError:
611
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
612

    
613
    log.debug("Setting tag of VM %s to %s", vm, profile)
614

    
615
    with pooled_rapi_client(vm) as client:
616
        # Delete all firewall tags
617
        for t in _firewall_tags.values():
618
            client.DeleteInstanceTags(vm.backend_vm_id, [t],
619
                                      dry_run=settings.TEST)
620

    
621
        client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
622

    
623
        # XXX NOP ModifyInstance call to force process_net_status to run
624
        # on the dispatcher
625
        client.ModifyInstance(vm.backend_vm_id,
626
                         os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
627

    
628

    
629
def get_ganeti_instances(backend=None, bulk=False):
630
    instances = []
631
    for backend in get_backends(backend):
632
        with pooled_rapi_client(backend) as client:
633
            instances.append(client.GetInstances(bulk=bulk))
634

    
635
    return reduce(list.__add__, instances, [])
636

    
637

    
638
def get_ganeti_nodes(backend=None, bulk=False):
639
    nodes = []
640
    for backend in get_backends(backend):
641
        with pooled_rapi_client(backend) as client:
642
            nodes.append(client.GetNodes(bulk=bulk))
643

    
644
    return reduce(list.__add__, nodes, [])
645

    
646

    
647
def get_ganeti_jobs(backend=None, bulk=False):
648
    jobs = []
649
    for backend in get_backends(backend):
650
        with pooled_rapi_client(backend) as client:
651
            jobs.append(client.GetJobs(bulk=bulk))
652
    return reduce(list.__add__, jobs, [])
653

    
654
##
655
##
656
##
657

    
658

    
659
def get_backends(backend=None):
660
    if backend:
661
        return [backend]
662
    return Backend.objects.filter(offline=False)
663

    
664

    
665
def get_physical_resources(backend):
666
    """ Get the physical resources of a backend.
667

668
    Get the resources of a backend as reported by the backend (not the db).
669

670
    """
671
    nodes = get_ganeti_nodes(backend, bulk=True)
672
    attr = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal']
673
    res = {}
674
    for a in attr:
675
        res[a] = 0
676
    for n in nodes:
677
        # Filter out drained, offline and not vm_capable nodes since they will
678
        # not take part in the vm allocation process
679
        if n['vm_capable'] and not n['drained'] and not n['offline']\
680
           and n['cnodes']:
681
            for a in attr:
682
                res[a] += int(n[a])
683
    return res
684

    
685

    
686
def update_resources(backend, resources=None):
687
    """ Update the state of the backend resources in db.
688

689
    """
690

    
691
    if not resources:
692
        resources = get_physical_resources(backend)
693

    
694
    backend.mfree = resources['mfree']
695
    backend.mtotal = resources['mtotal']
696
    backend.dfree = resources['dfree']
697
    backend.dtotal = resources['dtotal']
698
    backend.pinst_cnt = resources['pinst_cnt']
699
    backend.ctotal = resources['ctotal']
700
    backend.updated = datetime.now()
701
    backend.save()
702

    
703

    
704
def get_memory_from_instances(backend):
705
    """ Get the memory that is used from instances.
706

707
    Get the used memory of a backend. Note: This is different for
708
    the real memory used, due to kvm's memory de-duplication.
709

710
    """
711
    with pooled_rapi_client(backend) as client:
712
        instances = client.GetInstances(bulk=True)
713
    mem = 0
714
    for i in instances:
715
        mem += i['oper_ram']
716
    return mem
717

    
718
##
719
## Synchronized operations for reconciliation
720
##
721

    
722

    
723
def create_network_synced(network, backend):
724
    result = _create_network_synced(network, backend)
725
    if result[0] != 'success':
726
        return result
727
    result = connect_network_synced(network, backend)
728
    return result
729

    
730

    
731
def _create_network_synced(network, backend):
732
    with pooled_rapi_client(backend) as client:
733
        job = _create_network(network, backend)
734
        result = wait_for_job(client, job)
735
    return result
736

    
737

    
738
def connect_network_synced(network, backend):
739
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
740
        mode = 'routed'
741
    else:
742
        mode = 'bridged'
743
    with pooled_rapi_client(backend) as client:
744
        for group in client.GetGroups():
745
            job = client.ConnectNetwork(network.backend_id, group, mode,
746
                                        network.link)
747
            result = wait_for_job(client, job)
748
            if result[0] != 'success':
749
                return result
750

    
751
    return result
752

    
753

    
754
def wait_for_job(client, jobid):
755
    result = client.WaitForJobChange(jobid, ['status', 'opresult'], None, None)
756
    status = result['job_info'][0]
757
    while status not in ['success', 'error', 'cancel']:
758
        result = client.WaitForJobChange(jobid, ['status', 'opresult'],
759
                                        [result], None)
760
        status = result['job_info'][0]
761

    
762
    if status == 'success':
763
        return (status, None)
764
    else:
765
        error = result['job_info'][1]
766
        return (status, error)