Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / backend.py @ 7f2dbcad

History | View | Annotate | Download (23.1 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, VirtualMachineDiagnostic)
43
from synnefo.logic import utils
44

    
45
from logging import getLogger
46
log = getLogger(__name__)
47

    
48

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

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

    
56

    
57
@transaction.commit_on_success
58
def process_op_status(vm, etime, jobid, opcode, status, logmsg):
59
    """Process a job progress notification from the backend
60

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

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

    
71
    vm.backendjobid = jobid
72
    vm.backendjobstatus = status
73
    vm.backendopcode = opcode
74
    vm.backendlogmsg = logmsg
75

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

    
81

    
82
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
83
    if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
84
        vm.operstate = 'ERROR'
85
        vm.backendtime = etime
86

    
87
    if opcode == 'OP_INSTANCE_REMOVE':
88
        # Set the deleted flag explicitly, cater for admin-initiated removals
89
        # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR,
90
        # when no instance exists at the Ganeti backend.
91
        # See ticket #799 for all the details.
92
        #
93
        if status == 'success' or (status == 'error' and
94
                                   vm.operstate == 'ERROR'):
95
            release_instance_nics(vm)
96
            vm.nics.all().delete()
97
            vm.deleted = True
98
            vm.operstate = state_for_success
99
            vm.backendtime = etime
100

    
101
    # Update backendtime only for jobs that have been successfully completed,
102
    # since only these jobs update the state of the VM. Else a "race condition"
103
    # may occur when a successful job (e.g. OP_INSTANCE_REMOVE) completes
104
    # before an error job and messages arrive in reversed order.
105
    if status == 'success':
106
        vm.backendtime = etime
107

    
108
    vm.save()
109

    
110

    
111
@transaction.commit_on_success
112
def process_net_status(vm, etime, nics):
113
    """Process a net status notification from the backend
114

115
    Process an incoming message from the Ganeti backend,
116
    detailing the NIC configuration of a VM instance.
117

118
    Update the state of the VM in the DB accordingly.
119
    """
120

    
121
    release_instance_nics(vm)
122

    
123
    new_nics = enumerate(nics)
124
    for i, new_nic in new_nics:
125
        network = new_nic.get('network', '')
126
        n = str(network)
127
        pk = utils.id_from_network_name(n)
128

    
129
        net = Network.objects.get(pk=pk)
130

    
131
        # Get the new nic info
132
        mac = new_nic.get('mac', '')
133
        ipv4 = new_nic.get('ip', '')
134
        ipv6 = new_nic.get('ipv6', '')
135

    
136
        firewall = new_nic.get('firewall', '')
137
        firewall_profile = _reverse_tags.get(firewall, '')
138
        if not firewall_profile and net.public:
139
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
140

    
141
        if ipv4:
142
            net.reserve_address(ipv4)
143

    
144
        vm.nics.create(
145
            network=net,
146
            index=i,
147
            mac=mac,
148
            ipv4=ipv4,
149
            ipv6=ipv6,
150
            firewall_profile=firewall_profile,
151
            dirty=False)
152
        # Dummy save the network, because UI uses changed-since for VMs
153
        # and Networks in order to show the VM NICs
154
        net.save()
155

    
156
    vm.backendtime = etime
157
    vm.save()
158

    
159

    
160
def release_instance_nics(vm):
161
    for nic in vm.nics.all():
162
        net = nic.network
163
        if nic.ipv4:
164
            net.release_address(nic.ipv4)
165
        nic.delete()
166
        net.save()
167

    
168

    
169
@transaction.commit_on_success
170
def process_network_status(back_network, etime, jobid, opcode, status, logmsg):
171
    if status not in [x[0] for x in BACKEND_STATUSES]:
172
        raise Network.InvalidBackendMsgError(opcode, status)
173

    
174
    back_network.backendjobid = jobid
175
    back_network.backendjobstatus = status
176
    back_network.backendopcode = opcode
177
    back_network.backendlogmsg = logmsg
178

    
179
    # Notifications of success change the operating state
180
    state_for_success = BackendNetwork.OPER_STATE_FROM_OPCODE.get(opcode, None)
181
    if status == 'success' and state_for_success is not None:
182
        back_network.operstate = state_for_success
183

    
184
    if status in ('canceled', 'error') and opcode == 'OP_NETWORK_CREATE':
185
        utils.update_state(back_network, 'ERROR')
186
        back_network.backendtime = etime
187

    
188
    if opcode == 'OP_NETWORK_REMOVE':
189
        if status == 'success' or (status == 'error' and
190
                                   back_network.operstate == 'ERROR'):
191
            back_network.operstate = state_for_success
192
            back_network.deleted = True
193
            back_network.backendtime = etime
194

    
195
    if status == 'success':
196
        back_network.backendtime = etime
197
    back_network.save()
198

    
199

    
200
@transaction.commit_on_success
201
def process_network_modify(back_network, etime, jobid, opcode, status,
202
                           add_reserved_ips, remove_reserved_ips):
203
    assert (opcode == "OP_NETWORK_SET_PARAMS")
204
    if status not in [x[0] for x in BACKEND_STATUSES]:
205
        raise Network.InvalidBackendMsgError(opcode, status)
206

    
207
    back_network.backendjobid = jobid
208
    back_network.backendjobstatus = status
209
    back_network.opcode = opcode
210

    
211
    if add_reserved_ips or remove_reserved_ips:
212
        net = back_network.network
213
        pool = net.get_pool()
214
        if add_reserved_ips:
215
            for ip in add_reserved_ips:
216
                pool.reserve(ip, external=True)
217
        if remove_reserved_ips:
218
            for ip in remove_reserved_ips:
219
                pool.put(ip, external=True)
220
        pool.save()
221

    
222
    if status == 'success':
223
        back_network.backendtime = etime
224
    back_network.save()
225

    
226

    
227
@transaction.commit_on_success
228
def process_create_progress(vm, etime, progress):
229

    
230
    percentage = int(progress)
231

    
232
    # The percentage may exceed 100%, due to the way
233
    # snf-image:copy-progress tracks bytes read by image handling processes
234
    percentage = 100 if percentage > 100 else percentage
235
    if percentage < 0:
236
        raise ValueError("Percentage cannot be negative")
237

    
238
    # FIXME: log a warning here, see #1033
239
#   if last_update > percentage:
240
#       raise ValueError("Build percentage should increase monotonically " \
241
#                        "(old = %d, new = %d)" % (last_update, percentage))
242

    
243
    # This assumes that no message of type 'ganeti-create-progress' is going to
244
    # arrive once OP_INSTANCE_CREATE has succeeded for a Ganeti instance and
245
    # the instance is STARTED.  What if the two messages are processed by two
246
    # separate dispatcher threads, and the 'ganeti-op-status' message for
247
    # successful creation gets processed before the 'ganeti-create-progress'
248
    # message? [vkoukis]
249
    #
250
    #if not vm.operstate == 'BUILD':
251
    #    raise VirtualMachine.IllegalState("VM is not in building state")
252

    
253
    vm.buildpercentage = percentage
254
    vm.backendtime = etime
255
    vm.save()
256

    
257

    
258
def create_instance_diagnostic(vm, message, source, level="DEBUG", etime=None,
259
    details=None):
260
    """
261
    Create virtual machine instance diagnostic entry.
262

263
    :param vm: VirtualMachine instance to create diagnostic for.
264
    :param message: Diagnostic message.
265
    :param source: Diagnostic source identifier (e.g. image-helper).
266
    :param level: Diagnostic level (`DEBUG`, `INFO`, `WARNING`, `ERROR`).
267
    :param etime: The time the message occured (if available).
268
    :param details: Additional details or debug information.
269
    """
270
    VirtualMachineDiagnostic.objects.create_for_vm(vm, level, source=source,
271
            source_date=etime, message=message, details=details)
272

    
273

    
274
def create_instance(vm, public_nic, flavor, image, password, personality):
275
    """`image` is a dictionary which should contain the keys:
276
            'backend_id', 'format' and 'metadata'
277

278
        metadata value should be a dictionary.
279
    """
280

    
281
    if settings.IGNORE_FLAVOR_DISK_SIZES:
282
        if image['backend_id'].find("windows") >= 0:
283
            sz = 14000
284
        else:
285
            sz = 4000
286
    else:
287
        sz = flavor.disk * 1024
288

    
289
    # Handle arguments to CreateInstance() as a dictionary,
290
    # initialize it based on a deployment-specific value.
291
    # This enables the administrator to override deployment-specific
292
    # arguments, such as the disk template to use, name of os provider
293
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
294
    #
295
    kw = settings.GANETI_CREATEINSTANCE_KWARGS
296
    kw['mode'] = 'create'
297
    kw['name'] = vm.backend_vm_id
298
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
299

    
300
    # Identify if provider parameter should be set in disk options.
301
    # Current implementation support providers only fo ext template.
302
    # To select specific provider for an ext template, template name
303
    # should be formated as `ext_<provider_name>`.
304
    provider = None
305
    disk_template = flavor.disk_template
306
    if flavor.disk_template.startswith("ext"):
307
        disk_template, provider = flavor.disk_template.split("_", 1)
308

    
309
    kw['disk_template'] = disk_template
310
    kw['disks'] = [{"size": sz}]
311
    if provider:
312
        kw['disks'][0]['provider'] = provider
313

    
314
        if provider == 'vlmc':
315
            kw['disks'][0]['origin'] = image['backend_id']
316

    
317
    kw['nics'] = [public_nic]
318
    if settings.GANETI_USE_HOTPLUG:
319
        kw['hotplug'] = True
320
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
321
    # kw['os'] = settings.GANETI_OS_PROVIDER
322
    kw['ip_check'] = False
323
    kw['name_check'] = False
324
    # Do not specific a node explicitly, have
325
    # Ganeti use an iallocator instead
326
    #
327
    #kw['pnode'] = rapi.GetNodes()[0]
328
    kw['dry_run'] = settings.TEST
329

    
330
    kw['beparams'] = {
331
        'auto_balance': True,
332
        'vcpus': flavor.cpu,
333
        'memory': flavor.ram}
334

    
335
    kw['osparams'] = {
336
        'img_id': image['backend_id'],
337
        'img_passwd': password,
338
        'img_format': image['format']}
339
    if personality:
340
        kw['osparams']['img_personality'] = json.dumps(personality)
341

    
342
    if provider != None and provider == 'vlmc':
343
        kw['osparams']['img_id'] = 'null'
344

    
345
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
346

    
347
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
348
    # kw['hvparams'] = dict(serial_console=False)
349
    log.debug("Creating instance %s", utils.hide_pass(kw))
350
    with pooled_rapi_client(vm) as client:
351
        return client.CreateInstance(**kw)
352

    
353

    
354
def delete_instance(vm):
355
    with pooled_rapi_client(vm) as client:
356
        return client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
357

    
358

    
359
def reboot_instance(vm, reboot_type):
360
    assert reboot_type in ('soft', 'hard')
361
    with pooled_rapi_client(vm) as client:
362
        return client.RebootInstance(vm.backend_vm_id, reboot_type,
363
                                     dry_run=settings.TEST)
364

    
365

    
366
def startup_instance(vm):
367
    with pooled_rapi_client(vm) as client:
368
        return client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
369

    
370

    
371
def shutdown_instance(vm):
372
    with pooled_rapi_client(vm) as client:
373
        return client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
374

    
375

    
376
def get_instance_console(vm):
377
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
378
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
379
    # useless (see #783).
380
    #
381
    # Until this is fixed on the Ganeti side, construct a console info reply
382
    # directly.
383
    #
384
    # WARNING: This assumes that VNC runs on port network_port on
385
    #          the instance's primary node, and is probably
386
    #          hypervisor-specific.
387
    #
388
    log.debug("Getting console for vm %s", vm)
389

    
390
    console = {}
391
    console['kind'] = 'vnc'
392

    
393
    with pooled_rapi_client(vm) as client:
394
        i = client.GetInstance(vm.backend_vm_id)
395

    
396
    if i['hvparams']['serial_console']:
397
        raise Exception("hv parameter serial_console cannot be true")
398
    console['host'] = i['pnode']
399
    console['port'] = i['network_port']
400

    
401
    return console
402

    
403

    
404
def get_instance_info(vm):
405
    with pooled_rapi_client(vm) as client:
406
        return client.GetInstanceInfo(vm.backend_vm_id)
407

    
408

    
409
def create_network(network, backends=None, connect=True):
410
    """Create and connect a network."""
411
    if not backends:
412
        backends = Backend.objects.exclude(offline=True)
413

    
414
    log.debug("Creating network %s in backends %s", network, backends)
415

    
416
    for backend in backends:
417
        create_jobID = _create_network(network, backend)
418
        if connect:
419
            connect_network(network, backend, create_jobID)
420

    
421

    
422
def _create_network(network, backend):
423
    """Create a network."""
424

    
425
    network_type = network.public and 'public' or 'private'
426

    
427
    tags = network.backend_tag
428
    if network.dhcp:
429
        tags.append('nfdhcpd')
430

    
431
    if network.public:
432
        conflicts_check = True
433
    else:
434
        conflicts_check = False
435

    
436
    try:
437
        bn = BackendNetwork.objects.get(network=network, backend=backend)
438
        mac_prefix = bn.mac_prefix
439
    except BackendNetwork.DoesNotExist:
440
        raise Exception("BackendNetwork for network '%s' in backend '%s'"\
441
                        " does not exist" % (network.id, backend.id))
442

    
443
    with pooled_rapi_client(backend) as client:
444
        return client.CreateNetwork(network_name=network.backend_id,
445
                                    network=network.subnet,
446
                                    network6=network.subnet6,
447
                                    gateway=network.gateway,
448
                                    gateway6=network.gateway6,
449
                                    network_type=network_type,
450
                                    mac_prefix=mac_prefix,
451
                                    conflicts_check=conflicts_check,
452
                                    tags=tags)
453

    
454

    
455
def connect_network(network, backend, depend_job=None, group=None):
456
    """Connect a network to nodegroups."""
457
    log.debug("Connecting network %s to backend %s", network, backend)
458

    
459
    mode = "routed" if "ROUTED" in network.type else "bridged"
460

    
461
    if network.public:
462
        conflicts_check = True
463
    else:
464
        conflicts_check = False
465

    
466
    depend_jobs = [depend_job] if depend_job else []
467
    with pooled_rapi_client(backend) as client:
468
        if group:
469
            client.ConnectNetwork(network.backend_id, group, mode,
470
                                  network.link, conflicts_check, depend_jobs)
471
        else:
472
            for group in client.GetGroups():
473
                client.ConnectNetwork(network.backend_id, group, mode,
474
                                      network.link, conflicts_check,
475
                                      depend_jobs)
476

    
477

    
478
def delete_network(network, backends=None, disconnect=True):
479
    if not backends:
480
        backends = Backend.objects.exclude(offline=True)
481

    
482
    log.debug("Deleting network %s from backends %s", network, backends)
483

    
484
    for backend in backends:
485
        disconnect_jobIDs = []
486
        if disconnect:
487
            disconnect_jobIDs = disconnect_network(network, backend)
488
        _delete_network(network, backend, disconnect_jobIDs)
489

    
490

    
491
def _delete_network(network, backend, depend_jobs=[]):
492
    with pooled_rapi_client(backend) as client:
493
        return client.DeleteNetwork(network.backend_id, depend_jobs)
494

    
495

    
496
def disconnect_network(network, backend, group=None):
497
    log.debug("Disconnecting network %s to backend %s", network, backend)
498

    
499
    with pooled_rapi_client(backend) as client:
500
        if group:
501
            return [client.DisconnectNetwork(network.backend_id, group)]
502
        else:
503
            jobs = []
504
            for group in client.GetGroups():
505
                job = client.DisconnectNetwork(network.backend_id, group)
506
                jobs.append(job)
507
            return jobs
508

    
509

    
510
def connect_to_network(vm, network, address=None):
511
    nic = {'ip': address, 'network': network.backend_id}
512

    
513
    log.debug("Connecting vm %s to network %s(%s)", vm, network, address)
514

    
515
    with pooled_rapi_client(vm) as client:
516
        return client.ModifyInstance(vm.backend_vm_id, nics=[('add',  nic)],
517
                                     hotplug=settings.GANETI_USE_HOTPLUG,
518
                                     dry_run=settings.TEST)
519

    
520

    
521
def disconnect_from_network(vm, nic):
522
    op = [('remove', nic.index, {})]
523

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

    
526
    with pooled_rapi_client(vm) as client:
527
        return client.ModifyInstance(vm.backend_vm_id, nics=op,
528
                                     hotplug=settings.GANETI_USE_HOTPLUG,
529
                                     dry_run=settings.TEST)
530

    
531

    
532
def set_firewall_profile(vm, profile):
533
    try:
534
        tag = _firewall_tags[profile]
535
    except KeyError:
536
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
537

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

    
540
    with pooled_rapi_client(vm) as client:
541
        # Delete all firewall tags
542
        for t in _firewall_tags.values():
543
            client.DeleteInstanceTags(vm.backend_vm_id, [t],
544
                                      dry_run=settings.TEST)
545

    
546
        client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
547

    
548
        # XXX NOP ModifyInstance call to force process_net_status to run
549
        # on the dispatcher
550
        client.ModifyInstance(vm.backend_vm_id,
551
                         os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
552

    
553

    
554
def get_ganeti_instances(backend=None, bulk=False):
555
    instances = []
556
    for backend in get_backends(backend):
557
        with pooled_rapi_client(backend) as client:
558
            instances.append(client.GetInstances(bulk=bulk))
559

    
560
    return reduce(list.__add__, instances, [])
561

    
562

    
563
def get_ganeti_nodes(backend=None, bulk=False):
564
    nodes = []
565
    for backend in get_backends(backend):
566
        with pooled_rapi_client(backend) as client:
567
            nodes.append(client.GetNodes(bulk=bulk))
568

    
569
    return reduce(list.__add__, nodes, [])
570

    
571

    
572
def get_ganeti_jobs(backend=None, bulk=False):
573
    jobs = []
574
    for backend in get_backends(backend):
575
        with pooled_rapi_client(backend) as client:
576
            jobs.append(client.GetJobs(bulk=bulk))
577
    return reduce(list.__add__, jobs, [])
578

    
579
##
580
##
581
##
582

    
583

    
584
def get_backends(backend=None):
585
    if backend:
586
        return [backend]
587
    return Backend.objects.filter(offline=False)
588

    
589

    
590
def get_physical_resources(backend):
591
    """ Get the physical resources of a backend.
592

593
    Get the resources of a backend as reported by the backend (not the db).
594

595
    """
596
    nodes = get_ganeti_nodes(backend, bulk=True)
597
    attr = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal']
598
    res = {}
599
    for a in attr:
600
        res[a] = 0
601
    for n in nodes:
602
        # Filter out drained, offline and not vm_capable nodes since they will
603
        # not take part in the vm allocation process
604
        if n['vm_capable'] and not n['drained'] and not n['offline']\
605
           and n['cnodes']:
606
            for a in attr:
607
                res[a] += int(n[a])
608
    return res
609

    
610

    
611
def update_resources(backend, resources=None):
612
    """ Update the state of the backend resources in db.
613

614
    """
615

    
616
    if not resources:
617
        resources = get_physical_resources(backend)
618

    
619
    backend.mfree = resources['mfree']
620
    backend.mtotal = resources['mtotal']
621
    backend.dfree = resources['dfree']
622
    backend.dtotal = resources['dtotal']
623
    backend.pinst_cnt = resources['pinst_cnt']
624
    backend.ctotal = resources['ctotal']
625
    backend.updated = datetime.now()
626
    backend.save()
627

    
628

    
629
def get_memory_from_instances(backend):
630
    """ Get the memory that is used from instances.
631

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

635
    """
636
    with pooled_rapi_client(backend) as client:
637
        instances = client.GetInstances(bulk=True)
638
    mem = 0
639
    for i in instances:
640
        mem += i['oper_ram']
641
    return mem
642

    
643
##
644
## Synchronized operations for reconciliation
645
##
646

    
647

    
648
def create_network_synced(network, backend):
649
    result = _create_network_synced(network, backend)
650
    if result[0] != 'success':
651
        return result
652
    result = connect_network_synced(network, backend)
653
    return result
654

    
655

    
656
def _create_network_synced(network, backend):
657
    with pooled_rapi_client(backend) as client:
658
        job = _create_network(network, backend)
659
        result = wait_for_job(client, job)
660
    return result
661

    
662

    
663
def connect_network_synced(network, backend):
664
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
665
        mode = 'routed'
666
    else:
667
        mode = 'bridged'
668
    with pooled_rapi_client(backend) as client:
669
        for group in client.GetGroups():
670
            job = client.ConnectNetwork(network.backend_id, group, mode,
671
                                        network.link)
672
            result = wait_for_job(client, job)
673
            if result[0] != 'success':
674
                return result
675

    
676
    return result
677

    
678

    
679
def wait_for_job(client, jobid):
680
    result = client.WaitForJobChange(jobid, ['status', 'opresult'], None, None)
681
    status = result['job_info'][0]
682
    while status not in ['success', 'error', 'cancel']:
683
        result = client.WaitForJobChange(jobid, ['status', 'opresult'],
684
                                        [result], None)
685
        status = result['job_info'][0]
686

    
687
    if status == 'success':
688
        return (status, None)
689
    else:
690
        error = result['job_info'][1]
691
        return (status, error)