Statistics
| Branch: | Tag: | Revision:

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

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 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)
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
        utils.update_state(vm, state_for_success)
80
        # Set the deleted flag explicitly, cater for admin-initiated removals
81
        if opcode == 'OP_INSTANCE_REMOVE':
82
            release_instance_nics(vm)
83
            vm.deleted = True
84
            vm.nics.all().delete()
85

    
86
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
87
    if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
88
        utils.update_state(vm, 'ERROR')
89

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

    
98
    vm.backendtime = etime
99
    # Any other notification of failure leaves the operating state unchanged
100

    
101
    vm.save()
102

    
103

    
104
@transaction.commit_on_success
105
def process_net_status(vm, etime, nics):
106
    """Process a net status notification from the backend
107

108
    Process an incoming message from the Ganeti backend,
109
    detailing the NIC configuration of a VM instance.
110

111
    Update the state of the VM in the DB accordingly.
112
    """
113

    
114
    release_instance_nics(vm)
115

    
116
    new_nics = enumerate(nics)
117
    for i, new_nic in new_nics:
118
        network = new_nic.get('network', '')
119
        n = str(network)
120
        pk = utils.id_from_network_name(n)
121

    
122
        net = Network.objects.get(pk=pk)
123

    
124
        # Get the new nic info
125
        mac = new_nic.get('mac', '')
126
        ipv4 = new_nic.get('ip', '')
127
        ipv6 = new_nic.get('ipv6', '')
128

    
129
        firewall = new_nic.get('firewall', '')
130
        firewall_profile = _reverse_tags.get(firewall, '')
131
        if not firewall_profile and net.public:
132
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
133

    
134
        if ipv4:
135
            net.reserve_address(ipv4)
136

    
137
        vm.nics.create(
138
            network=net,
139
            index=i,
140
            mac=mac,
141
            ipv4=ipv4,
142
            ipv6=ipv6,
143
            firewall_profile=firewall_profile,
144
            dirty=False)
145

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

    
149

    
150
def release_instance_nics(vm):
151
    for nic in vm.nics.all():
152
        nic.network.release_address(nic.ipv4)
153
        nic.delete()
154

    
155

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

    
162
    back_network.backendjobid = jobid
163
    back_network.backendjobstatus = status
164
    back_network.backendopcode = opcode
165
    back_network.backendlogmsg = logmsg
166

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

    
174
    if status in ('canceled', 'error') and opcode == 'OP_NETWORK_CREATE':
175
        utils.update_state(back_network, 'ERROR')
176

    
177
    if (status == 'error' and opcode == 'OP_NETWORK_REMOVE'):
178
        back_network.deleted = True
179
        back_network.operstate = 'DELETED'
180

    
181
    back_network.save()
182

    
183

    
184
@transaction.commit_on_success
185
def process_create_progress(vm, etime, rprogress, wprogress):
186

    
187
    # XXX: This only uses the read progress for now.
188
    #      Explore whether it would make sense to use the value of wprogress
189
    #      somewhere.
190
    percentage = int(rprogress)
191

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

    
198
    # FIXME: log a warning here, see #1033
199
#   if last_update > percentage:
200
#       raise ValueError("Build percentage should increase monotonically " \
201
#                        "(old = %d, new = %d)" % (last_update, percentage))
202

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

    
213
    vm.buildpercentage = percentage
214
    vm.backendtime = etime
215
    vm.save()
216

    
217

    
218
def start_action(vm, action):
219
    """Update the state of a VM when a new action is initiated."""
220
    log.debug("Applying action %s to VM %s", action, vm)
221

    
222
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
223
        raise VirtualMachine.InvalidActionError(action)
224

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

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

    
233
    vm.action = action
234
    vm.backendjobid = None
235
    vm.backendopcode = None
236
    vm.backendjobstatus = None
237
    vm.backendlogmsg = None
238

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

    
255

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

260
        metadata value should be a dictionary.
261
    """
262

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

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

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

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

    
296
        if provider == 'vlmc':
297
            kw['disks'][0]['origin'] = image['backend_id']
298

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

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

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

    
324
    if provider != None and provider == 'vlmc':
325
        kw['osparams']['img_id'] = 'null'
326

    
327
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
328

    
329
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
330
    # kw['hvparams'] = dict(serial_console=False)
331
    log.debug("Creating instance %s", utils.hide_pass(kw))
332
    with pooled_rapi_client(vm) as client:
333
        return client.CreateInstance(**kw)
334

    
335

    
336
def delete_instance(vm):
337
    start_action(vm, 'DESTROY')
338
    with pooled_rapi_client(vm) as client:
339
        return client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
340

    
341

    
342
def reboot_instance(vm, reboot_type):
343
    assert reboot_type in ('soft', 'hard')
344
    with pooled_rapi_client(vm) as client:
345
        return client.RebootInstance(vm.backend_vm_id, reboot_type,
346
                                     dry_run=settings.TEST)
347

    
348

    
349
def startup_instance(vm):
350
    start_action(vm, 'START')
351
    with pooled_rapi_client(vm) as client:
352
        return client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
353

    
354

    
355
def shutdown_instance(vm):
356
    start_action(vm, 'STOP')
357
    with pooled_rapi_client(vm) as client:
358
        return client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
359

    
360

    
361
def get_instance_console(vm):
362
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
363
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
364
    # useless (see #783).
365
    #
366
    # Until this is fixed on the Ganeti side, construct a console info reply
367
    # directly.
368
    #
369
    # WARNING: This assumes that VNC runs on port network_port on
370
    #          the instance's primary node, and is probably
371
    #          hypervisor-specific.
372
    #
373
    log.debug("Getting console for vm %s", vm)
374

    
375
    console = {}
376
    console['kind'] = 'vnc'
377

    
378
    with pooled_rapi_client(vm) as client:
379
        i = client.GetInstance(vm.backend_vm_id)
380

    
381
    if i['hvparams']['serial_console']:
382
        raise Exception("hv parameter serial_console cannot be true")
383
    console['host'] = i['pnode']
384
    console['port'] = i['network_port']
385

    
386
    return console
387

    
388

    
389
def get_instance_info(vm):
390
    with pooled_rapi_client(vm) as client:
391
        return client.GetInstanceInfo(vm.backend_vm_id)
392

    
393

    
394
def create_network(network, backends=None, connect=True):
395
    """Create and connect a network."""
396
    if not backends:
397
        backends = Backend.objects.exclude(offline=True)
398

    
399
    log.debug("Creating network %s in backends %s", network, backends)
400

    
401
    for backend in backends:
402
        create_jobID = _create_network(network, backend)
403
        if connect:
404
            connect_network(network, backend, create_jobID)
405

    
406

    
407
def _create_network(network, backend):
408
    """Create a network."""
409

    
410
    network_type = network.public and 'public' or 'private'
411

    
412
    tags = network.backend_tag
413
    if network.dhcp:
414
        tags.append('nfdhcpd')
415
    tags = ','.join(tags)
416

    
417
    try:
418
        bn = BackendNetwork.objects.get(network=network, backend=backend)
419
        mac_prefix = bn.mac_prefix
420
    except BackendNetwork.DoesNotExist:
421
        raise Exception("BackendNetwork for network '%s' in backend '%s'"\
422
                        " does not exist" % (network.id, backend.id))
423

    
424
    with pooled_rapi_client(backend) as client:
425
        return client.CreateNetwork(network_name=network.backend_id,
426
                                    network=network.subnet,
427
                                    gateway=network.gateway,
428
                                    network_type=network_type,
429
                                    mac_prefix=mac_prefix,
430
                                    tags=tags)
431

    
432

    
433
def connect_network(network, backend, depend_job=None, group=None):
434
    """Connect a network to nodegroups."""
435
    log.debug("Connecting network %s to backend %s", network, backend)
436

    
437
    mode = "routed" if "ROUTED" in network.type else "bridged"
438

    
439
    with pooled_rapi_client(backend) as client:
440
        if group:
441
            client.ConnectNetwork(network.backend_id, group, mode,
442
                                  network.link, [depend_job])
443
        else:
444
            for group in client.GetGroups():
445
                client.ConnectNetwork(network.backend_id, group, mode,
446
                                      network.link, [depend_job])
447

    
448

    
449
def delete_network(network, backends=None, disconnect=True):
450
    if not backends:
451
        backends = Backend.objects.exclude(offline=True)
452

    
453
    log.debug("Deleting network %s from backends %s", network, backends)
454

    
455
    for backend in backends:
456
        disconnect_jobIDs = []
457
        if disconnect:
458
            disconnect_jobIDs = disconnect_network(network, backend)
459
        _delete_network(network, backend, disconnect_jobIDs)
460

    
461

    
462
def _delete_network(network, backend, depend_jobs=[]):
463
    with pooled_rapi_client(backend) as client:
464
        return client.DeleteNetwork(network.backend_id, depend_jobs)
465

    
466

    
467
def disconnect_network(network, backend, group=None):
468
    log.debug("Disconnecting network %s to backend %s", network, backend)
469

    
470
    with pooled_rapi_client(backend) as client:
471
        if group:
472
            return [client.DisconnectNetwork(network.backend_id, group)]
473
        else:
474
            jobs = []
475
            for group in client.GetGroups():
476
                job = client.DisconnectNetwork(network.backend_id, group)
477
                jobs.append(job)
478
            return jobs
479

    
480

    
481
def connect_to_network(vm, network, address):
482
    nic = {'ip': address, 'network': network.backend_id}
483

    
484
    log.debug("Connecting vm %s to network %s(%s)", vm, network, address)
485

    
486
    with pooled_rapi_client(vm) as client:
487
        return client.ModifyInstance(vm.backend_vm_id, nics=[('add',  nic)],
488
                                     hotplug=settings.GANETI_USE_HOTPLUG,
489
                                     dry_run=settings.TEST)
490

    
491

    
492
def disconnect_from_network(vm, nic):
493
    op = [('remove', nic.index, {})]
494

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

    
497
    with pooled_rapi_client(vm) as client:
498
        return client.ModifyInstance(vm.backend_vm_id, nics=op,
499
                                     hotplug=settings.GANETI_USE_HOTPLUG,
500
                                     dry_run=settings.TEST)
501

    
502

    
503
def set_firewall_profile(vm, profile):
504
    try:
505
        tag = _firewall_tags[profile]
506
    except KeyError:
507
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
508

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

    
511
    with pooled_rapi_client(vm) as client:
512
        # Delete all firewall tags
513
        for t in _firewall_tags.values():
514
            client.DeleteInstanceTags(vm.backend_vm_id, [t],
515
                                      dry_run=settings.TEST)
516

    
517
        client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
518

    
519
        # XXX NOP ModifyInstance call to force process_net_status to run
520
        # on the dispatcher
521
        client.ModifyInstance(vm.backend_vm_id,
522
                         os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
523

    
524

    
525
def get_ganeti_instances(backend=None, bulk=False):
526
    instances = []
527
    for backend in get_backends(backend):
528
        with pooled_rapi_client(backend) as client:
529
            instances.append(client.GetInstances(bulk=bulk))
530

    
531
    return reduce(list.__add__, instances, [])
532

    
533

    
534
def get_ganeti_nodes(backend=None, bulk=False):
535
    nodes = []
536
    for backend in get_backends(backend):
537
        with pooled_rapi_client(backend) as client:
538
            nodes.append(client.GetNodes(bulk=bulk))
539

    
540
    return reduce(list.__add__, nodes, [])
541

    
542

    
543
def get_ganeti_jobs(backend=None, bulk=False):
544
    jobs = []
545
    for backend in get_backends(backend):
546
        with pooled_rapi_client(backend) as client:
547
            jobs.append(client.GetJobs(bulk=bulk))
548
    return reduce(list.__add__, jobs, [])
549

    
550
##
551
##
552
##
553

    
554

    
555
def get_backends(backend=None):
556
    if backend:
557
        return [backend]
558
    return Backend.objects.filter(offline=False)
559

    
560

    
561
def get_physical_resources(backend):
562
    """ Get the physical resources of a backend.
563

564
    Get the resources of a backend as reported by the backend (not the db).
565

566
    """
567
    nodes = get_ganeti_nodes(backend, bulk=True)
568
    attr = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal']
569
    res = {}
570
    for a in attr:
571
        res[a] = 0
572
    for n in nodes:
573
        # Filter out drained, offline and not vm_capable nodes since they will
574
        # not take part in the vm allocation process
575
        if n['vm_capable'] and not n['drained'] and not n['offline']\
576
           and n['cnodes']:
577
            for a in attr:
578
                res[a] += int(n[a])
579
    return res
580

    
581

    
582
def update_resources(backend, resources=None):
583
    """ Update the state of the backend resources in db.
584

585
    """
586

    
587
    if not resources:
588
        resources = get_physical_resources(backend)
589

    
590
    backend.mfree = resources['mfree']
591
    backend.mtotal = resources['mtotal']
592
    backend.dfree = resources['dfree']
593
    backend.dtotal = resources['dtotal']
594
    backend.pinst_cnt = resources['pinst_cnt']
595
    backend.ctotal = resources['ctotal']
596
    backend.updated = datetime.now()
597
    backend.save()
598

    
599

    
600
def get_memory_from_instances(backend):
601
    """ Get the memory that is used from instances.
602

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

606
    """
607
    with pooled_rapi_client(backend) as client:
608
        instances = client.GetInstances(bulk=True)
609
    mem = 0
610
    for i in instances:
611
        mem += i['oper_ram']
612
    return mem
613

    
614
##
615
## Synchronized operations for reconciliation
616
##
617

    
618

    
619
def create_network_synced(network, backend):
620
    result = _create_network_synced(network, backend)
621
    if result[0] != 'success':
622
        return result
623
    result = connect_network_synced(network, backend)
624
    return result
625

    
626

    
627
def _create_network_synced(network, backend):
628
    with pooled_rapi_client(backend) as client:
629
        backend_jobs = _create_network(network, [backend])
630
        (_, job) = backend_jobs[0]
631
        result = wait_for_job(client, job)
632
    return result
633

    
634

    
635
def connect_network_synced(network, backend):
636
    if network.type in ('PUBLIC_ROUTED', 'CUSTOM_ROUTED'):
637
        mode = 'routed'
638
    else:
639
        mode = 'bridged'
640
    with pooled_rapi_client(backend) as client:
641
        for group in client.GetGroups():
642
            job = client.ConnectNetwork(network.backend_id, group, mode,
643
                                        network.link)
644
            result = wait_for_job(client, job)
645
            if result[0] != 'success':
646
                return result
647

    
648
    return result
649

    
650

    
651
def wait_for_job(client, jobid):
652
    result = client.WaitForJobChange(jobid, ['status', 'opresult'], None, None)
653
    status = result['job_info'][0]
654
    while status not in ['success', 'error', 'cancel']:
655
        result = client.WaitForJobChange(jobid, ['status', 'opresult'],
656
                                        [result], None)
657
        status = result['job_info'][0]
658

    
659
    if status == 'success':
660
        return (status, None)
661
    else:
662
        error = result['job_info'][1]
663
        return (status, error)