Statistics
| Branch: | Tag: | Revision:

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

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
44
from synnefo.util.rapi import GanetiRapiClient
45

    
46
log = getLogger('synnefo.logic')
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
def create_client(hostname, port=5080, username=None, password=None):
58
    return GanetiRapiClient(hostname, port, username, password)
59

    
60

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

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

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

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

    
80
    # Notifications of success change the operating state
81
    state_for_success = VirtualMachine.OPER_STATE_FROM_OPCODE.get(opcode, None)
82
    if status == 'success' and state_for_success is not None:
83
        utils.update_state(vm, state_for_success)
84
        # Set the deleted flag explicitly, cater for admin-initiated removals
85
        if opcode == 'OP_INSTANCE_REMOVE':
86
            release_instance_nics(vm)
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
    release_instance_nics(vm)
119

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

    
126
        net = Network.objects.get(pk=pk)
127

    
128
        # Get the new nic info
129
        mac = new_nic.get('mac', '')
130
        ipv4 = new_nic.get('ip', '')
131
        ipv6 = new_nic.get('ipv6', '')
132

    
133
        firewall = new_nic.get('firewall', '')
134
        firewall_profile = _reverse_tags.get(firewall, '')
135
        if not firewall_profile and net.public:
136
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
137

    
138
        if ipv4:
139
            net.reserve_address(ipv4)
140

    
141
        vm.nics.create(
142
            network=net,
143
            index=i,
144
            mac=mac,
145
            ipv4=ipv4,
146
            ipv6=ipv6,
147
            firewall_profile=firewall_profile,
148
            dirty=False)
149

    
150
    vm.backendtime = etime
151
    vm.save()
152

    
153

    
154
def release_instance_nics(vm):
155
    for nic in vm.nics.all():
156
        nic.network.release_address(nic.ipv4)
157
        nic.delete()
158

    
159

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

    
166
    back_network.backendjobid = jobid
167
    back_network.backendjobstatus = status
168
    back_network.backendopcode = opcode
169
    back_network.backendlogmsg = logmsg
170

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

    
178
    if status in ('canceled', 'error') and opcode == 'OP_NETWORK_CREATE':
179
        utils.update_state(back_network, 'ERROR')
180

    
181
    if (status == 'error' and opcode == 'OP_NETWORK_REMOVE'):
182
        back_network.deleted = True
183
        back_network.operstate = 'DELETED'
184

    
185
    back_network.save()
186

    
187

    
188
@transaction.commit_on_success
189
def process_create_progress(vm, etime, rprogress, wprogress):
190

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

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

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

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

    
217
    vm.buildpercentage = percentage
218
    vm.backendtime = etime
219
    vm.save()
220

    
221

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

    
227
    # No actions to deleted and no actions beside destroy to suspended VMs
228
    if vm.deleted:
229
        raise VirtualMachine.DeletedError
230

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

    
235
    vm.action = action
236
    vm.backendjobid = None
237
    vm.backendopcode = None
238
    vm.backendjobstatus = None
239
    vm.backendlogmsg = None
240

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

    
257

    
258
def create_instance(vm, public_nic, flavor, image, password, personality):
259
    """`image` is a dictionary which should contain the keys:
260
            'backend_id', 'format' and 'metadata'
261

262
        metadata value should be a dictionary.
263
    """
264

    
265
    if settings.IGNORE_FLAVOR_DISK_SIZES:
266
        if image['backend_id'].find("windows") >= 0:
267
            sz = 14000
268
        else:
269
            sz = 4000
270
    else:
271
        sz = flavor.disk * 1024
272

    
273
    # Handle arguments to CreateInstance() as a dictionary,
274
    # initialize it based on a deployment-specific value.
275
    # This enables the administrator to override deployment-specific
276
    # arguments, such as the disk template to use, name of os provider
277
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
278
    #
279
    kw = settings.GANETI_CREATEINSTANCE_KWARGS
280
    kw['mode'] = 'create'
281
    kw['name'] = vm.backend_vm_id
282
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
283

    
284
    # Identify if provider parameter should be set in disk options.
285
    # Current implementation support providers only fo ext template.
286
    # To select specific provider for an ext template, template name
287
    # should be formated as `ext_<provider_name>`.
288
    provider = None
289
    disk_template = flavor.disk_template
290
    if flavor.disk_template.startswith("ext"):
291
        disk_template, provider = flavor.disk_template.split("_", 1)
292

    
293
    kw['disk_template'] = disk_template
294
    kw['disks'] = [{"size": sz}]
295
    if provider:
296
        kw['disks'][0]['provider'] = provider
297

    
298
    kw['nics'] = [public_nic]
299
    if settings.GANETI_USE_HOTPLUG:
300
        kw['hotplug'] = True
301
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
302
    # kw['os'] = settings.GANETI_OS_PROVIDER
303
    kw['ip_check'] = False
304
    kw['name_check'] = False
305
    # Do not specific a node explicitly, have
306
    # Ganeti use an iallocator instead
307
    #
308
    # kw['pnode']=rapi.GetNodes()[0]
309
    kw['dry_run'] = settings.TEST
310

    
311
    kw['beparams'] = {
312
        'auto_balance': True,
313
        'vcpus': flavor.cpu,
314
        'memory': flavor.ram}
315

    
316
    kw['osparams'] = {
317
        'img_id': image['backend_id'],
318
        'img_passwd': password,
319
        'img_format': image['format']}
320
    if personality:
321
        kw['osparams']['img_personality'] = json.dumps(personality)
322

    
323
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
324

    
325
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
326
    # kw['hvparams'] = dict(serial_console=False)
327

    
328
    return vm.client.CreateInstance(**kw)
329

    
330

    
331
def delete_instance(vm):
332
    start_action(vm, 'DESTROY')
333
    vm.client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
334

    
335

    
336
def reboot_instance(vm, reboot_type):
337
    assert reboot_type in ('soft', 'hard')
338
    vm.client.RebootInstance(vm.backend_vm_id, reboot_type, dry_run=settings.TEST)
339
    log.info('Rebooting instance %s', vm.backend_vm_id)
340

    
341

    
342
def startup_instance(vm):
343
    start_action(vm, 'START')
344
    vm.client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
345

    
346

    
347
def shutdown_instance(vm):
348
    start_action(vm, 'STOP')
349
    vm.client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
350

    
351

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

    
372
    return console
373
    # return rapi.GetInstanceConsole(vm.backend_vm_id)
374

    
375

    
376
def request_status_update(vm):
377
    return vm.client.GetInstanceInfo(vm.backend_vm_id)
378

    
379

    
380
def update_status(vm, status):
381
    utils.update_state(vm, status)
382

    
383

    
384
def create_network(network, backends=None):
385
    """ Add and connect a network to backends.
386

387
    @param network: Network object
388
    @param backends: List of Backend objects. None defaults to all.
389

390
    """
391
    backend_jobs = _create_network(network, backends)
392
    connect_network(network, backend_jobs)
393
    return network
394

    
395

    
396
def _create_network(network, backends=None):
397
    """Add a network to backends.
398
    @param network: Network object
399
    @param backends: List of Backend objects. None defaults to all.
400

401
    """
402

    
403
    network_type = network.public and 'public' or 'private'
404
    if not backends:
405
        backends = Backend.objects.exclude(offline=True)
406

    
407
    tags = network.backend_tag
408
    if network.dhcp:
409
        tags.append('nfdhcpd')
410
    tags = ','.join(tags)
411

    
412
    backend_jobs = []
413
    for backend in backends:
414
        try:
415
            backend_network = BackendNetwork.objects.get(network=network,
416
                                                         backend=backend)
417
        except BackendNetwork.DoesNotExist:
418
            raise Exception("BackendNetwork for network '%s' in backend '%s'"\
419
                            " does not exist" % (network.id, backend.id))
420
        job = backend.client.CreateNetwork(
421
                network_name=network.backend_id,
422
                network=network.subnet,
423
                gateway=network.gateway,
424
                network_type=network_type,
425
                mac_prefix=backend_network.mac_prefix,
426
                tags=tags)
427
        backend_jobs.append((backend, job))
428

    
429
    return backend_jobs
430

    
431

    
432
def connect_network(network, backend_jobs=None):
433
    """Connect a network to all nodegroups.
434

435
    @param network: Network object
436
    @param backend_jobs: List of tuples of the form (Backend, jobs) which are
437
                         the backends to connect the network and the jobs on
438
                         which the connect job depends.
439

440
    """
441

    
442
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
443
        mode = 'routed'
444
    else:
445
        mode = 'bridged'
446

    
447
    if not backend_jobs:
448
        backend_jobs = [(backend, []) for backend in
449
                        Backend.objects.exclude(offline=True)]
450

    
451
    for backend, job in backend_jobs:
452
        client = backend.client
453
        for group in client.GetGroups():
454
            client.ConnectNetwork(network.backend_id, group, mode,
455
                                  network.link, [job])
456

    
457

    
458
def connect_network_group(backend, network, group):
459
    """Connect a network to a specific nodegroup of a backend.
460

461
    """
462
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
463
        mode = 'routed'
464
    else:
465
        mode = 'bridged'
466

    
467
    return backend.client.ConnectNetwork(network.backend_id, group, mode,
468
                                         network.link)
469

    
470

    
471
def delete_network(network, backends=None):
472
    """ Disconnect and a remove a network from backends.
473

474
    @param network: Network object
475
    @param backends: List of Backend objects. None defaults to all.
476

477
    """
478
    backend_jobs = disconnect_network(network, backends)
479
    _delete_network(network, backend_jobs)
480

    
481

    
482
def disconnect_network(network, backends=None):
483
    """Disconnect a network from all nodegroups.
484

485
    @param network: Network object
486
    @param backends: List of Backend objects. None defaults to all.
487

488
    """
489

    
490
    if not backends:
491
        backends = Backend.objects.exclude(offline=True)
492

    
493
    backend_jobs = []
494
    for backend in backends:
495
        client = backend.client
496
        jobs = []
497
        for group in client.GetGroups():
498
            job = client.DisconnectNetwork(network.backend_id, group)
499
            jobs.append(job)
500
        backend_jobs.append((backend, jobs))
501

    
502
    return backend_jobs
503

    
504

    
505
def disconnect_from_network(vm, nic):
506
    """Disconnect a virtual machine from a network by removing it's nic.
507

508
    @param vm: VirtualMachine object
509
    @param network: Network object
510

511
    """
512

    
513
    op = [('remove', nic.index, {})]
514
    return vm.client.ModifyInstance(vm.backend_vm_id, nics=op,
515
                                    hotplug=settings.GANETI_USE_HOTPLUG,
516
                                    dry_run=settings.TEST)
517

    
518

    
519
def _delete_network(network, backend_jobs=None):
520
    if not backend_jobs:
521
        backend_jobs = [(backend, []) for backend in
522
                Backend.objects.exclude(offline=True)]
523
    for backend, jobs in backend_jobs:
524
        backend.client.DeleteNetwork(network.backend_id, jobs)
525

    
526

    
527
def connect_to_network(vm, network, address):
528
    """Connect a virtual machine to a network.
529

530
    @param vm: VirtualMachine object
531
    @param network: Network object
532

533
    """
534

    
535
    # ip = network.dhcp and 'pool' or None
536

    
537
    nic = {'ip': address, 'network': network.backend_id}
538
    vm.client.ModifyInstance(vm.backend_vm_id, nics=[('add',  nic)],
539
                             hotplug=settings.GANETI_USE_HOTPLUG,
540
                             dry_run=settings.TEST)
541

    
542

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

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

    
554
    client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
555

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

    
561

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

    
567

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

    
572

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

    
577
##
578
##
579
##
580

    
581

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

    
587

    
588
def get_physical_resources(backend):
589
    """ Get the physical resources of a backend.
590

591
    Get the resources of a backend as reported by the backend (not the db).
592

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

    
608

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

612
    """
613

    
614
    if not resources:
615
        resources = get_physical_resources(backend)
616

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

    
626

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

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

633
    """
634
    instances = backend.client.GetInstances(bulk=True)
635
    mem = 0
636
    for i in instances:
637
        mem += i['oper_ram']
638
    return mem
639

    
640
##
641
## Synchronized operations for reconciliation
642
##
643

    
644

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

    
652

    
653
def _create_network_synced(network, backend):
654
    client = backend.client
655

    
656
    backend_jobs = _create_network(network, [backend])
657
    (_, job) = backend_jobs[0]
658
    return wait_for_job(client, job)
659

    
660

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

    
668
    for group in client.GetGroups():
669
        job = client.ConnectNetwork(network.backend_id, group, mode,
670
                                    network.link)
671
        result = wait_for_job(client, job)
672
        if result[0] != 'success':
673
            return result
674

    
675
    return result
676

    
677

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

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