Revision a1baa42b snf-cyclades-app/synnefo/logic/backend.py

b/snf-cyclades-app/synnefo/logic/backend.py
32 32
# or implied, of GRNET S.A.
33 33
from django.conf import settings
34 34
from django.db import transaction
35
from datetime import datetime
35
from datetime import datetime, timedelta
36 36

  
37 37
from synnefo.db.models import (Backend, VirtualMachine, Network,
38
                               FloatingIP,
39 38
                               BackendNetwork, BACKEND_STATUSES,
40 39
                               pooled_rapi_client, VirtualMachineDiagnostic,
41 40
                               Flavor)
......
57 56
_reverse_tags = dict((v.split(':')[3], k) for k, v in _firewall_tags.items())
58 57

  
59 58

  
59
NIC_FIELDS = ["state", "mac", "ipv4", "ipv6", "network", "firewall_profile"]
60

  
61

  
60 62
def handle_vm_quotas(vm, job_id, job_opcode, job_status, job_fields):
61 63
    """Handle quotas for updated VirtualMachine.
62 64

  
......
165 167
        # See ticket #799 for all the details.
166 168
        if status == 'success' or (status == 'error' and
167 169
                                   not vm_exists_in_backend(vm)):
168
            # VM has been deleted. Release the instance IPs
169
            release_instance_ips(vm, [])
170
            # And delete the releated NICs (must be performed after release!)
171
            vm.nics.all().delete()
170
            # VM has been deleted
171
            for nic in vm.nics.all():
172
                # Release the IP
173
                release_nic_address(nic)
174
                # And delete the NIC.
175
                nic.delete()
172 176
            vm.deleted = True
173 177
            vm.operstate = state_for_success
174 178
            vm.backendtime = etime
......
217 221
    detailing the NIC configuration of a VM instance.
218 222

  
219 223
    Update the state of the VM in the DB accordingly.
220
    """
221 224

  
225
    """
222 226
    ganeti_nics = process_ganeti_nics(nics)
223
    if not nics_changed(vm.nics.order_by('index'), ganeti_nics):
224
        log.debug("NICs for VM %s have not changed", vm)
225
        return
227
    db_nics = dict([(nic.id, nic) for nic in vm.nics.all()])
226 228

  
227 229
    # Get X-Lock on backend before getting X-Lock on network IP pools, to
228 230
    # guarantee that no deadlock will occur with Backend allocator.
229 231
    Backend.objects.select_for_update().get(id=vm.backend_id)
230 232

  
231
    # NICs have changed. Release the instance IPs
232
    release_instance_ips(vm, ganeti_nics)
233
    # And delete the releated NICs (must be performed after release!)
234
    vm.nics.all().delete()
235

  
236
    for nic in ganeti_nics:
237
        ipv4 = nic["ipv4"]
238
        net = nic['network']
239
        if ipv4:
240
            net.reserve_address(ipv4)
241

  
242
        vm.nics.create(**nic)
243
        # Dummy save the network, because UI uses changed-since for VMs
244
        # and Networks in order to show the VM NICs
245
        net.save()
233
    for nic_name in set(db_nics.keys()) | set(ganeti_nics.keys()):
234
        db_nic = db_nics.get(nic_name)
235
        ganeti_nic = ganeti_nics.get(nic_name)
236
        if ganeti_nic is None:
237
            # NIC exists in DB but not in Ganeti. If the NIC is in 'building'
238
            # state for more than 5 minutes, then we remove the NIC.
239
            # TODO: This is dangerous as the job may be stack in the queue, and
240
            # releasing the IP may lead to duplicate IP use.
241
            if nic.state != "BUILDING" or (nic.state == "BUILDING" and
242
               etime > nic.created + timedelta(minutes=5)):
243
                release_nic_address(nic)
244
                nic.delete()
245
            else:
246
                log.warning("Ignoring recent building NIC: %s", nic)
247
        elif db_nic is None:
248
            # NIC exists in Ganeti but not in DB
249
            if ganeti_nic["ipv4"]:
250
                network = ganeti_nic["network"]
251
                network.reserve_address(ganeti_nic["ipv4"])
252
            vm.nics.create(**ganeti_nic)
253
        elif not nics_are_equal(db_nic, ganeti_nic):
254
            # Special case where the IPv4 address has changed, because you
255
            # need to release the old IPv4 address and reserve the new one
256
            if db_nic.ipv4 != ganeti_nic["ipv4"]:
257
                release_nic_address(db_nic)
258
                if ganeti_nic["ipv4"]:
259
                    ganeti_nic["network"].reserve_address(ganeti_nic["ipv4"])
260

  
261
            # Update the NIC in DB with the values from Ganeti NIC
262
            [setattr(db_nic, f, ganeti_nic[f]) for f in NIC_FIELDS]
263
            db_nic.save()
264

  
265
            # Dummy update the network, to work with 'changed-since'
266
            db_nic.network.save()
246 267

  
247 268
    vm.backendtime = etime
248 269
    vm.save()
249 270

  
250 271

  
272
def nics_are_equal(db_nic, gnt_nic):
273
    for field in NIC_FIELDS:
274
        if getattr(db_nic, field) != gnt_nic[field]:
275
            return False
276
    return True
277

  
278

  
251 279
def process_ganeti_nics(ganeti_nics):
252
    """Process NIC dict from ganeti hooks."""
280
    """Process NIC dict from ganeti"""
253 281
    new_nics = []
254
    for i, new_nic in enumerate(ganeti_nics):
255
        network = new_nic.get('network', '')
256
        n = str(network)
257
        pk = utils.id_from_network_name(n)
258

  
259
        net = Network.objects.get(pk=pk)
282
    for index, gnic in enumerate(ganeti_nics):
283
        nic_name = gnic.get("name", None)
284
        if nic_name is not None:
285
            nic_id = utils.id_from_nic_name(nic_name)
286
        else:
287
            # Put as default value the index. If it is an unknown NIC to
288
            # synnefo it will be created automaticaly.
289
            nic_id = "unknown-" + str(index)
290
        network_name = gnic.get('network', '')
291
        network_id = utils.id_from_network_name(network_name)
292
        network = Network.objects.get(id=network_id)
260 293

  
261 294
        # Get the new nic info
262
        mac = new_nic.get('mac')
263
        ipv4 = new_nic.get('ip')
264
        ipv6 = mac2eui64(mac, net.subnet6) if net.subnet6 is not None else None
295
        mac = gnic.get('mac')
296
        ipv4 = gnic.get('ip')
297
        ipv6 = mac2eui64(mac, network.subnet6)\
298
            if network.subnet6 is not None else None
265 299

  
266
        firewall = new_nic.get('firewall')
300
        firewall = gnic.get('firewall')
267 301
        firewall_profile = _reverse_tags.get(firewall)
268
        if not firewall_profile and net.public:
302
        if not firewall_profile and network.public:
269 303
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
270 304

  
271
        nic = {
272
            'index': i,
273
            'network': net,
305
        nic_info = {
306
            'index': index,
307
            'network': network,
274 308
            'mac': mac,
275 309
            'ipv4': ipv4,
276 310
            'ipv6': ipv6,
277 311
            'firewall_profile': firewall_profile,
278 312
            'state': 'ACTIVE'}
279 313

  
280
        new_nics.append(nic)
281
    return new_nics
314
        new_nics.append((nic_id, nic_info))
315
    return dict(new_nics)
282 316

  
283 317

  
284
def nics_changed(old_nics, new_nics):
285
    """Return True if NICs have changed in any way."""
286
    if len(old_nics) != len(new_nics):
287
        return True
288
    fields = ["ipv4", "ipv6", "mac", "firewall_profile", "index", "network"]
289
    for old_nic, new_nic in zip(old_nics, new_nics):
290
        for field in fields:
291
            if getattr(old_nic, field) != new_nic[field]:
292
                return True
293
    return False
294

  
295

  
296
def release_instance_ips(vm, ganeti_nics):
297
    old_addresses = set(vm.nics.values_list("network", "ipv4"))
298
    new_addresses = set(map(lambda nic: (nic["network"].id, nic["ipv4"]),
299
                            ganeti_nics))
300
    to_release = old_addresses - new_addresses
301
    for (network_id, ipv4) in to_release:
302
        if ipv4:
303
            # Get X-Lock before searching floating IP, to exclusively search
304
            # and release floating IP. Otherwise you may release a floating IP
305
            # that has been just reserved.
306
            net = Network.objects.select_for_update().get(id=network_id)
307
            if net.floating_ip_pool:
308
                try:
309
                    floating_ip = net.floating_ips.select_for_update()\
310
                                                  .get(ipv4=ipv4, machine=vm,
311
                                                       deleted=False)
312
                    floating_ip.machine = None
313
                    floating_ip.save()
314
                except FloatingIP.DoesNotExist:
315
                    net.release_address(ipv4)
316
            else:
317
                net.release_address(ipv4)
318
def release_nic_address(nic):
319
    """Release the IPv4 address of a NIC.
320

  
321
    Check if an instance's NIC has an IPv4 address and release it if it is not
322
    a Floating IP.
323

  
324
    """
325

  
326
    if nic.ipv4 and not nic.ip_type == "FLOATING":
327
        nic.network.release_address(nic.ipv4)
318 328

  
319 329

  
320 330
@transaction.commit_on_success

Also available in: Unified diff