Skip to content

Commit 7564538

Browse files
Allocate NIC device id atomically to stop duplicate ethN on a VM
createNicForVm() reads the next free device id and persists the nic in two separate steps: int deviceId = _nicDao.getFreeDeviceId(vm.getId()); // reads existing nics ... allocateNic(..., deviceId, ...); // persists much later getFreeDeviceId() has no lock, and the nic row it is choosing an id for is not written until the end of allocateNic(). Two nics added to the same VM at once therefore both read the same free id and land on the same ethN. This happens when several tiers of a VPC are brought up in parallel: each tier attaches the *shared* redundant VR, so N concurrent createNicForVm() calls run against one router. The result is two guest nics on one ethN (one network blackholed) and, because the two routers of a redundant VPC end up with different device -> network maps, divergent keepalived configs -> both routers go PRIMARY and the VPC is dead. (issue #11710) Fix: compute the free device id and persist the nic under the vm_instance row lock, so device id assignment is atomic per VM. This is the same shape as the existing persistNicAfterRaceCheck() IPv4 race guard and needs no change to job dispatch. Serializing per router is what the non-concurrent path already does implicitly (one tier at a time). Fixes: #11710 Signed-off-by: Brad House <bhouse@nexthop.ai>
1 parent b9a5977 commit 7564538

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@
237237
import com.cloud.utils.db.Transaction;
238238
import com.cloud.utils.db.TransactionCallback;
239239
import com.cloud.utils.db.TransactionCallbackNoReturn;
240+
import com.cloud.utils.db.TransactionCallbackWithException;
240241
import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn;
241242
import com.cloud.utils.db.TransactionStatus;
242243
import com.cloud.utils.exception.CloudRuntimeException;
@@ -1141,6 +1142,29 @@ private NicVO checkForRaceAndAllocateNic(final NicProfile requested, final Netwo
11411142
return vo;
11421143
}
11431144

1145+
/**
1146+
* Allocate a nic for {@code vm} on {@code network}, choosing its device id atomically.
1147+
*
1148+
* {@link NicDao#getFreeDeviceId(long)} picks the first unused device id by reading the vm's
1149+
* existing nics, but the new nic row is not persisted until the end of {@link #allocateNic}. Two
1150+
* nics being added to the same vm concurrently (e.g. several tiers of a VPC brought up in parallel,
1151+
* each attaching the shared redundant VR) would otherwise both read the same free id and land on
1152+
* the same {@code ethN} — corrupting the VR config and, for a redundant VPC, driving both routers
1153+
* PRIMARY (issue #11710). Holding the {@code vm_instance} row lock across the read-and-persist makes
1154+
* the device id assignment atomic per vm.
1155+
*/
1156+
protected NicProfile allocateNicWithFreeDeviceId(final NicProfile requested, final Network network, final boolean isDefaultNic, final VirtualMachineProfile vm)
1157+
throws InsufficientCapacityException, ConcurrentOperationException {
1158+
return Transaction.execute(new TransactionCallbackWithException<NicProfile, InsufficientCapacityException>() {
1159+
@Override
1160+
public NicProfile doInTransaction(final TransactionStatus status) throws InsufficientCapacityException {
1161+
_vmDao.lockRow(vm.getId(), true);
1162+
final int deviceId = _nicDao.getFreeDeviceId(vm.getId());
1163+
return allocateNic(requested, network, isDefaultNic, deviceId, vm).first();
1164+
}
1165+
});
1166+
}
1167+
11441168
@DB
11451169
@Override
11461170
public Pair<NicProfile, Integer> allocateNic(final NicProfile requested, final Network network, final Boolean isDefaultNic, int deviceId, final VirtualMachineProfile vm)
@@ -4510,11 +4534,9 @@ public NicProfile createNicForVm(final Network network, final NicProfile request
45104534

45114535
//1) allocate nic (if needed) Always allocate if it is a user vm
45124536
if (nic == null || vmProfile.getType() == VirtualMachine.Type.User) {
4513-
final int deviceId = _nicDao.getFreeDeviceId(vm.getId());
4514-
4515-
boolean isDefaultNic = getNicProfileDefaultNic(requested);
4537+
final boolean isDefaultNic = getNicProfileDefaultNic(requested);
45164538

4517-
nic = allocateNic(requested, network, isDefaultNic, deviceId, vmProfile).first();
4539+
nic = allocateNicWithFreeDeviceId(requested, network, isDefaultNic, vmProfile);
45184540

45194541
if (nic == null) {
45204542
throw new CloudRuntimeException("Failed to allocate nic for Instance " + vm + " in network " + network);

engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.junit.runner.RunWith;
4242
import org.junit.runners.JUnit4;
4343
import org.mockito.ArgumentMatchers;
44+
import org.mockito.InOrder;
4445
import org.mockito.MockedStatic;
4546
import org.mockito.Mockito;
4647

@@ -76,6 +77,8 @@
7677
import com.cloud.utils.db.EntityManager;
7778
import com.cloud.utils.db.Transaction;
7879
import com.cloud.utils.db.TransactionCallback;
80+
import com.cloud.utils.db.TransactionCallbackWithException;
81+
import com.cloud.utils.db.TransactionStatus;
7982
import com.cloud.utils.exception.CloudRuntimeException;
8083
import com.cloud.utils.net.Ip;
8184
import com.cloud.vm.DomainRouterVO;
@@ -88,6 +91,7 @@
8891
import com.cloud.vm.VirtualMachineProfile;
8992
import com.cloud.vm.dao.DomainRouterDao;
9093
import com.cloud.vm.dao.NicDao;
94+
import com.cloud.vm.dao.VMInstanceDao;
9195
import com.cloud.vm.dao.NicExtraDhcpOptionDao;
9296
import com.cloud.vm.dao.NicIpAliasDao;
9397
import com.cloud.vm.dao.NicSecondaryIpDao;
@@ -135,6 +139,7 @@ public void setUp() {
135139
testOrchestrator.routerJoinDao = mock(DomainRouterJoinDao.class);
136140
testOrchestrator._ipAddrMgr = mock(IpAddressManager.class);
137141
testOrchestrator._entityMgr = mock(EntityManager.class);
142+
testOrchestrator._vmDao = mock(VMInstanceDao.class);
138143
DhcpServiceProvider provider = mock(DhcpServiceProvider.class);
139144

140145
Map<Network.Capability, String> capabilities = new HashMap<Network.Capability, String>();
@@ -1010,4 +1015,43 @@ public void testImportNicWithIP4Address() throws Exception {
10101015
assertEquals("testtag", nicProfile.getName());
10111016
}
10121017
}
1018+
1019+
@Test
1020+
public void testAllocateNicWithFreeDeviceIdLocksVmRowBeforeReadingFreeDeviceId() throws Exception {
1021+
final long vmId = 100L;
1022+
final int freeDeviceId = 5;
1023+
1024+
VirtualMachine vm = mock(VirtualMachine.class);
1025+
when(vm.getId()).thenReturn(vmId);
1026+
VirtualMachineProfile vmProfile = mock(VirtualMachineProfile.class);
1027+
when(vmProfile.getId()).thenReturn(vmId);
1028+
when(vmProfile.getVirtualMachine()).thenReturn(vm);
1029+
1030+
Network network = mock(Network.class);
1031+
NicProfile requested = mock(NicProfile.class);
1032+
NicProfile allocated = mock(NicProfile.class);
1033+
1034+
when(testOrchestrator._nicDao.getFreeDeviceId(vmId)).thenReturn(freeDeviceId);
1035+
Mockito.doReturn(new Pair<>(allocated, freeDeviceId)).when(testOrchestrator)
1036+
.allocateNic(requested, network, false, freeDeviceId, vmProfile);
1037+
1038+
try (MockedStatic<Transaction> transactionMocked = Mockito.mockStatic(Transaction.class)) {
1039+
// run the callback body so the lock/read/allocate ordering is exercised
1040+
transactionMocked.when(() -> Transaction.execute(any(TransactionCallbackWithException.class)))
1041+
.thenAnswer(invocation -> {
1042+
TransactionCallbackWithException<NicProfile, InsufficientCapacityException> cb = invocation.getArgument(0);
1043+
return cb.doInTransaction(mock(TransactionStatus.class));
1044+
});
1045+
1046+
NicProfile result = testOrchestrator.allocateNicWithFreeDeviceId(requested, network, false, vmProfile);
1047+
1048+
assertEquals(allocated, result);
1049+
// the vm_instance row must be locked before the free device id is read,
1050+
// otherwise two concurrent adds pick the same id (issue #11710)
1051+
InOrder inOrder = Mockito.inOrder(testOrchestrator._vmDao, testOrchestrator._nicDao);
1052+
inOrder.verify(testOrchestrator._vmDao).lockRow(vmId, true);
1053+
inOrder.verify(testOrchestrator._nicDao).getFreeDeviceId(vmId);
1054+
verify(testOrchestrator, times(1)).allocateNic(requested, network, false, freeDeviceId, vmProfile);
1055+
}
1056+
}
10131057
}

0 commit comments

Comments
 (0)