Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.8 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, ippool
44
from synnefo.api.faults import OverLimit
45
from synnefo.util.rapi import GanetiRapiClient
46

    
47
log = getLogger('synnefo.logic')
48

    
49

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

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

    
57

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

    
61

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

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

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

    
76
    vm.backendjobid = jobid
77
    vm.backendjobstatus = status
78
    vm.backendopcode = opcode
79
    vm.backendlogmsg = logmsg
80

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

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

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

    
102
    vm.backendtime = etime
103
    # Any other notification of failure leaves the operating state unchanged
104

    
105
    vm.save()
106

    
107

    
108
@transaction.commit_on_success
109
def process_net_status(vm, etime, nics):
110
    """Process a net status notification from the backend
111

112
    Process an incoming message from the Ganeti backend,
113
    detailing the NIC configuration of a VM instance.
114

115
    Update the state of the VM in the DB accordingly.
116
    """
117

    
118
    old_nics = vm.nics.order_by('index')
119
    new_nics = enumerate(nics)
120

    
121
    networks = {}
122

    
123
    for old_nic in old_nics:
124
        pk = old_nic.network.pk
125
        # Get the cached Network or get it from DB
126
        if pk in networks:
127
            net = networks[pk]
128
        else:
129
            # Get the network object in exclusive mode in order
130
            # to guarantee consistency of the address pool
131
            net = Network.objects.select_for_update().get(pk=pk)
132
        net.release_address(old_nic.ipv4)
133
        old_nic.delete()
134

    
135
    for i, new_nic in new_nics:
136
        network = new_nic.get('network', '')
137
        n = str(network)
138
        pk = utils.id_from_network_name(n)
139

    
140
        # Get the cached Network or get it from DB
141
        if pk in networks:
142
            net = networks[pk]
143
        else:
144
            net = Network.objects.select_for_update().get(pk=pk)
145

    
146
        # Get the new nic info
147
        mac = new_nic.get('mac', '')
148
        ipv4 = new_nic.get('ip', '')
149
        ipv6 = new_nic.get('ipv6', '')
150

    
151
        firewall = new_nic.get('firewall', '')
152
        firewall_profile = _reverse_tags.get(firewall, '')
153
        if not firewall_profile and net.public:
154
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
155

    
156
        if ipv4:
157
            net.reserve_address(ipv4)
158

    
159
        vm.nics.create(
160
            network=net,
161
            index=i,
162
            mac=mac,
163
            ipv4=ipv4,
164
            ipv6=ipv6,
165
            firewall_profile=firewall_profile,
166
            dirty=False)
167

    
168
    vm.backendtime = etime
169
    vm.save()
170

    
171

    
172
@transaction.commit_on_success
173
def process_network_status(back_network, etime, jobid, opcode, status, logmsg):
174
    if status not in [x[0] for x in BACKEND_STATUSES]:
175
        return
176
        #raise Network.InvalidBackendMsgError(opcode, status)
177

    
178
    back_network.backendjobid = jobid
179
    back_network.backendjobstatus = status
180
    back_network.backendopcode = opcode
181
    back_network.backendlogmsg = logmsg
182

    
183
    # Notifications of success change the operating state
184
    state_for_success = BackendNetwork.OPER_STATE_FROM_OPCODE.get(opcode, None)
185
    if status == 'success' and state_for_success is not None:
186
        back_network.operstate = state_for_success
187
        if opcode == 'OP_NETWORK_REMOVE':
188
            back_network.deleted = True
189

    
190
    if status in ('canceled', 'error') and opcode == 'OP_NETWORK_CREATE':
191
        utils.update_state(back_network, 'ERROR')
192

    
193
    if (status == 'error' and opcode == 'OP_NETWORK_REMOVE'):
194
        back_network.deleted = True
195
        back_network.operstate = 'DELETED'
196

    
197
    back_network.save()
198

    
199

    
200
@transaction.commit_on_success
201
def process_create_progress(vm, etime, rprogress, wprogress):
202

    
203
    # XXX: This only uses the read progress for now.
204
    #      Explore whether it would make sense to use the value of wprogress
205
    #      somewhere.
206
    percentage = int(rprogress)
207

    
208
    # The percentage may exceed 100%, due to the way
209
    # snf-progress-monitor tracks bytes read by image handling processes
210
    percentage = 100 if percentage > 100 else percentage
211
    if percentage < 0:
212
        raise ValueError("Percentage cannot be negative")
213

    
214
    # FIXME: log a warning here, see #1033
215
#   if last_update > percentage:
216
#       raise ValueError("Build percentage should increase monotonically " \
217
#                        "(old = %d, new = %d)" % (last_update, percentage))
218

    
219
    # This assumes that no message of type 'ganeti-create-progress' is going to
220
    # arrive once OP_INSTANCE_CREATE has succeeded for a Ganeti instance and
221
    # the instance is STARTED.  What if the two messages are processed by two
222
    # separate dispatcher threads, and the 'ganeti-op-status' message for
223
    # successful creation gets processed before the 'ganeti-create-progress'
224
    # message? [vkoukis]
225
    #
226
    #if not vm.operstate == 'BUILD':
227
    #    raise VirtualMachine.IllegalState("VM is not in building state")
228

    
229
    vm.buildpercentage = percentage
230
    vm.backendtime = etime
231
    vm.save()
232

    
233

    
234
def start_action(vm, action):
235
    """Update the state of a VM when a new action is initiated."""
236
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
237
        raise VirtualMachine.InvalidActionError(action)
238

    
239
    # No actions to deleted and no actions beside destroy to suspended VMs
240
    if vm.deleted:
241
        raise VirtualMachine.DeletedError
242

    
243
    # No actions to machines being built. They may be destroyed, however.
244
    if vm.operstate == 'BUILD' and action != 'DESTROY':
245
        raise VirtualMachine.BuildingError
246

    
247
    vm.action = action
248
    vm.backendjobid = None
249
    vm.backendopcode = None
250
    vm.backendjobstatus = None
251
    vm.backendlogmsg = None
252

    
253
    # Update the relevant flags if the VM is being suspended or destroyed.
254
    # Do not set the deleted flag here, see ticket #721.
255
    #
256
    # The deleted flag is set asynchronously, when an OP_INSTANCE_REMOVE
257
    # completes successfully. Hence, a server may be visible for some time
258
    # after a DELETE /servers/id returns HTTP 204.
259
    #
260
    if action == "DESTROY":
261
        # vm.deleted = True
262
        pass
263
    elif action == "SUSPEND":
264
        vm.suspended = True
265
    elif action == "START":
266
        vm.suspended = False
267
    vm.save()
268

    
269

    
270
def create_instance(vm, flavor, image, password, personality):
271
    """`image` is a dictionary which should contain the keys:
272
            'backend_id', 'format' and 'metadata'
273

274
        metadata value should be a dictionary.
275
    """
276

    
277
    # Get the Network object in exclusive mode in order to
278
    # safely (isolated) reserve an IP address
279
    network = Network.objects.select_for_update().get(id=1)
280
    pool = ippool.IPPool(network)
281
    try:
282
        address = pool.get_free_address()
283
    except ippool.IPPool.IPPoolExhausted:
284
        raise OverLimit("Can not allocate IP for new machine."
285
                        " Public network is full.")
286
    pool.save()
287

    
288
    nic = {'ip': address, 'network': settings.GANETI_PUBLIC_NETWORK}
289

    
290
    if settings.IGNORE_FLAVOR_DISK_SIZES:
291
        if image['backend_id'].find("windows") >= 0:
292
            sz = 14000
293
        else:
294
            sz = 4000
295
    else:
296
        sz = flavor.disk * 1024
297

    
298
    # Handle arguments to CreateInstance() as a dictionary,
299
    # initialize it based on a deployment-specific value.
300
    # This enables the administrator to override deployment-specific
301
    # arguments, such as the disk template to use, name of os provider
302
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
303
    #
304
    kw = settings.GANETI_CREATEINSTANCE_KWARGS
305
    kw['mode'] = 'create'
306
    kw['name'] = vm.backend_vm_id
307
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
308
    kw['disk_template'] = flavor.disk_template
309
    kw['disks'] = [{"size": sz}]
310
    kw['nics'] = [nic]
311
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
312
    # kw['os'] = settings.GANETI_OS_PROVIDER
313
    kw['ip_check'] = False
314
    kw['name_check'] = False
315
    # Do not specific a node explicitly, have
316
    # Ganeti use an iallocator instead
317
    #
318
    # kw['pnode']=rapi.GetNodes()[0]
319
    kw['dry_run'] = settings.TEST
320

    
321
    kw['beparams'] = {
322
        'auto_balance': True,
323
        'vcpus': flavor.cpu,
324
        'memory': flavor.ram}
325

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

    
333
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
334

    
335
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
336
    # kw['hvparams'] = dict(serial_console=False)
337

    
338
    return vm.client.CreateInstance(**kw)
339

    
340

    
341
def delete_instance(vm):
342
    start_action(vm, 'DESTROY')
343
    vm.client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
344

    
345

    
346
def reboot_instance(vm, reboot_type):
347
    assert reboot_type in ('soft', 'hard')
348
    vm.client.RebootInstance(vm.backend_vm_id, reboot_type, dry_run=settings.TEST)
349
    log.info('Rebooting instance %s', vm.backend_vm_id)
350

    
351

    
352
def startup_instance(vm):
353
    start_action(vm, 'START')
354
    vm.client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
355

    
356

    
357
def shutdown_instance(vm):
358
    start_action(vm, 'STOP')
359
    vm.client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
360

    
361

    
362
def get_instance_console(vm):
363
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
364
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
365
    # useless (see #783).
366
    #
367
    # Until this is fixed on the Ganeti side, construct a console info reply
368
    # directly.
369
    #
370
    # WARNING: This assumes that VNC runs on port network_port on
371
    #          the instance's primary node, and is probably
372
    #          hypervisor-specific.
373
    #
374
    console = {}
375
    console['kind'] = 'vnc'
376
    i = vm.client.GetInstance(vm.backend_vm_id)
377
    if i['hvparams']['serial_console']:
378
        raise Exception("hv parameter serial_console cannot be true")
379
    console['host'] = i['pnode']
380
    console['port'] = i['network_port']
381

    
382
    return console
383
    # return rapi.GetInstanceConsole(vm.backend_vm_id)
384

    
385

    
386
def request_status_update(vm):
387
    return vm.client.GetInstanceInfo(vm.backend_vm_id)
388

    
389

    
390
def update_status(vm, status):
391
    utils.update_state(vm, status)
392

    
393

    
394
def create_network(network, backends=None):
395
    """ Add and connect a network to backends.
396

397
    @param network: Network object
398
    @param backends: List of Backend objects. None defaults to all.
399

400
    """
401
    backend_jobs = _create_network(network, backends)
402
    connect_network(network, backend_jobs)
403
    return network
404

    
405

    
406
def _create_network(network, backends=None):
407
    """Add a network to backends.
408
    @param network: Network object
409
    @param backends: List of Backend objects. None defaults to all.
410

411
    """
412

    
413
    network_type = network.public and 'public' or 'private'
414

    
415
    if not backends:
416
        backends = Backend.objects.exclude(offline=True)
417

    
418
    tags = network.backend_tag
419
    if network.dhcp:
420
        tags.append('nfdhcpd')
421
    tags = ','.join(tags)
422

    
423
    backend_jobs = []
424
    for backend in backends:
425
        job = backend.client.CreateNetwork(
426
                network_name=network.backend_id,
427
                network=network.subnet,
428
                gateway=network.gateway,
429
                network_type=network_type,
430
                mac_prefix=network.mac_prefix,
431
                tags=tags)
432
        backend_jobs.append((backend, job))
433

    
434
    return backend_jobs
435

    
436

    
437
def connect_network(network, backend_jobs=None):
438
    """Connect a network to all nodegroups.
439

440
    @param network: Network object
441
    @param backend_jobs: List of tuples of the form (Backend, jobs) which are
442
                         the backends to connect the network and the jobs on
443
                         which the connect job depends.
444

445
    """
446

    
447
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
448
        mode = 'routed'
449
    else:
450
        mode = 'bridged'
451

    
452
    if not backend_jobs:
453
        backend_jobs = [(backend, []) for backend in
454
                        Backend.objects.exclude(offline=True)]
455

    
456
    for backend, job in backend_jobs:
457
        client = backend.client
458
        for group in client.GetGroups():
459
            client.ConnectNetwork(network.backend_id, group, mode,
460
                                  network.link, [job])
461

    
462

    
463
def connect_network_group(backend, network, group):
464
    """Connect a network to a specific nodegroup of a backend.
465

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

    
472
    return backend.client.ConnectNetwork(network.backend_id, group, mode,
473
                                         network.link)
474

    
475

    
476
def delete_network(network, backends=None):
477
    """ Disconnect and a remove a network from backends.
478

479
    @param network: Network object
480
    @param backends: List of Backend objects. None defaults to all.
481

482
    """
483
    backend_jobs = disconnect_network(network, backends)
484
    _delete_network(network, backend_jobs)
485

    
486

    
487
def disconnect_network(network, backends=None):
488
    """Disconnect a network from all nodegroups.
489

490
    @param network: Network object
491
    @param backends: List of Backend objects. None defaults to all.
492

493
    """
494

    
495
    if not backends:
496
        backends = Backend.objects.exclude(offline=True)
497

    
498
    backend_jobs = []
499
    for backend in backends:
500
        client = backend.client
501
        jobs = []
502
        for group in client.GetGroups():
503
            job = client.DisconnectNetwork(network.backend_id, group)
504
            jobs.append(job)
505
        backend_jobs.append((backend, jobs))
506

    
507
    return backend_jobs
508

    
509

    
510
def disconnect_from_network(vm, nic):
511
    """Disconnect a virtual machine from a network by removing it's nic.
512

513
    @param vm: VirtualMachine object
514
    @param network: Network object
515

516
    """
517

    
518
    op = [('remove', nic.index, {})]
519
    return vm.client.ModifyInstance(vm.backend_vm_id, nics=op,
520
                                   hotplug=True, dry_run=settings.TEST)
521

    
522

    
523
def _delete_network(network, backend_jobs=None):
524
    if not backend_jobs:
525
        backend_jobs = [(backend, []) for backend in
526
                Backend.objects.exclude(offline=True)]
527
    for backend, jobs in backend_jobs:
528
        backend.client.DeleteNetwork(network.backend_id, jobs)
529

    
530

    
531
def connect_to_network(vm, network, address):
532
    """Connect a virtual machine to a network.
533

534
    @param vm: VirtualMachine object
535
    @param network: Network object
536

537
    """
538

    
539
    # ip = network.dhcp and 'pool' or None
540

    
541
    nic = {'ip': address, 'network': network.backend_id}
542
    vm.client.ModifyInstance(vm.backend_vm_id, nics=[('add',  nic)],
543
                             hotplug=True, dry_run=settings.TEST)
544

    
545

    
546
def set_firewall_profile(vm, profile):
547
    try:
548
        tag = _firewall_tags[profile]
549
    except KeyError:
550
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
551

    
552
    client = vm.client
553
    # Delete all firewall tags
554
    for t in _firewall_tags.values():
555
        client.DeleteInstanceTags(vm.backend_vm_id, [t], dry_run=settings.TEST)
556

    
557
    client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
558

    
559
    # XXX NOP ModifyInstance call to force process_net_status to run
560
    # on the dispatcher
561
    vm.client.ModifyInstance(vm.backend_vm_id,
562
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
563

    
564

    
565
def get_ganeti_instances(backend=None, bulk=False):
566
    Instances = [c.client.GetInstances(bulk=bulk)\
567
                 for c in get_backends(backend)]
568
    return reduce(list.__add__, Instances, [])
569

    
570

    
571
def get_ganeti_nodes(backend=None, bulk=False):
572
    Nodes = [c.client.GetNodes(bulk=bulk) for c in get_backends(backend)]
573
    return reduce(list.__add__, Nodes, [])
574

    
575

    
576
def get_ganeti_jobs(backend=None, bulk=False):
577
    Jobs = [c.client.GetJobs(bulk=bulk) for c in get_backends(backend)]
578
    return reduce(list.__add__, Jobs, [])
579

    
580
##
581
##
582
##
583

    
584

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

    
590

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

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

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

    
611

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

615
    """
616

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

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

    
629

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

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

636
    """
637
    instances = backend.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
    client = backend.client
658
    job = client.CreateNetwork(network.backend_id, network.subnet)
659
    return wait_for_job(client, job)
660

    
661

    
662
def connect_network_synced(network, backend):
663
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
664
        mode = 'routed'
665
    else:
666
        mode = 'bridged'
667
    client = backend.client
668

    
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)