Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (22.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 logging import getLogger
37
from django.conf import settings
38
from django.db import transaction
39
from datetime import datetime
40

    
41
from synnefo.db.models import (Backend, VirtualMachine, Network,
42
                               BackendNetwork, BACKEND_STATUSES)
43
from synnefo.logic import utils
44
from synnefo.db.pools import EmptyPool
45
from synnefo.api.faults import OverLimit
46
from synnefo.api.util import backend_public_networks, get_network_free_address
47
from synnefo.util.rapi import GanetiRapiClient
48

    
49
log = getLogger('synnefo.logic')
50

    
51

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

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

    
59

    
60
def create_client(hostname, port=5080, username=None, password=None):
61
    return GanetiRapiClient(hostname, port, username, password)
62

    
63

    
64
@transaction.commit_on_success
65
def process_op_status(vm, etime, jobid, opcode, status, logmsg):
66
    """Process a job progress notification from the backend
67

68
    Process an incoming message from the backend (currently Ganeti).
69
    Job notifications with a terminating status (sucess, error, or canceled),
70
    also update the operating state of the VM.
71

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

    
78
    vm.backendjobid = jobid
79
    vm.backendjobstatus = status
80
    vm.backendopcode = opcode
81
    vm.backendlogmsg = logmsg
82

    
83
    # Notifications of success change the operating state
84
    state_for_success = VirtualMachine.OPER_STATE_FROM_OPCODE.get(opcode, None)
85
    if status == 'success' and state_for_success is not None:
86
        utils.update_state(vm, state_for_success)
87
        # Set the deleted flag explicitly, cater for admin-initiated removals
88
        if opcode == 'OP_INSTANCE_REMOVE':
89
            release_instance_nics(vm)
90
            vm.deleted = True
91
            vm.nics.all().delete()
92

    
93
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
94
    if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
95
        utils.update_state(vm, 'ERROR')
96

    
97
    # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR,
98
    # when no instance exists at the Ganeti backend.
99
    # See ticket #799 for all the details.
100
    #
101
    if (status == 'error' and opcode == 'OP_INSTANCE_REMOVE'):
102
        vm.deleted = True
103
        vm.nics.all().delete()
104

    
105
    vm.backendtime = etime
106
    # Any other notification of failure leaves the operating state unchanged
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

    
153
    vm.backendtime = etime
154
    vm.save()
155

    
156

    
157
def release_instance_nics(vm):
158
    for nic in vm.nics.all():
159
        nic.network.release_address(nic.ipv4)
160
        nic.delete()
161

    
162

    
163
@transaction.commit_on_success
164
def process_network_status(back_network, etime, jobid, opcode, status, logmsg):
165
    if status not in [x[0] for x in BACKEND_STATUSES]:
166
        return
167
        #raise Network.InvalidBackendMsgError(opcode, status)
168

    
169
    back_network.backendjobid = jobid
170
    back_network.backendjobstatus = status
171
    back_network.backendopcode = opcode
172
    back_network.backendlogmsg = logmsg
173

    
174
    # Notifications of success change the operating state
175
    state_for_success = BackendNetwork.OPER_STATE_FROM_OPCODE.get(opcode, None)
176
    if status == 'success' and state_for_success is not None:
177
        back_network.operstate = state_for_success
178
        if opcode == 'OP_NETWORK_REMOVE':
179
            back_network.deleted = True
180

    
181
    if status in ('canceled', 'error') and opcode == 'OP_NETWORK_CREATE':
182
        utils.update_state(back_network, 'ERROR')
183

    
184
    if (status == 'error' and opcode == 'OP_NETWORK_REMOVE'):
185
        back_network.deleted = True
186
        back_network.operstate = 'DELETED'
187

    
188
    back_network.save()
189

    
190

    
191
@transaction.commit_on_success
192
def process_create_progress(vm, etime, rprogress, wprogress):
193

    
194
    # XXX: This only uses the read progress for now.
195
    #      Explore whether it would make sense to use the value of wprogress
196
    #      somewhere.
197
    percentage = int(rprogress)
198

    
199
    # The percentage may exceed 100%, due to the way
200
    # snf-progress-monitor tracks bytes read by image handling processes
201
    percentage = 100 if percentage > 100 else percentage
202
    if percentage < 0:
203
        raise ValueError("Percentage cannot be negative")
204

    
205
    # FIXME: log a warning here, see #1033
206
#   if last_update > percentage:
207
#       raise ValueError("Build percentage should increase monotonically " \
208
#                        "(old = %d, new = %d)" % (last_update, percentage))
209

    
210
    # This assumes that no message of type 'ganeti-create-progress' is going to
211
    # arrive once OP_INSTANCE_CREATE has succeeded for a Ganeti instance and
212
    # the instance is STARTED.  What if the two messages are processed by two
213
    # separate dispatcher threads, and the 'ganeti-op-status' message for
214
    # successful creation gets processed before the 'ganeti-create-progress'
215
    # message? [vkoukis]
216
    #
217
    #if not vm.operstate == 'BUILD':
218
    #    raise VirtualMachine.IllegalState("VM is not in building state")
219

    
220
    vm.buildpercentage = percentage
221
    vm.backendtime = etime
222
    vm.save()
223

    
224

    
225
def start_action(vm, action):
226
    """Update the state of a VM when a new action is initiated."""
227
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
228
        raise VirtualMachine.InvalidActionError(action)
229

    
230
    # No actions to deleted and no actions beside destroy to suspended VMs
231
    if vm.deleted:
232
        raise VirtualMachine.DeletedError
233

    
234
    # No actions to machines being built. They may be destroyed, however.
235
    if vm.operstate == 'BUILD' and action != 'DESTROY':
236
        raise VirtualMachine.BuildingError
237

    
238
    vm.action = action
239
    vm.backendjobid = None
240
    vm.backendopcode = None
241
    vm.backendjobstatus = None
242
    vm.backendlogmsg = None
243

    
244
    # Update the relevant flags if the VM is being suspended or destroyed.
245
    # Do not set the deleted flag here, see ticket #721.
246
    #
247
    # The deleted flag is set asynchronously, when an OP_INSTANCE_REMOVE
248
    # completes successfully. Hence, a server may be visible for some time
249
    # after a DELETE /servers/id returns HTTP 204.
250
    #
251
    if action == "DESTROY":
252
        # vm.deleted = True
253
        pass
254
    elif action == "SUSPEND":
255
        vm.suspended = True
256
    elif action == "START":
257
        vm.suspended = False
258
    vm.save()
259

    
260

    
261
@transaction.commit_on_success
262
def create_instance(vm, flavor, image, password, personality):
263
    """`image` is a dictionary which should contain the keys:
264
            'backend_id', 'format' and 'metadata'
265

266
        metadata value should be a dictionary.
267
    """
268

    
269
    if settings.PUBLIC_ROUTED_USE_POOL:
270
        (network, address) = allocate_public_address(vm)
271
        if address is None:
272
            raise OverLimit("Can not allocate IP for new machine."
273
                            " Public networks are full.")
274
        nic = {'ip': address, 'network': network.backend_id}
275
    else:
276
        nic = {'ip': 'pool', 'network': network.backend_id}
277

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

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

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

    
306
    kw['disk_template'] = disk_template
307
    kw['disks'] = [{"size": sz}]
308
    if provider:
309
        kw['disks'][0]['provider'] = provider
310

    
311
    kw['nics'] = [nic]
312
    if settings.GANETI_USE_HOTPLUG:
313
        kw['hotplug'] = True
314
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
315
    # kw['os'] = settings.GANETI_OS_PROVIDER
316
    kw['ip_check'] = False
317
    kw['name_check'] = False
318
    # Do not specific a node explicitly, have
319
    # Ganeti use an iallocator instead
320
    #
321
    # kw['pnode']=rapi.GetNodes()[0]
322
    kw['dry_run'] = settings.TEST
323

    
324
    kw['beparams'] = {
325
        'auto_balance': True,
326
        'vcpus': flavor.cpu,
327
        'memory': flavor.ram}
328

    
329
    kw['osparams'] = {
330
        'img_id': image['backend_id'],
331
        'img_passwd': password,
332
        'img_format': image['format']}
333
    if personality:
334
        kw['osparams']['img_personality'] = json.dumps(personality)
335

    
336
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
337

    
338
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
339
    # kw['hvparams'] = dict(serial_console=False)
340

    
341
    return vm.client.CreateInstance(**kw)
342

    
343

    
344
def allocate_public_address(vm):
345
    """Allocate a public IP for a vm."""
346
    for network in backend_public_networks(vm.backend):
347
        try:
348
            address = get_network_free_address(network)
349
            return (network, address)
350
        except EmptyPool:
351
            pass
352
    return (None, None)
353

    
354

    
355
def delete_instance(vm):
356
    start_action(vm, 'DESTROY')
357
    vm.client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
358

    
359

    
360
def reboot_instance(vm, reboot_type):
361
    assert reboot_type in ('soft', 'hard')
362
    vm.client.RebootInstance(vm.backend_vm_id, reboot_type, dry_run=settings.TEST)
363
    log.info('Rebooting instance %s', vm.backend_vm_id)
364

    
365

    
366
def startup_instance(vm):
367
    start_action(vm, 'START')
368
    vm.client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
369

    
370

    
371
def shutdown_instance(vm):
372
    start_action(vm, 'STOP')
373
    vm.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
    console = {}
389
    console['kind'] = 'vnc'
390
    i = vm.client.GetInstance(vm.backend_vm_id)
391
    if i['hvparams']['serial_console']:
392
        raise Exception("hv parameter serial_console cannot be true")
393
    console['host'] = i['pnode']
394
    console['port'] = i['network_port']
395

    
396
    return console
397
    # return rapi.GetInstanceConsole(vm.backend_vm_id)
398

    
399

    
400
def request_status_update(vm):
401
    return vm.client.GetInstanceInfo(vm.backend_vm_id)
402

    
403

    
404
def update_status(vm, status):
405
    utils.update_state(vm, status)
406

    
407

    
408
def create_network(network, backends=None):
409
    """ Add and connect a network to backends.
410

411
    @param network: Network object
412
    @param backends: List of Backend objects. None defaults to all.
413

414
    """
415
    backend_jobs = _create_network(network, backends)
416
    connect_network(network, backend_jobs)
417
    return network
418

    
419

    
420
def _create_network(network, backends=None):
421
    """Add a network to backends.
422
    @param network: Network object
423
    @param backends: List of Backend objects. None defaults to all.
424

425
    """
426

    
427
    network_type = network.public and 'public' or 'private'
428
    if not backends:
429
        backends = Backend.objects.exclude(offline=True)
430

    
431
    tags = network.backend_tag
432
    if network.dhcp:
433
        tags.append('nfdhcpd')
434
    tags = ','.join(tags)
435

    
436
    backend_jobs = []
437
    for backend in backends:
438
        try:
439
            backend_network = BackendNetwork.objects.get(network=network,
440
                                                         backend=backend)
441
        except BackendNetwork.DoesNotExist:
442
            raise Exception("BackendNetwork for network '%s' in backend '%s'"\
443
                            " does not exist" % (network.id, backend.id))
444
        job = backend.client.CreateNetwork(
445
                network_name=network.backend_id,
446
                network=network.subnet,
447
                gateway=network.gateway,
448
                network_type=network_type,
449
                mac_prefix=backend_network.mac_prefix,
450
                tags=tags)
451
        backend_jobs.append((backend, job))
452

    
453
    return backend_jobs
454

    
455

    
456
def connect_network(network, backend_jobs=None):
457
    """Connect a network to all nodegroups.
458

459
    @param network: Network object
460
    @param backend_jobs: List of tuples of the form (Backend, jobs) which are
461
                         the backends to connect the network and the jobs on
462
                         which the connect job depends.
463

464
    """
465

    
466
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
467
        mode = 'routed'
468
    else:
469
        mode = 'bridged'
470

    
471
    if not backend_jobs:
472
        backend_jobs = [(backend, []) for backend in
473
                        Backend.objects.exclude(offline=True)]
474

    
475
    for backend, job in backend_jobs:
476
        client = backend.client
477
        for group in client.GetGroups():
478
            client.ConnectNetwork(network.backend_id, group, mode,
479
                                  network.link, [job])
480

    
481

    
482
def connect_network_group(backend, network, group):
483
    """Connect a network to a specific nodegroup of a backend.
484

485
    """
486
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
487
        mode = 'routed'
488
    else:
489
        mode = 'bridged'
490

    
491
    return backend.client.ConnectNetwork(network.backend_id, group, mode,
492
                                         network.link)
493

    
494

    
495
def delete_network(network, backends=None):
496
    """ Disconnect and a remove a network from backends.
497

498
    @param network: Network object
499
    @param backends: List of Backend objects. None defaults to all.
500

501
    """
502
    backend_jobs = disconnect_network(network, backends)
503
    _delete_network(network, backend_jobs)
504

    
505

    
506
def disconnect_network(network, backends=None):
507
    """Disconnect a network from all nodegroups.
508

509
    @param network: Network object
510
    @param backends: List of Backend objects. None defaults to all.
511

512
    """
513

    
514
    if not backends:
515
        backends = Backend.objects.exclude(offline=True)
516

    
517
    backend_jobs = []
518
    for backend in backends:
519
        client = backend.client
520
        jobs = []
521
        for group in client.GetGroups():
522
            job = client.DisconnectNetwork(network.backend_id, group)
523
            jobs.append(job)
524
        backend_jobs.append((backend, jobs))
525

    
526
    return backend_jobs
527

    
528

    
529
def disconnect_from_network(vm, nic):
530
    """Disconnect a virtual machine from a network by removing it's nic.
531

532
    @param vm: VirtualMachine object
533
    @param network: Network object
534

535
    """
536

    
537
    op = [('remove', nic.index, {})]
538
    return vm.client.ModifyInstance(vm.backend_vm_id, nics=op,
539
                                    hotplug=settings.GANETI_USE_HOTPLUG,
540
                                    dry_run=settings.TEST)
541

    
542

    
543
def _delete_network(network, backend_jobs=None):
544
    if not backend_jobs:
545
        backend_jobs = [(backend, []) for backend in
546
                Backend.objects.exclude(offline=True)]
547
    for backend, jobs in backend_jobs:
548
        backend.client.DeleteNetwork(network.backend_id, jobs)
549

    
550

    
551
def connect_to_network(vm, network, address):
552
    """Connect a virtual machine to a network.
553

554
    @param vm: VirtualMachine object
555
    @param network: Network object
556

557
    """
558

    
559
    # ip = network.dhcp and 'pool' or None
560

    
561
    nic = {'ip': address, 'network': network.backend_id}
562
    vm.client.ModifyInstance(vm.backend_vm_id, nics=[('add',  nic)],
563
                             hotplug=settings.GANETI_USE_HOTPLUG,
564
                             dry_run=settings.TEST)
565

    
566

    
567
def set_firewall_profile(vm, profile):
568
    try:
569
        tag = _firewall_tags[profile]
570
    except KeyError:
571
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
572

    
573
    client = vm.client
574
    # Delete all firewall tags
575
    for t in _firewall_tags.values():
576
        client.DeleteInstanceTags(vm.backend_vm_id, [t], dry_run=settings.TEST)
577

    
578
    client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
579

    
580
    # XXX NOP ModifyInstance call to force process_net_status to run
581
    # on the dispatcher
582
    vm.client.ModifyInstance(vm.backend_vm_id,
583
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
584

    
585

    
586
def get_ganeti_instances(backend=None, bulk=False):
587
    Instances = [c.client.GetInstances(bulk=bulk)\
588
                 for c in get_backends(backend)]
589
    return reduce(list.__add__, Instances, [])
590

    
591

    
592
def get_ganeti_nodes(backend=None, bulk=False):
593
    Nodes = [c.client.GetNodes(bulk=bulk) for c in get_backends(backend)]
594
    return reduce(list.__add__, Nodes, [])
595

    
596

    
597
def get_ganeti_jobs(backend=None, bulk=False):
598
    Jobs = [c.client.GetJobs(bulk=bulk) for c in get_backends(backend)]
599
    return reduce(list.__add__, Jobs, [])
600

    
601
##
602
##
603
##
604

    
605

    
606
def get_backends(backend=None):
607
    if backend:
608
        return [backend]
609
    return Backend.objects.filter(offline=False)
610

    
611

    
612
def get_physical_resources(backend):
613
    """ Get the physical resources of a backend.
614

615
    Get the resources of a backend as reported by the backend (not the db).
616

617
    """
618
    nodes = get_ganeti_nodes(backend, bulk=True)
619
    attr = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal']
620
    res = {}
621
    for a in attr:
622
        res[a] = 0
623
    for n in nodes:
624
        # Filter out drained, offline and not vm_capable nodes since they will
625
        # not take part in the vm allocation process
626
        if n['vm_capable'] and not n['drained'] and not n['offline']\
627
           and n['cnodes']:
628
            for a in attr:
629
                res[a] += int(n[a])
630
    return res
631

    
632

    
633
def update_resources(backend, resources=None):
634
    """ Update the state of the backend resources in db.
635

636
    """
637

    
638
    if not resources:
639
        resources = get_physical_resources(backend)
640

    
641
    backend.mfree = resources['mfree']
642
    backend.mtotal = resources['mtotal']
643
    backend.dfree = resources['dfree']
644
    backend.dtotal = resources['dtotal']
645
    backend.pinst_cnt = resources['pinst_cnt']
646
    backend.ctotal = resources['ctotal']
647
    backend.updated = datetime.now()
648
    backend.save()
649

    
650

    
651
def get_memory_from_instances(backend):
652
    """ Get the memory that is used from instances.
653

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

657
    """
658
    instances = backend.client.GetInstances(bulk=True)
659
    mem = 0
660
    for i in instances:
661
        mem += i['oper_ram']
662
    return mem
663

    
664
##
665
## Synchronized operations for reconciliation
666
##
667

    
668

    
669
def create_network_synced(network, backend):
670
    result = _create_network_synced(network, backend)
671
    if result[0] != 'success':
672
        return result
673
    result = connect_network_synced(network, backend)
674
    return result
675

    
676

    
677
def _create_network_synced(network, backend):
678
    client = backend.client
679

    
680
    backend_jobs = _create_network(network, [backend])
681
    (_, job) = backend_jobs[0]
682
    return wait_for_job(client, job)
683

    
684

    
685
def connect_network_synced(network, backend):
686
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
687
        mode = 'routed'
688
    else:
689
        mode = 'bridged'
690
    client = backend.client
691

    
692
    for group in client.GetGroups():
693
        job = client.ConnectNetwork(network.backend_id, group, mode,
694
                                    network.link)
695
        result = wait_for_job(client, job)
696
        if result[0] != 'success':
697
            return result
698

    
699
    return result
700

    
701

    
702
def wait_for_job(client, jobid):
703
    result = client.WaitForJobChange(jobid, ['status', 'opresult'], None, None)
704
    status = result['job_info'][0]
705
    while status not in ['success', 'error', 'cancel']:
706
        result = client.WaitForJobChange(jobid, ['status', 'opresult'],
707
                                        [result], None)
708
        status = result['job_info'][0]
709

    
710
    if status == 'success':
711
        return (status, None)
712
    else:
713
        error = result['job_info'][1]
714
        return (status, error)