Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.8 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, JOB_STATUS_FINALIZED
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
    # Update backendtime only for jobs that have been successfully completed,
78
    # since only these jobs update the state of the VM. Else a "race condition"
79
    # may occur when a successful job (e.g. OP_INSTANCE_REMOVE) completes
80
    # before an error job and messages arrive in reversed order.
81
    if status == 'success':
82
        vm.backendtime = etime
83

    
84
    # Notifications of success change the operating state
85
    state_for_success = VirtualMachine.OPER_STATE_FROM_OPCODE.get(opcode, None)
86
    if status == 'success' and state_for_success is not None:
87
        vm.operstate = state_for_success
88

    
89
    # Update the NICs of the VM
90
    if status == "success" and nics is not None:
91
        _process_net_status(vm, etime, nics)
92

    
93
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
94
    if opcode == 'OP_INSTANCE_CREATE' and status in ('canceled', 'error'):
95
        vm.operstate = 'ERROR'
96
        vm.backendtime = etime
97
    elif opcode == 'OP_INSTANCE_REMOVE':
98
        # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR,
99
        # when no instance exists at the Ganeti backend.
100
        if status == "success" or (status == "error" and
101
                                   not vm_exists_in_backend(vm)):
102
            _process_net_status(vm, etime, nics=[])
103
            vm.operstate = state_for_success
104
            vm.backendtime = etime
105
            if not vm.deleted:
106
                vm.deleted = True
107
                # Issue and accept commission to Quotaholder
108
                quotas.issue_and_accept_commission(vm, delete=True)
109
                # the above has already saved the object and committed;
110
                # a second save would override others' changes, since the
111
                # object is now unlocked
112
                return
113

    
114
    vm.save()
115

    
116

    
117
@transaction.commit_on_success
118
def process_net_status(vm, etime, nics):
119
    """Wrap _process_net_status inside transaction."""
120
    _process_net_status(vm, etime, nics)
121

    
122

    
123
def _process_net_status(vm, etime, nics):
124
    """Process a net status notification from the backend
125

126
    Process an incoming message from the Ganeti backend,
127
    detailing the NIC configuration of a VM instance.
128

129
    Update the state of the VM in the DB accordingly.
130
    """
131

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

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

    
141
    release_instance_nics(vm)
142

    
143
    for nic in ganeti_nics:
144
        ipv4 = nic.get('ipv4', '')
145
        net = nic['network']
146
        if ipv4:
147
            net.reserve_address(ipv4)
148

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

    
155
    vm.backendtime = etime
156
    vm.save()
157

    
158

    
159
def process_ganeti_nics(ganeti_nics):
160
    """Process NIC dict from ganeti hooks."""
161
    new_nics = []
162
    for i, new_nic in enumerate(ganeti_nics):
163
        network_name = new_nic.get('network', '')
164
        network_id = utils.id_from_network_name(network_name)
165
        net = Network.objects.get(id=network_id)
166

    
167
        nic_name = new_nic.get("name", None)
168
        nic_id = None
169
        if nic_name is not None:
170
            nic_id = utils.id_from_nic_name(nic_name)
171

    
172
        # Get the new nic info
173
        mac = new_nic.get('mac', '')
174
        ipv4 = new_nic.get('ip', '')
175
        if net.subnet6:
176
            ipv6 = mac2eui64(mac, net.subnet6)
177
        else:
178
            ipv6 = ''
179

    
180
        firewall = new_nic.get('firewall', '')
181
        firewall_profile = _reverse_tags.get(firewall, '')
182
        if not firewall_profile and net.public:
183
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
184

    
185
        nic = {
186
            'index': i,
187
            'network': net,
188
            'mac': mac,
189
            'ipv4': ipv4,
190
            'ipv6': ipv6,
191
            'firewall_profile': firewall_profile,
192
            'state': 'ACTIVE',
193
            'id': nic_id}
194

    
195
        new_nics.append(nic)
196
    return new_nics
197

    
198

    
199
def nics_changed(old_nics, new_nics):
200
    """Return True if NICs have changed in any way."""
201
    if len(old_nics) != len(new_nics):
202
        return True
203
    fields = ["ipv4", "ipv6", "mac", "firewall_profile", "index", "network"]
204
    for old_nic, new_nic in zip(old_nics, new_nics):
205
        for field in fields:
206
            if getattr(old_nic, field) != new_nic[field]:
207
                return True
208
    return False
209

    
210

    
211
def release_instance_nics(vm):
212
    for nic in vm.nics.all():
213
        net = nic.network
214
        if nic.ipv4:
215
            net.release_address(nic.ipv4)
216
        nic.delete()
217
        net.save()
218

    
219

    
220
@transaction.commit_on_success
221
def process_network_status(back_network, etime, jobid, opcode, status, logmsg):
222
    if status not in [x[0] for x in BACKEND_STATUSES]:
223
        raise Network.InvalidBackendMsgError(opcode, status)
224

    
225
    back_network.backendjobid = jobid
226
    back_network.backendjobstatus = status
227
    back_network.backendopcode = opcode
228
    back_network.backendlogmsg = logmsg
229

    
230
    network = back_network.network
231

    
232
    # Notifications of success change the operating state
233
    state_for_success = BackendNetwork.OPER_STATE_FROM_OPCODE.get(opcode, None)
234
    if status == 'success' and state_for_success is not None:
235
        back_network.operstate = state_for_success
236

    
237
    if status in ('canceled', 'error') and opcode == 'OP_NETWORK_ADD':
238
        back_network.operstate = 'ERROR'
239
        back_network.backendtime = etime
240

    
241
    if opcode == 'OP_NETWORK_REMOVE':
242
        network_is_deleted = (status == "success")
243
        if network_is_deleted or (status == "error" and not
244
                                  network_exists_in_backend(back_network)):
245
            back_network.operstate = state_for_success
246
            back_network.deleted = True
247
            back_network.backendtime = etime
248

    
249
    if status == 'success':
250
        back_network.backendtime = etime
251
    back_network.save()
252
    # Also you must update the state of the Network!!
253
    update_network_state(network)
254

    
255

    
256
def update_network_state(network):
257
    """Update the state of a Network based on BackendNetwork states.
258

259
    Update the state of a Network based on the operstate of the networks in the
260
    backends that network exists.
261

262
    The state of the network is:
263
    * ACTIVE: If it is 'ACTIVE' in at least one backend.
264
    * DELETED: If it is is 'DELETED' in all backends that have been created.
265

266
    This function also releases the resources (MAC prefix or Bridge) and the
267
    quotas for the network.
268

269
    """
270
    if network.deleted:
271
        # Network has already been deleted. Just assert that state is also
272
        # DELETED
273
        if not network.state == "DELETED":
274
            network.state = "DELETED"
275
            network.save()
276
        return
277

    
278
    backend_states = [s.operstate for s in network.backend_networks.all()]
279
    if not backend_states and network.action != "DESTROY":
280
        if network.state != "ACTIVE":
281
            network.state = "ACTIVE"
282
            network.save()
283
            return
284

    
285
    # Network is deleted when all BackendNetworks go to "DELETED" operstate
286
    deleted = reduce(lambda x, y: x == y and "DELETED", backend_states,
287
                     "DELETED")
288

    
289
    # Release the resources on the deletion of the Network
290
    if deleted:
291
        log.info("Network %r deleted. Releasing link %r mac_prefix %r",
292
                 network.id, network.mac_prefix, network.link)
293
        network.deleted = True
294
        network.state = "DELETED"
295
        if network.mac_prefix:
296
            if network.FLAVORS[network.flavor]["mac_prefix"] == "pool":
297
                release_resource(res_type="mac_prefix",
298
                                 value=network.mac_prefix)
299
        if network.link:
300
            if network.FLAVORS[network.flavor]["link"] == "pool":
301
                release_resource(res_type="bridge", value=network.link)
302

    
303
        # Issue commission
304
        if network.userid:
305
            quotas.issue_and_accept_commission(network, delete=True)
306
            # the above has already saved the object and committed;
307
            # a second save would override others' changes, since the
308
            # object is now unlocked
309
            return
310
        elif not network.public:
311
            log.warning("Network %s does not have an owner!", network.id)
312
    network.save()
313

    
314

    
315
@transaction.commit_on_success
316
def process_network_modify(back_network, etime, jobid, opcode, status,
317
                           add_reserved_ips, remove_reserved_ips):
318
    assert (opcode == "OP_NETWORK_SET_PARAMS")
319
    if status not in [x[0] for x in BACKEND_STATUSES]:
320
        raise Network.InvalidBackendMsgError(opcode, status)
321

    
322
    back_network.backendjobid = jobid
323
    back_network.backendjobstatus = status
324
    back_network.opcode = opcode
325

    
326
    if add_reserved_ips or remove_reserved_ips:
327
        net = back_network.network
328
        pool = net.get_pool()
329
        if add_reserved_ips:
330
            for ip in add_reserved_ips:
331
                pool.reserve(ip, external=True)
332
        if remove_reserved_ips:
333
            for ip in remove_reserved_ips:
334
                pool.put(ip, external=True)
335
        pool.save()
336

    
337
    if status == 'success':
338
        back_network.backendtime = etime
339
    back_network.save()
340

    
341

    
342
@transaction.commit_on_success
343
def process_create_progress(vm, etime, progress):
344

    
345
    percentage = int(progress)
346

    
347
    # The percentage may exceed 100%, due to the way
348
    # snf-image:copy-progress tracks bytes read by image handling processes
349
    percentage = 100 if percentage > 100 else percentage
350
    if percentage < 0:
351
        raise ValueError("Percentage cannot be negative")
352

    
353
    # FIXME: log a warning here, see #1033
354
#   if last_update > percentage:
355
#       raise ValueError("Build percentage should increase monotonically " \
356
#                        "(old = %d, new = %d)" % (last_update, percentage))
357

    
358
    # This assumes that no message of type 'ganeti-create-progress' is going to
359
    # arrive once OP_INSTANCE_CREATE has succeeded for a Ganeti instance and
360
    # the instance is STARTED.  What if the two messages are processed by two
361
    # separate dispatcher threads, and the 'ganeti-op-status' message for
362
    # successful creation gets processed before the 'ganeti-create-progress'
363
    # message? [vkoukis]
364
    #
365
    #if not vm.operstate == 'BUILD':
366
    #    raise VirtualMachine.IllegalState("VM is not in building state")
367

    
368
    vm.buildpercentage = percentage
369
    vm.backendtime = etime
370
    vm.save()
371

    
372

    
373
@transaction.commit_on_success
374
def create_instance_diagnostic(vm, message, source, level="DEBUG", etime=None,
375
                               details=None):
376
    """
377
    Create virtual machine instance diagnostic entry.
378

379
    :param vm: VirtualMachine instance to create diagnostic for.
380
    :param message: Diagnostic message.
381
    :param source: Diagnostic source identifier (e.g. image-helper).
382
    :param level: Diagnostic level (`DEBUG`, `INFO`, `WARNING`, `ERROR`).
383
    :param etime: The time the message occured (if available).
384
    :param details: Additional details or debug information.
385
    """
386
    VirtualMachineDiagnostic.objects.create_for_vm(vm, level, source=source,
387
                                                   source_date=etime,
388
                                                   message=message,
389
                                                   details=details)
390

    
391

    
392
def create_instance(vm, public_nic, flavor, image):
393
    """`image` is a dictionary which should contain the keys:
394
            'backend_id', 'format' and 'metadata'
395

396
        metadata value should be a dictionary.
397
    """
398

    
399
    # Handle arguments to CreateInstance() as a dictionary,
400
    # initialize it based on a deployment-specific value.
401
    # This enables the administrator to override deployment-specific
402
    # arguments, such as the disk template to use, name of os provider
403
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
404
    #
405
    kw = vm.backend.get_create_params()
406
    kw['mode'] = 'create'
407
    kw['name'] = vm.backend_vm_id
408
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
409

    
410
    kw['disk_template'] = flavor.disk_template
411
    kw['disks'] = [{"size": flavor.disk * 1024}]
412
    provider = flavor.disk_provider
413
    if provider:
414
        kw['disks'][0]['provider'] = provider
415
        kw['disks'][0]['origin'] = flavor.disk_origin
416

    
417
    kw['nics'] = [{"name": public_nic.backend_uuid,
418
                  "network": public_nic.network.backend_id,
419
                  "ip": public_nic.ipv4}]
420
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
421
    # kw['os'] = settings.GANETI_OS_PROVIDER
422
    kw['ip_check'] = False
423
    kw['name_check'] = False
424

    
425
    # Do not specific a node explicitly, have
426
    # Ganeti use an iallocator instead
427
    #kw['pnode'] = rapi.GetNodes()[0]
428

    
429
    kw['dry_run'] = settings.TEST
430

    
431
    kw['beparams'] = {
432
        'auto_balance': True,
433
        'vcpus': flavor.cpu,
434
        'memory': flavor.ram}
435

    
436
    kw['osparams'] = {
437
        'config_url': vm.config_url,
438
        # Store image id and format to Ganeti
439
        'img_id': image['backend_id'],
440
        'img_format': image['format']}
441

    
442
    # Use opportunistic locking
443
    kw['opportunistic_locking'] = settings.GANETI_USE_OPPORTUNISTIC_LOCKING
444

    
445
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
446
    # kw['hvparams'] = dict(serial_console=False)
447

    
448
    log.debug("Creating instance %s", utils.hide_pass(kw))
449
    with pooled_rapi_client(vm) as client:
450
        return client.CreateInstance(**kw)
451

    
452

    
453
def delete_instance(vm):
454
    with pooled_rapi_client(vm) as client:
455
        return client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
456

    
457

    
458
def reboot_instance(vm, reboot_type):
459
    assert reboot_type in ('soft', 'hard')
460
    with pooled_rapi_client(vm) as client:
461
        return client.RebootInstance(vm.backend_vm_id, reboot_type,
462
                                     dry_run=settings.TEST)
463

    
464

    
465
def startup_instance(vm):
466
    with pooled_rapi_client(vm) as client:
467
        return client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
468

    
469

    
470
def shutdown_instance(vm):
471
    with pooled_rapi_client(vm) as client:
472
        return client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
473

    
474

    
475
def get_instance_console(vm):
476
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
477
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
478
    # useless (see #783).
479
    #
480
    # Until this is fixed on the Ganeti side, construct a console info reply
481
    # directly.
482
    #
483
    # WARNING: This assumes that VNC runs on port network_port on
484
    #          the instance's primary node, and is probably
485
    #          hypervisor-specific.
486
    #
487
    log.debug("Getting console for vm %s", vm)
488

    
489
    console = {}
490
    console['kind'] = 'vnc'
491

    
492
    with pooled_rapi_client(vm) as client:
493
        i = client.GetInstance(vm.backend_vm_id)
494

    
495
    if vm.backend.hypervisor == "kvm" and i['hvparams']['serial_console']:
496
        raise Exception("hv parameter serial_console cannot be true")
497
    console['host'] = i['pnode']
498
    console['port'] = i['network_port']
499

    
500
    return console
501

    
502

    
503
def get_instance_info(vm):
504
    with pooled_rapi_client(vm) as client:
505
        return client.GetInstance(vm.backend_vm_id)
506

    
507

    
508
def vm_exists_in_backend(vm):
509
    try:
510
        get_instance_info(vm)
511
        return True
512
    except GanetiApiError as e:
513
        if e.code == 404:
514
            return False
515
        raise e
516

    
517

    
518
def get_network_info(backend_network):
519
    with pooled_rapi_client(backend_network) as client:
520
        return client.GetNetwork(backend_network.network.backend_id)
521

    
522

    
523
def network_exists_in_backend(backend_network):
524
    try:
525
        get_network_info(backend_network)
526
        return True
527
    except GanetiApiError as e:
528
        if e.code == 404:
529
            return False
530

    
531

    
532
def job_is_still_running(vm):
533
    with pooled_rapi_client(vm) as c:
534
        try:
535
            job_info = c.GetJobStatus(vm.backendjobid)
536
            return not (job_info["status"] in JOB_STATUS_FINALIZED)
537
        except GanetiApiError:
538
            return False
539

    
540

    
541
def create_network(network, backend, connect=True):
542
    """Create a network in a Ganeti backend"""
543
    log.debug("Creating network %s in backend %s", network, backend)
544

    
545
    job_id = _create_network(network, backend)
546

    
547
    if connect:
548
        job_ids = connect_network(network, backend, depends=[job_id])
549
        return job_ids
550
    else:
551
        return [job_id]
552

    
553

    
554
def _create_network(network, backend):
555
    """Create a network."""
556

    
557
    tags = network.backend_tag
558
    if network.dhcp:
559
        tags.append('nfdhcpd')
560

    
561
    if network.public:
562
        conflicts_check = True
563
        tags.append('public')
564
    else:
565
        conflicts_check = False
566
        tags.append('private')
567

    
568
    try:
569
        bn = BackendNetwork.objects.get(network=network, backend=backend)
570
        mac_prefix = bn.mac_prefix
571
    except BackendNetwork.DoesNotExist:
572
        raise Exception("BackendNetwork for network '%s' in backend '%s'"
573
                        " does not exist" % (network.id, backend.id))
574

    
575
    with pooled_rapi_client(backend) as client:
576
        return client.CreateNetwork(network_name=network.backend_id,
577
                                    network=network.subnet,
578
                                    network6=network.subnet6,
579
                                    gateway=network.gateway,
580
                                    gateway6=network.gateway6,
581
                                    mac_prefix=mac_prefix,
582
                                    conflicts_check=conflicts_check,
583
                                    tags=tags)
584

    
585

    
586
def connect_network(network, backend, depends=[], group=None):
587
    """Connect a network to nodegroups."""
588
    log.debug("Connecting network %s to backend %s", network, backend)
589

    
590
    if network.public:
591
        conflicts_check = True
592
    else:
593
        conflicts_check = False
594

    
595
    depends = [[job, ["success", "error", "canceled"]] for job in depends]
596
    with pooled_rapi_client(backend) as client:
597
        groups = [group] if group is not None else client.GetGroups()
598
        job_ids = []
599
        for group in groups:
600
            job_id = client.ConnectNetwork(network.backend_id, group,
601
                                           network.mode, network.link,
602
                                           conflicts_check,
603
                                           depends=depends)
604
            job_ids.append(job_id)
605
    return job_ids
606

    
607

    
608
def delete_network(network, backend, disconnect=True):
609
    log.debug("Deleting network %s from backend %s", network, backend)
610

    
611
    depends = []
612
    if disconnect:
613
        depends = disconnect_network(network, backend)
614
    _delete_network(network, backend, depends=depends)
615

    
616

    
617
def _delete_network(network, backend, depends=[]):
618
    depends = [[job, ["success", "error", "canceled"]] for job in depends]
619
    with pooled_rapi_client(backend) as client:
620
        return client.DeleteNetwork(network.backend_id, depends)
621

    
622

    
623
def disconnect_network(network, backend, group=None):
624
    log.debug("Disconnecting network %s to backend %s", network, backend)
625

    
626
    with pooled_rapi_client(backend) as client:
627
        groups = [group] if group is not None else client.GetGroups()
628
        job_ids = []
629
        for group in groups:
630
            job_id = client.DisconnectNetwork(network.backend_id, group)
631
            job_ids.append(job_id)
632
    return job_ids
633

    
634

    
635
def connect_to_network(vm, nic):
636
    backend = vm.backend
637
    network = nic.network
638
    address = nic.ipv4
639
    network = Network.objects.select_for_update().get(id=network.id)
640
    bnet, created = BackendNetwork.objects.get_or_create(backend=backend,
641
                                                         network=network)
642
    depend_jobs = []
643
    if bnet.operstate != "ACTIVE":
644
        depend_jobs = create_network(network, backend, connect=True)
645

    
646
    depends = [[job, ["success", "error", "canceled"]] for job in depend_jobs]
647

    
648
    nic = {'ip': address,
649
           'network': network.backend_id,
650
           'name': nic.backend_uuid}
651

    
652
    log.debug("Connecting vm %s to network %s(%s)", vm, network, address)
653

    
654
    with pooled_rapi_client(vm) as client:
655
        return client.ModifyInstance(vm.backend_vm_id,
656
                                     nics=[('add',  "-1", nic)],
657
                                     hotplug=vm.backend.use_hotplug(),
658
                                     depends=depends,
659
                                     dry_run=settings.TEST)
660

    
661

    
662
def disconnect_from_network(vm, nic):
663
    op = [('remove', str(nic.index), {})]
664

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

    
667
    with pooled_rapi_client(vm) as client:
668
        return client.ModifyInstance(vm.backend_vm_id, nics=op,
669
                                     hotplug=vm.backend.use_hotplug(),
670
                                     dry_run=settings.TEST)
671

    
672

    
673
def set_firewall_profile(vm, profile):
674
    try:
675
        tag = _firewall_tags[profile]
676
    except KeyError:
677
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
678

    
679
    try:
680
        public_nic = vm.nics.filter(network__public=True)[0]
681
    except IndexError:
682
        public_nic = None
683

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

    
686
    with pooled_rapi_client(vm) as client:
687
        # Delete all firewall tags
688
        for t in _firewall_tags.values():
689
            client.DeleteInstanceTags(vm.backend_vm_id, [t],
690
                                      dry_run=settings.TEST)
691
            if public_nic is not None:
692
                tag_with_name = t.replace("0", public_nic.backend_uuid)
693
                client.DeleteInstanceTags(vm.backend_vm_id, [tag_with_name],
694
                                          dry_run=settings.TEST)
695

    
696
        client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
697
        if public_nic is not None:
698
            tag_with_name = tag.replace("0", public_nic.backend_uuid)
699
            client.AddInstanceTags(vm.backend_vm_id, [tag_with_name],
700
                                   dry_run=settings.TEST)
701

    
702
        # XXX NOP ModifyInstance call to force process_net_status to run
703
        # on the dispatcher
704
        os_name = settings.GANETI_CREATEINSTANCE_KWARGS['os']
705
        client.ModifyInstance(vm.backend_vm_id,
706
                              os_name=os_name)
707

    
708

    
709
def get_ganeti_instances(backend=None, bulk=False):
710
    instances = []
711
    for backend in get_backends(backend):
712
        with pooled_rapi_client(backend) as client:
713
            instances.append(client.GetInstances(bulk=bulk))
714

    
715
    return reduce(list.__add__, instances, [])
716

    
717

    
718
def get_ganeti_nodes(backend=None, bulk=False):
719
    nodes = []
720
    for backend in get_backends(backend):
721
        with pooled_rapi_client(backend) as client:
722
            nodes.append(client.GetNodes(bulk=bulk))
723

    
724
    return reduce(list.__add__, nodes, [])
725

    
726

    
727
def get_ganeti_jobs(backend=None, bulk=False):
728
    jobs = []
729
    for backend in get_backends(backend):
730
        with pooled_rapi_client(backend) as client:
731
            jobs.append(client.GetJobs(bulk=bulk))
732
    return reduce(list.__add__, jobs, [])
733

    
734
##
735
##
736
##
737

    
738

    
739
def get_backends(backend=None):
740
    if backend:
741
        if backend.offline:
742
            return []
743
        return [backend]
744
    return Backend.objects.filter(offline=False)
745

    
746

    
747
def get_physical_resources(backend):
748
    """ Get the physical resources of a backend.
749

750
    Get the resources of a backend as reported by the backend (not the db).
751

752
    """
753
    nodes = get_ganeti_nodes(backend, bulk=True)
754
    attr = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal']
755
    res = {}
756
    for a in attr:
757
        res[a] = 0
758
    for n in nodes:
759
        # Filter out drained, offline and not vm_capable nodes since they will
760
        # not take part in the vm allocation process
761
        can_host_vms = n['vm_capable'] and not (n['drained'] or n['offline'])
762
        if can_host_vms and n['cnodes']:
763
            for a in attr:
764
                res[a] += int(n[a] or 0)
765
    return res
766

    
767

    
768
def update_resources(backend, resources=None):
769
    """ Update the state of the backend resources in db.
770

771
    """
772

    
773
    if not resources:
774
        resources = get_physical_resources(backend)
775

    
776
    backend.mfree = resources['mfree']
777
    backend.mtotal = resources['mtotal']
778
    backend.dfree = resources['dfree']
779
    backend.dtotal = resources['dtotal']
780
    backend.pinst_cnt = resources['pinst_cnt']
781
    backend.ctotal = resources['ctotal']
782
    backend.updated = datetime.now()
783
    backend.save()
784

    
785

    
786
def get_memory_from_instances(backend):
787
    """ Get the memory that is used from instances.
788

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

792
    """
793
    with pooled_rapi_client(backend) as client:
794
        instances = client.GetInstances(bulk=True)
795
    mem = 0
796
    for i in instances:
797
        mem += i['oper_ram']
798
    return mem
799

    
800
##
801
## Synchronized operations for reconciliation
802
##
803

    
804

    
805
def create_network_synced(network, backend):
806
    result = _create_network_synced(network, backend)
807
    if result[0] != 'success':
808
        return result
809
    result = connect_network_synced(network, backend)
810
    return result
811

    
812

    
813
def _create_network_synced(network, backend):
814
    with pooled_rapi_client(backend) as client:
815
        job = _create_network(network, backend)
816
        result = wait_for_job(client, job)
817
    return result
818

    
819

    
820
def connect_network_synced(network, backend):
821
    with pooled_rapi_client(backend) as client:
822
        for group in client.GetGroups():
823
            job = client.ConnectNetwork(network.backend_id, group,
824
                                        network.mode, network.link)
825
            result = wait_for_job(client, job)
826
            if result[0] != 'success':
827
                return result
828

    
829
    return result
830

    
831

    
832
def wait_for_job(client, jobid):
833
    result = client.WaitForJobChange(jobid, ['status', 'opresult'], None, None)
834
    status = result['job_info'][0]
835
    while status not in ['success', 'error', 'cancel']:
836
        result = client.WaitForJobChange(jobid, ['status', 'opresult'],
837
                                         [result], None)
838
        status = result['job_info'][0]
839

    
840
    if status == 'success':
841
        return (status, None)
842
    else:
843
        error = result['job_info'][1]
844
        return (status, error)