Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.1 kB)

1
# Copyright 2011-2013 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
from django.conf import settings
34
from django.db import transaction
35
from datetime import datetime
36

    
37
from synnefo.db.models import (Backend, VirtualMachine, Network,
38
                               BackendNetwork, BACKEND_STATUSES,
39
                               pooled_rapi_client, VirtualMachineDiagnostic)
40
from synnefo.logic import utils
41
from synnefo import quotas
42
from synnefo.api.util import release_resource
43
from synnefo.util.mac2eui64 import mac2eui64
44
from synnefo.logic.rapi import GanetiApiError
45

    
46
from logging import getLogger
47
log = getLogger(__name__)
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
@transaction.commit_on_success
59
def process_op_status(vm, etime, jobid, opcode, status, logmsg, nics=None):
60
    """Process a job progress notification from the backend
61

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

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

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

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

    
82
    # Update the NICs of the VM
83
    if status == "success" and nics is not None:
84
        _process_net_status(vm, etime, nics)
85

    
86
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
87
    if opcode == 'OP_INSTANCE_CREATE' and status in ('canceled', 'error'):
88
        vm.operstate = 'ERROR'
89
        vm.backendtime = etime
90
    elif opcode == 'OP_INSTANCE_REMOVE':
91
        # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR,
92
        # when no instance exists at the Ganeti backend.
93
        if status == "success" or (status == "error" and
94
                                   not vm_exists_in_backend(vm)):
95
            _process_net_status(vm, etime, nics=[])
96
            already_deleted = vm.deleted
97
            vm.deleted = True
98
            vm.operstate = state_for_success
99
            vm.backendtime = etime
100
            # Issue and accept commission to Quotaholder
101
            if not already_deleted:
102
                quotas.issue_and_accept_commission(vm, delete=True)
103

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

    
111
    vm.save()
112

    
113

    
114
@transaction.commit_on_success
115
def process_net_status(vm, etime, nics):
116
    """Wrap _process_net_status inside transaction."""
117
    _process_net_status(vm, etime, nics)
118

    
119

    
120
def _process_net_status(vm, etime, nics):
121
    """Process a net status notification from the backend
122

123
    Process an incoming message from the Ganeti backend,
124
    detailing the NIC configuration of a VM instance.
125

126
    Update the state of the VM in the DB accordingly.
127
    """
128

    
129
    ganeti_nics = process_ganeti_nics(nics)
130
    if not nics_changed(vm.nics.order_by('index'), ganeti_nics):
131
        log.debug("NICs for VM %s have not changed", vm)
132
        return
133

    
134
    # Get X-Lock on backend before getting X-Lock on network IP pools, to
135
    # guarantee that no deadlock will occur with Backend allocator.
136
    Backend.objects.select_for_update().get(id=vm.backend_id)
137

    
138
    release_instance_nics(vm)
139

    
140
    for nic in ganeti_nics:
141
        ipv4 = nic.get('ipv4', '')
142
        net = nic['network']
143
        if ipv4:
144
            net.reserve_address(ipv4)
145

    
146
        nic['dirty'] = False
147
        vm.nics.create(**nic)
148
        # Dummy save the network, because UI uses changed-since for VMs
149
        # and Networks in order to show the VM NICs
150
        net.save()
151

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

    
155

    
156
def process_ganeti_nics(ganeti_nics):
157
    """Process NIC dict from ganeti hooks."""
158
    new_nics = []
159
    for i, new_nic in enumerate(ganeti_nics):
160
        network = new_nic.get('network', '')
161
        n = str(network)
162
        pk = utils.id_from_network_name(n)
163

    
164
        net = Network.objects.get(pk=pk)
165

    
166
        # Get the new nic info
167
        mac = new_nic.get('mac', '')
168
        ipv4 = new_nic.get('ip', '')
169
        if net.subnet6:
170
            ipv6 = mac2eui64(mac, net.subnet6)
171
        else:
172
            ipv6 = ''
173

    
174
        firewall = new_nic.get('firewall', '')
175
        firewall_profile = _reverse_tags.get(firewall, '')
176
        if not firewall_profile and net.public:
177
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
178

    
179
        nic = {
180
            'index': i,
181
            'network': net,
182
            'mac': mac,
183
            'ipv4': ipv4,
184
            'ipv6': ipv6,
185
            'firewall_profile': firewall_profile,
186
            'state': 'ACTIVE'}
187

    
188
        new_nics.append(nic)
189
    return new_nics
190

    
191

    
192
def nics_changed(old_nics, new_nics):
193
    """Return True if NICs have changed in any way."""
194
    if len(old_nics) != len(new_nics):
195
        return True
196
    fields = ["ipv4", "ipv6", "mac", "firewall_profile", "index", "network"]
197
    for old_nic, new_nic in zip(old_nics, new_nics):
198
        for field in fields:
199
            if getattr(old_nic, field) != new_nic[field]:
200
                return True
201
    return False
202

    
203

    
204
def release_instance_nics(vm):
205
    for nic in vm.nics.all():
206
        net = nic.network
207
        if nic.ipv4:
208
            net.release_address(nic.ipv4)
209
        nic.delete()
210
        net.save()
211

    
212

    
213
@transaction.commit_on_success
214
def process_network_status(back_network, etime, jobid, opcode, status, logmsg):
215
    if status not in [x[0] for x in BACKEND_STATUSES]:
216
        raise Network.InvalidBackendMsgError(opcode, status)
217

    
218
    back_network.backendjobid = jobid
219
    back_network.backendjobstatus = status
220
    back_network.backendopcode = opcode
221
    back_network.backendlogmsg = logmsg
222

    
223
    network = back_network.network
224

    
225
    # Notifications of success change the operating state
226
    state_for_success = BackendNetwork.OPER_STATE_FROM_OPCODE.get(opcode, None)
227
    if status == 'success' and state_for_success is not None:
228
        back_network.operstate = state_for_success
229

    
230
    if status in ('canceled', 'error') and opcode == 'OP_NETWORK_ADD':
231
        back_network.operstate = 'ERROR'
232
        back_network.backendtime = etime
233

    
234
    if opcode == 'OP_NETWORK_REMOVE':
235
        network_is_deleted = (status == "success")
236
        if network_is_deleted or (status == "error" and not
237
                                  network_exists_in_backend(back_network)):
238
            back_network.operstate = state_for_success
239
            back_network.deleted = True
240
            back_network.backendtime = etime
241

    
242
    if status == 'success':
243
        back_network.backendtime = etime
244
    back_network.save()
245
    # Also you must update the state of the Network!!
246
    update_network_state(network)
247

    
248

    
249
def update_network_state(network):
250
    """Update the state of a Network based on BackendNetwork states.
251

252
    Update the state of a Network based on the operstate of the networks in the
253
    backends that network exists.
254

255
    The state of the network is:
256
    * ACTIVE: If it is 'ACTIVE' in at least one backend.
257
    * DELETED: If it is is 'DELETED' in all backends that have been created.
258

259
    This function also releases the resources (MAC prefix or Bridge) and the
260
    quotas for the network.
261

262
    """
263
    if network.deleted:
264
        # Network has already been deleted. Just assert that state is also
265
        # DELETED
266
        if not network.state == "DELETED":
267
            network.state = "DELETED"
268
            network.save()
269
        return
270

    
271
    backend_states = [s.operstate for s in network.backend_networks.all()]
272
    if not backend_states and network.action != "DESTROY":
273
        if network.state != "ACTIVE":
274
            network.state = "ACTIVE"
275
            network.save()
276
            return
277

    
278
    # Network is deleted when all BackendNetworks go to "DELETED" operstate
279
    deleted = reduce(lambda x, y: x == y and "DELETED", backend_states,
280
                     "DELETED")
281

    
282
    # Release the resources on the deletion of the Network
283
    if deleted:
284
        log.info("Network %r deleted. Releasing link %r mac_prefix %r",
285
                 network.id, network.mac_prefix, network.link)
286
        network.deleted = True
287
        network.state = "DELETED"
288
        if network.mac_prefix:
289
            if network.FLAVORS[network.flavor]["mac_prefix"] == "pool":
290
                release_resource(res_type="mac_prefix",
291
                                 value=network.mac_prefix)
292
        if network.link:
293
            if network.FLAVORS[network.flavor]["link"] == "pool":
294
                release_resource(res_type="bridge", value=network.link)
295

    
296
        # Issue commission
297
        if network.userid:
298
            quotas.issue_and_accept_commission(network, delete=True)
299
        elif not network.public:
300
            log.warning("Network %s does not have an owner!", network.id)
301
    network.save()
302

    
303

    
304
@transaction.commit_on_success
305
def process_network_modify(back_network, etime, jobid, opcode, status,
306
                           add_reserved_ips, remove_reserved_ips):
307
    assert (opcode == "OP_NETWORK_SET_PARAMS")
308
    if status not in [x[0] for x in BACKEND_STATUSES]:
309
        raise Network.InvalidBackendMsgError(opcode, status)
310

    
311
    back_network.backendjobid = jobid
312
    back_network.backendjobstatus = status
313
    back_network.opcode = opcode
314

    
315
    if add_reserved_ips or remove_reserved_ips:
316
        net = back_network.network
317
        pool = net.get_pool()
318
        if add_reserved_ips:
319
            for ip in add_reserved_ips:
320
                pool.reserve(ip, external=True)
321
        if remove_reserved_ips:
322
            for ip in remove_reserved_ips:
323
                pool.put(ip, external=True)
324
        pool.save()
325

    
326
    if status == 'success':
327
        back_network.backendtime = etime
328
    back_network.save()
329

    
330

    
331
@transaction.commit_on_success
332
def process_create_progress(vm, etime, progress):
333

    
334
    percentage = int(progress)
335

    
336
    # The percentage may exceed 100%, due to the way
337
    # snf-image:copy-progress tracks bytes read by image handling processes
338
    percentage = 100 if percentage > 100 else percentage
339
    if percentage < 0:
340
        raise ValueError("Percentage cannot be negative")
341

    
342
    # FIXME: log a warning here, see #1033
343
#   if last_update > percentage:
344
#       raise ValueError("Build percentage should increase monotonically " \
345
#                        "(old = %d, new = %d)" % (last_update, percentage))
346

    
347
    # This assumes that no message of type 'ganeti-create-progress' is going to
348
    # arrive once OP_INSTANCE_CREATE has succeeded for a Ganeti instance and
349
    # the instance is STARTED.  What if the two messages are processed by two
350
    # separate dispatcher threads, and the 'ganeti-op-status' message for
351
    # successful creation gets processed before the 'ganeti-create-progress'
352
    # message? [vkoukis]
353
    #
354
    #if not vm.operstate == 'BUILD':
355
    #    raise VirtualMachine.IllegalState("VM is not in building state")
356

    
357
    vm.buildpercentage = percentage
358
    vm.backendtime = etime
359
    vm.save()
360

    
361

    
362
@transaction.commit_on_success
363
def create_instance_diagnostic(vm, message, source, level="DEBUG", etime=None,
364
                               details=None):
365
    """
366
    Create virtual machine instance diagnostic entry.
367

368
    :param vm: VirtualMachine instance to create diagnostic for.
369
    :param message: Diagnostic message.
370
    :param source: Diagnostic source identifier (e.g. image-helper).
371
    :param level: Diagnostic level (`DEBUG`, `INFO`, `WARNING`, `ERROR`).
372
    :param etime: The time the message occured (if available).
373
    :param details: Additional details or debug information.
374
    """
375
    VirtualMachineDiagnostic.objects.create_for_vm(vm, level, source=source,
376
                                                   source_date=etime,
377
                                                   message=message,
378
                                                   details=details)
379

    
380

    
381
def create_instance(vm, public_nic, flavor, image):
382
    """`image` is a dictionary which should contain the keys:
383
            'backend_id', 'format' and 'metadata'
384

385
        metadata value should be a dictionary.
386
    """
387

    
388
    # Handle arguments to CreateInstance() as a dictionary,
389
    # initialize it based on a deployment-specific value.
390
    # This enables the administrator to override deployment-specific
391
    # arguments, such as the disk template to use, name of os provider
392
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
393
    #
394
    kw = vm.backend.get_create_params()
395
    kw['mode'] = 'create'
396
    kw['name'] = vm.backend_vm_id
397
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
398

    
399
    kw['disk_template'] = flavor.disk_template
400
    kw['disks'] = [{"size": flavor.disk * 1024}]
401
    provider = flavor.disk_provider
402
    if provider:
403
        kw['disks'][0]['provider'] = provider
404
        kw['disks'][0]['origin'] = flavor.disk_origin
405

    
406
    kw['nics'] = [public_nic]
407
    if vm.backend.use_hotplug():
408
        kw['hotplug'] = True
409
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
410
    # kw['os'] = settings.GANETI_OS_PROVIDER
411
    kw['ip_check'] = False
412
    kw['name_check'] = False
413

    
414
    # Do not specific a node explicitly, have
415
    # Ganeti use an iallocator instead
416
    #kw['pnode'] = rapi.GetNodes()[0]
417

    
418
    kw['dry_run'] = settings.TEST
419

    
420
    kw['beparams'] = {
421
        'auto_balance': True,
422
        'vcpus': flavor.cpu,
423
        'memory': flavor.ram}
424

    
425
    kw['osparams'] = {
426
        'config_url': vm.config_url,
427
        # Store image id and format to Ganeti
428
        'img_id': image['backend_id'],
429
        'img_format': image['format']}
430

    
431
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
432
    # kw['hvparams'] = dict(serial_console=False)
433

    
434
    log.debug("Creating instance %s", utils.hide_pass(kw))
435
    with pooled_rapi_client(vm) as client:
436
        return client.CreateInstance(**kw)
437

    
438

    
439
def delete_instance(vm):
440
    with pooled_rapi_client(vm) as client:
441
        return client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
442

    
443

    
444
def reboot_instance(vm, reboot_type):
445
    assert reboot_type in ('soft', 'hard')
446
    with pooled_rapi_client(vm) as client:
447
        return client.RebootInstance(vm.backend_vm_id, reboot_type,
448
                                     dry_run=settings.TEST)
449

    
450

    
451
def startup_instance(vm):
452
    with pooled_rapi_client(vm) as client:
453
        return client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
454

    
455

    
456
def shutdown_instance(vm):
457
    with pooled_rapi_client(vm) as client:
458
        return client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
459

    
460

    
461
def get_instance_console(vm):
462
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
463
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
464
    # useless (see #783).
465
    #
466
    # Until this is fixed on the Ganeti side, construct a console info reply
467
    # directly.
468
    #
469
    # WARNING: This assumes that VNC runs on port network_port on
470
    #          the instance's primary node, and is probably
471
    #          hypervisor-specific.
472
    #
473
    log.debug("Getting console for vm %s", vm)
474

    
475
    console = {}
476
    console['kind'] = 'vnc'
477

    
478
    with pooled_rapi_client(vm) as client:
479
        i = client.GetInstance(vm.backend_vm_id)
480

    
481
    if vm.backend.hypervisor == "kvm" and i['hvparams']['serial_console']:
482
        raise Exception("hv parameter serial_console cannot be true")
483
    console['host'] = i['pnode']
484
    console['port'] = i['network_port']
485

    
486
    return console
487

    
488

    
489
def get_instance_info(vm):
490
    with pooled_rapi_client(vm) as client:
491
        return client.GetInstance(vm.backend_vm_id)
492

    
493

    
494
def vm_exists_in_backend(vm):
495
    try:
496
        get_instance_info(vm)
497
        return True
498
    except GanetiApiError as e:
499
        if e.code == 404:
500
            return False
501
        raise e
502

    
503

    
504
def get_network_info(backend_network):
505
    with pooled_rapi_client(backend_network) as client:
506
        return client.GetNetwork(backend_network.network.backend_id)
507

    
508

    
509
def network_exists_in_backend(backend_network):
510
    try:
511
        get_network_info(backend_network)
512
        return True
513
    except GanetiApiError as e:
514
        if e.code == 404:
515
            return False
516

    
517

    
518
def create_network(network, backend, connect=True):
519
    """Create a network in a Ganeti backend"""
520
    log.debug("Creating network %s in backend %s", network, backend)
521

    
522
    job_id = _create_network(network, backend)
523

    
524
    if connect:
525
        job_ids = connect_network(network, backend, depends=[job_id])
526
        return job_ids
527
    else:
528
        return [job_id]
529

    
530

    
531
def _create_network(network, backend):
532
    """Create a network."""
533

    
534
    network_type = network.public and 'public' or 'private'
535

    
536
    tags = network.backend_tag
537
    if network.dhcp:
538
        tags.append('nfdhcpd')
539

    
540
    if network.public:
541
        conflicts_check = True
542
    else:
543
        conflicts_check = False
544

    
545
    try:
546
        bn = BackendNetwork.objects.get(network=network, backend=backend)
547
        mac_prefix = bn.mac_prefix
548
    except BackendNetwork.DoesNotExist:
549
        raise Exception("BackendNetwork for network '%s' in backend '%s'"
550
                        " does not exist" % (network.id, backend.id))
551

    
552
    with pooled_rapi_client(backend) as client:
553
        return client.CreateNetwork(network_name=network.backend_id,
554
                                    network=network.subnet,
555
                                    network6=network.subnet6,
556
                                    gateway=network.gateway,
557
                                    gateway6=network.gateway6,
558
                                    network_type=network_type,
559
                                    mac_prefix=mac_prefix,
560
                                    conflicts_check=conflicts_check,
561
                                    tags=tags)
562

    
563

    
564
def connect_network(network, backend, depends=[], group=None):
565
    """Connect a network to nodegroups."""
566
    log.debug("Connecting network %s to backend %s", network, backend)
567

    
568
    if network.public:
569
        conflicts_check = True
570
    else:
571
        conflicts_check = False
572

    
573
    depends = [[job, ["success", "error", "canceled"]] for job in depends]
574
    with pooled_rapi_client(backend) as client:
575
        groups = [group] if group is not None else client.GetGroups()
576
        job_ids = []
577
        for group in groups:
578
            job_id = client.ConnectNetwork(network.backend_id, group,
579
                                           network.mode, network.link,
580
                                           conflicts_check,
581
                                           depends=depends)
582
            job_ids.append(job_id)
583
    return job_ids
584

    
585

    
586
def delete_network(network, backend, disconnect=True):
587
    log.debug("Deleting network %s from backend %s", network, backend)
588

    
589
    depends = []
590
    if disconnect:
591
        depends = disconnect_network(network, backend)
592
    _delete_network(network, backend, depends=depends)
593

    
594

    
595
def _delete_network(network, backend, depends=[]):
596
    depends = [[job, ["success", "error", "canceled"]] for job in depends]
597
    with pooled_rapi_client(backend) as client:
598
        return client.DeleteNetwork(network.backend_id, depends)
599

    
600

    
601
def disconnect_network(network, backend, group=None):
602
    log.debug("Disconnecting network %s to backend %s", network, backend)
603

    
604
    with pooled_rapi_client(backend) as client:
605
        groups = [group] if group is not None else client.GetGroups()
606
        job_ids = []
607
        for group in groups:
608
            job_id = client.DisconnectNetwork(network.backend_id, group)
609
            job_ids.append(job_id)
610
    return job_ids
611

    
612

    
613
def connect_to_network(vm, network, address=None):
614
    backend = vm.backend
615
    network = Network.objects.select_for_update().get(id=network.id)
616
    bnet, created = BackendNetwork.objects.get_or_create(backend=backend,
617
                                                         network=network)
618
    depend_jobs = []
619
    if bnet.operstate != "ACTIVE":
620
        depend_jobs = create_network(network, backend, connect=True)
621

    
622
    depends = [[job, ["success", "error", "canceled"]] for job in depend_jobs]
623

    
624
    nic = {'ip': address, 'network': network.backend_id}
625

    
626
    log.debug("Connecting vm %s to network %s(%s)", vm, network, address)
627

    
628
    with pooled_rapi_client(vm) as client:
629
        return client.ModifyInstance(vm.backend_vm_id, nics=[('add',  nic)],
630
                                     hotplug=vm.backend.use_hotplug(),
631
                                     depends=depends,
632
                                     dry_run=settings.TEST)
633

    
634

    
635
def disconnect_from_network(vm, nic):
636
    op = [('remove', nic.index, {})]
637

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

    
640
    with pooled_rapi_client(vm) as client:
641
        return client.ModifyInstance(vm.backend_vm_id, nics=op,
642
                                     hotplug=vm.backend.use_hotplug(),
643
                                     dry_run=settings.TEST)
644

    
645

    
646
def set_firewall_profile(vm, profile):
647
    try:
648
        tag = _firewall_tags[profile]
649
    except KeyError:
650
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
651

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

    
654
    with pooled_rapi_client(vm) as client:
655
        # Delete all firewall tags
656
        for t in _firewall_tags.values():
657
            client.DeleteInstanceTags(vm.backend_vm_id, [t],
658
                                      dry_run=settings.TEST)
659

    
660
        client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
661

    
662
        # XXX NOP ModifyInstance call to force process_net_status to run
663
        # on the dispatcher
664
        os_name = settings.GANETI_CREATEINSTANCE_KWARGS['os']
665
        client.ModifyInstance(vm.backend_vm_id,
666
                              os_name=os_name)
667

    
668

    
669
def get_ganeti_instances(backend=None, bulk=False):
670
    instances = []
671
    for backend in get_backends(backend):
672
        with pooled_rapi_client(backend) as client:
673
            instances.append(client.GetInstances(bulk=bulk))
674

    
675
    return reduce(list.__add__, instances, [])
676

    
677

    
678
def get_ganeti_nodes(backend=None, bulk=False):
679
    nodes = []
680
    for backend in get_backends(backend):
681
        with pooled_rapi_client(backend) as client:
682
            nodes.append(client.GetNodes(bulk=bulk))
683

    
684
    return reduce(list.__add__, nodes, [])
685

    
686

    
687
def get_ganeti_jobs(backend=None, bulk=False):
688
    jobs = []
689
    for backend in get_backends(backend):
690
        with pooled_rapi_client(backend) as client:
691
            jobs.append(client.GetJobs(bulk=bulk))
692
    return reduce(list.__add__, jobs, [])
693

    
694
##
695
##
696
##
697

    
698

    
699
def get_backends(backend=None):
700
    if backend:
701
        if backend.offline:
702
            return []
703
        return [backend]
704
    return Backend.objects.filter(offline=False)
705

    
706

    
707
def get_physical_resources(backend):
708
    """ Get the physical resources of a backend.
709

710
    Get the resources of a backend as reported by the backend (not the db).
711

712
    """
713
    nodes = get_ganeti_nodes(backend, bulk=True)
714
    attr = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal']
715
    res = {}
716
    for a in attr:
717
        res[a] = 0
718
    for n in nodes:
719
        # Filter out drained, offline and not vm_capable nodes since they will
720
        # not take part in the vm allocation process
721
        can_host_vms = n['vm_capable'] and not (n['drained'] or n['offline'])
722
        if can_host_vms and n['cnodes']:
723
            for a in attr:
724
                res[a] += int(n[a])
725
    return res
726

    
727

    
728
def update_resources(backend, resources=None):
729
    """ Update the state of the backend resources in db.
730

731
    """
732

    
733
    if not resources:
734
        resources = get_physical_resources(backend)
735

    
736
    backend.mfree = resources['mfree']
737
    backend.mtotal = resources['mtotal']
738
    backend.dfree = resources['dfree']
739
    backend.dtotal = resources['dtotal']
740
    backend.pinst_cnt = resources['pinst_cnt']
741
    backend.ctotal = resources['ctotal']
742
    backend.updated = datetime.now()
743
    backend.save()
744

    
745

    
746
def get_memory_from_instances(backend):
747
    """ Get the memory that is used from instances.
748

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

752
    """
753
    with pooled_rapi_client(backend) as client:
754
        instances = client.GetInstances(bulk=True)
755
    mem = 0
756
    for i in instances:
757
        mem += i['oper_ram']
758
    return mem
759

    
760
##
761
## Synchronized operations for reconciliation
762
##
763

    
764

    
765
def create_network_synced(network, backend):
766
    result = _create_network_synced(network, backend)
767
    if result[0] != 'success':
768
        return result
769
    result = connect_network_synced(network, backend)
770
    return result
771

    
772

    
773
def _create_network_synced(network, backend):
774
    with pooled_rapi_client(backend) as client:
775
        job = _create_network(network, backend)
776
        result = wait_for_job(client, job)
777
    return result
778

    
779

    
780
def connect_network_synced(network, backend):
781
    with pooled_rapi_client(backend) as client:
782
        for group in client.GetGroups():
783
            job = client.ConnectNetwork(network.backend_id, group,
784
                                        network.mode, network.link)
785
            result = wait_for_job(client, job)
786
            if result[0] != 'success':
787
                return result
788

    
789
    return result
790

    
791

    
792
def wait_for_job(client, jobid):
793
    result = client.WaitForJobChange(jobid, ['status', 'opresult'], None, None)
794
    status = result['job_info'][0]
795
    while status not in ['success', 'error', 'cancel']:
796
        result = client.WaitForJobChange(jobid, ['status', 'opresult'],
797
                                         [result], None)
798
        status = result['job_info'][0]
799

    
800
    if status == 'success':
801
        return (status, None)
802
    else:
803
        error = result['job_info'][1]
804
        return (status, error)