Revision a1baa42b

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
b/snf-cyclades-app/synnefo/logic/reconciliation.py
288 288
        db_nics = db_server.nics.order_by("index")
289 289
        gnt_nics = gnt_server["nics"]
290 290
        gnt_nics_parsed = backend_mod.process_ganeti_nics(gnt_nics)
291
        if backend_mod.nics_changed(db_nics, gnt_nics_parsed):
292
            msg = "Found unsynced NICs for server '%s'.\n\t"\
293
                  "DB: %s\n\tGaneti: %s"
294
            db_nics_str = ", ".join(map(format_db_nic, db_nics))
295
            gnt_nics_str = ", ".join(map(format_gnt_nic, gnt_nics_parsed))
291
        nics_changed = len(db_nics) != len(gnt_nics)
292
        for db_nic, gnt_nic in zip(db_nics, sorted(gnt_nics_parsed.items())):
293
            gnt_nic_id, gnt_nic = gnt_nic
294
            if (db_nic.id == gnt_nic_id) and\
295
               backend_mod.nics_are_equal(db_nic, gnt_nic):
296
                continue
297
            else:
298
                nics_changed = True
299
                break
300
        if nics_changed:
301
            msg = "Found unsynced NICs for server '%s'.\n"\
302
                  "\tDB:\n\t\t%s\n\tGaneti:\n\t\t%s"
303
            db_nics_str = "\n\t\t".join(map(format_db_nic, db_nics))
304
            gnt_nics_str = "\n\t\t".join(map(format_gnt_nic,
305
                                         gnt_nics_parsed.items()))
296 306
            self.log.info(msg, server_id, db_nics_str, gnt_nics_str)
297 307
            if self.options["fix_unsynced_nics"]:
298 308
                backend_mod.process_net_status(vm=db_server,
......
322 332
                self.log.info("Cleared pending task for server '%s", server_id)
323 333

  
324 334

  
335
NIC_MSG = ": %s\t".join(["ID", "State", "IP", "Network", "MAC", "Firewall"])\
336
    + ": %s"
337

  
338

  
325 339
def format_db_nic(nic):
326
    return "Index: %s, IP: %s Network: %s MAC: %s Firewall: %s" % (nic.index,
327
           nic.ipv4, nic.network_id, nic.mac, nic.firewall_profile)
340
    return NIC_MSG % (nic.id, nic.state, nic.ipv4, nic.network_id, nic.mac,
341
                      nic.firewall_profile)
328 342

  
329 343

  
330 344
def format_gnt_nic(nic):
331
    return "Index: %s IP: %s Network: %s MAC: %s Firewall: %s" %\
332
           (nic["index"], nic["ipv4"], nic["network"], nic["mac"],
333
            nic["firewall_profile"])
345
    nic_name, nic = nic
346
    return NIC_MSG % (nic_name, nic["state"], nic["ipv4"], nic["network"],
347
                      nic["mac"], nic["firewall_profile"])
334 348

  
335 349

  
336 350
#
......
420 434

  
421 435
def nics_from_instance(i):
422 436
    ips = zip(itertools.repeat('ip'), i['nic.ips'])
437
    names = zip(itertools.repeat('name'), i['nic.names'])
423 438
    macs = zip(itertools.repeat('mac'), i['nic.macs'])
424
    networks = zip(itertools.repeat('network'), i['nic.networks'])
439
    networks = zip(itertools.repeat('network'), i['nic.networks.names'])
425 440
    # modes = zip(itertools.repeat('mode'), i['nic.modes'])
426 441
    # links = zip(itertools.repeat('link'), i['nic.links'])
427 442
    # nics = zip(ips,macs,modes,networks,links)
428
    nics = zip(ips, macs, networks)
443
    nics = zip(ips, names, macs, networks)
429 444
    nics = map(lambda x: dict(x), nics)
430 445
    #nics = dict(enumerate(nics))
431 446
    tags = i["tags"]
b/snf-cyclades-app/synnefo/logic/tests/reconciliation.py
122 122
             "mtime": time(),
123 123
             "disk.sizes": [],
124 124
             "nic.ips": [],
125
             "nic.names": [],
125 126
             "nic.macs": [],
126
             "nic.networks": [],
127
             "nic.networks.names": [],
127 128
             "tags": []}]
128 129
        self.reconciler.reconcile()
129 130
        cmrapi.DeleteInstance\
......
142 143
             "mtime": time(),
143 144
             "disk.sizes": [],
144 145
             "nic.ips": [],
146
             "nic.names": [],
145 147
             "nic.macs": [],
146
             "nic.networks": [],
148
             "nic.networks.names": [],
147 149
             "tags": []}]
148 150
        with mocked_quotaholder():
149 151
            self.reconciler.reconcile()
......
168 170
             "mtime": time(),
169 171
             "disk.sizes": [],
170 172
             "nic.ips": [],
173
             "nic.names": [],
171 174
             "nic.macs": [],
172
             "nic.networks": [],
175
             "nic.networks.names": [],
173 176
             "tags": []}]
174 177
        with mocked_quotaholder():
175 178
            self.reconciler.reconcile()
......
183 186
        vm1 = mfactory.VirtualMachineFactory(backend=self.backend,
184 187
                                             deleted=False,
185 188
                                             operstate="STOPPED")
186
        mfactory.NetworkInterfaceFactory(machine=vm1, network=network1,
187
                                         ipv4="10.0.0.0")
189
        nic = mfactory.NetworkInterfaceFactory(machine=vm1, network=network1,
190
                                               ipv4="10.0.0.0")
188 191
        mrapi().GetInstances.return_value =\
189 192
            [{"name": vm1.backend_vm_id,
190 193
             "beparams": {"maxmem": 2048,
......
193 196
             "oper_state": True,
194 197
             "mtime": time(),
195 198
             "disk.sizes": [],
199
             "nic.names": [nic.backend_uuid],
196 200
             "nic.ips": ["192.168.2.1"],
197 201
             "nic.macs": ["aa:00:bb:cc:dd:ee"],
198
             "nic.networks": [network2.backend_id],
202
             "nic.networks.names": [network2.backend_id],
199 203
             "tags": []}]
200 204
        with mocked_quotaholder():
201 205
            self.reconciler.reconcile()
......
227 231
        # Test creation if exists in Ganeti
228 232
        self.assertEqual(net1.backend_networks.count(), 0)
229 233
        mrapi().GetNetworks.return_value = [{"name": net1.backend_id,
230
                                             "group_list": ["default"],
234
                                             "group_list": [["default",
235
                                                             "bridged",
236
                                                             "prv0"]],
231 237
                                             "network": net1.subnet,
232 238
                                             "map": "....",
233 239
                                             "external_reservations": ""}]
b/snf-cyclades-app/synnefo/logic/tests/servers.py
128 128
        nics = kwargs["nics"][0]
129 129
        self.assertEqual(kwargs["instance"], vm.backend_vm_id)
130 130
        self.assertEqual(nics[0], "add")
131
        self.assertEqual(nics[1]["ip"], "192.168.2.2")
132
        self.assertEqual(nics[1]["network"], net.backend_id)
131
        self.assertEqual(nics[1], "-1")
132
        self.assertEqual(nics[2]["ip"], "192.168.2.2")
133
        self.assertEqual(nics[2]["network"], net.backend_id)
133 134

  
134 135
        # No dhcp
135 136
        vm = mfactory.VirtualMachineFactory(operstate="STARTED")
......
145 146
        nics = kwargs["nics"][0]
146 147
        self.assertEqual(kwargs["instance"], vm.backend_vm_id)
147 148
        self.assertEqual(nics[0], "add")
148
        self.assertEqual(nics[1]["ip"], None)
149
        self.assertEqual(nics[1]["network"], net.backend_id)
149
        self.assertEqual(nics[1], "-1")
150
        self.assertEqual(nics[2]["ip"], None)
151
        self.assertEqual(nics[2]["network"], net.backend_id)
150 152

  
151 153
        # Test connect to IPv6 only network
152 154
        vm = mfactory.VirtualMachineFactory(operstate="STARTED")
......
159 161
        nics = kwargs["nics"][0]
160 162
        self.assertEqual(kwargs["instance"], vm.backend_vm_id)
161 163
        self.assertEqual(nics[0], "add")
162
        self.assertEqual(nics[1]["ip"], None)
163
        self.assertEqual(nics[1]["network"], net.backend_id)
164
        self.assertEqual(nics[1], "-1")
165
        self.assertEqual(nics[2]["ip"], None)
166
        self.assertEqual(nics[2]["network"], net.backend_id)
164 167

  
165 168

  
166 169
@patch("synnefo.logic.rapi_pool.GanetiRapiClient")
b/snf-cyclades-app/synnefo/logic/utils.py
55 55

  
56 56

  
57 57
def id_from_network_name(name):
58
    """Returns Network's Django id, given a ganeti machine name.
58
    """Returns Network's Django id, given a ganeti network name.
59 59

  
60 60
    Strips the ganeti prefix atm. Needs a better name!
61 61

  
......
73 73
    return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(id))
74 74

  
75 75

  
76
def id_from_nic_name(name):
77
    """Returns NIC's Django id, given a Ganeti's NIC name.
78

  
79
    """
80
    if not str(name).startswith(settings.BACKEND_PREFIX_ID):
81
        raise ValueError("Invalid NIC name: %s" % name)
82
    ns = str(name).replace(settings.BACKEND_PREFIX_ID + 'nic-', "", 1)
83
    if not ns.isdigit():
84
        raise ValueError("Invalid NIC name: %s" % name)
85

  
86
    return int(ns)
87

  
88

  
76 89
def get_rsapi_state(vm):
77 90
    """Returns the API state for a virtual machine
78 91

  

Also available in: Unified diff