Skip to content

Commit 0b7aa78

Browse files
Fix MAC conflict options for remote KVM VM imports
Remote KVM imports always generated a replacement MAC on conflict because the import path hardcoded forced=true and duplicate preservation=false. Preserve the historical replacement default when forced is omitted, while honoring explicit forced=false and allowduplicatemacaddresses=true. Reject incompatible options before import side effects and forward the resolved policy to NIC allocation. Update the UI to show the remote KVM default, preserve mutual exclusion, and submit forced=false explicitly. Add backend, allocator, and UI coverage for the supported conflict policies.
1 parent f3ec294 commit 0b7aa78

7 files changed

Lines changed: 340 additions & 26 deletions

File tree

‎api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@ public boolean isForced() {
284284
return BooleanUtils.isTrue(forced);
285285
}
286286

287+
public Boolean getForced() {
288+
return forced;
289+
}
290+
287291
public boolean isAllowDuplicateMacAddresses() {
288292
return BooleanUtils.isTrue(allowDuplicateMacAddresses);
289293
}

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
import com.cloud.utils.db.EntityManager;
8383
import com.cloud.utils.db.Transaction;
8484
import com.cloud.utils.db.TransactionCallback;
85+
import com.cloud.utils.db.TransactionStatus;
8586
import com.cloud.utils.exception.CloudRuntimeException;
8687
import com.cloud.utils.net.Ip;
8788
import com.cloud.vm.DomainRouterVO;
@@ -945,6 +946,23 @@ public void testImportNicAutoAcquireGuestIPFailed() throws Exception {
945946
testOrchestrator.importNic(macAddress, deviceId, network, true, vm, ipAddresses, dataCenter, false, false);
946947
}
947948

949+
@Test(expected = InsufficientVirtualNetworkCapacityException.class)
950+
public void testImportNicDuplicateMacAllowanceDoesNotBypassIpAllocation() throws Exception {
951+
DataCenter dataCenter = Mockito.mock(DataCenter.class);
952+
VirtualMachine vm = mock(VirtualMachine.class);
953+
Network network = Mockito.mock(Network.class);
954+
Mockito.when(network.getGuestType()).thenReturn(GuestType.Isolated);
955+
Mockito.when(network.getNetworkOfferingId()).thenReturn(networkOfferingId);
956+
long dataCenterId = 1L;
957+
Mockito.when(network.getDataCenterId()).thenReturn(dataCenterId);
958+
Network.IpAddresses ipAddresses = Mockito.mock(Network.IpAddresses.class);
959+
Mockito.when(ipAddresses.getIp4Address()).thenReturn("10.1.10.10");
960+
Mockito.when(testOrchestrator.getSelectedIpForNicImport(network, dataCenter, ipAddresses)).thenReturn(null);
961+
Mockito.when(testOrchestrator._networkModel.listNetworkOfferingServices(networkOfferingId)).thenReturn(Arrays.asList(Service.Dns, Service.Dhcp));
962+
963+
testOrchestrator.importNic("02:01:01:82:00:01", 0, network, true, vm, ipAddresses, dataCenter, false, true);
964+
}
965+
948966
@Test
949967
public void testImportNicNoIP4Address() throws Exception {
950968
DataCenter dataCenter = Mockito.mock(DataCenter.class);
@@ -1021,6 +1039,70 @@ public void testImportNicWithIP4Address() throws Exception {
10211039
}
10221040
}
10231041

1042+
@Test
1043+
public void testImportNicReplacesConflictingMacWhenForced() throws Exception {
1044+
String sourceMacAddress = "02:01:01:82:00:01";
1045+
String replacementMacAddress = "02:01:01:82:00:02";
1046+
when(testOrchestrator._networkModel.getNextAvailableMacAddressInNetwork(1L)).thenReturn(replacementMacAddress);
1047+
1048+
NicProfile nicProfile = importNicWithMacPolicy(sourceMacAddress, true, true, false);
1049+
1050+
assertEquals(replacementMacAddress, nicProfile.getMacAddress());
1051+
}
1052+
1053+
@Test(expected = CloudRuntimeException.class)
1054+
public void testImportNicRejectsConflictingMacWhenNotForced() throws Exception {
1055+
importNicWithMacPolicy("02:01:01:82:00:01", true, false, false);
1056+
}
1057+
1058+
@Test
1059+
public void testImportNicPreservesAllowedDuplicateMac() throws Exception {
1060+
String sourceMacAddress = "02:01:01:82:00:01";
1061+
1062+
NicProfile nicProfile = importNicWithMacPolicy(sourceMacAddress, true, false, true);
1063+
1064+
assertEquals(sourceMacAddress, nicProfile.getMacAddress());
1065+
verify(testOrchestrator._networkModel, never()).getNextAvailableMacAddressInNetwork(1L);
1066+
}
1067+
1068+
@Test
1069+
public void testImportNicPreservesUnusedSourceMac() throws Exception {
1070+
String sourceMacAddress = "02:01:01:82:00:01";
1071+
1072+
NicProfile nicProfile = importNicWithMacPolicy(sourceMacAddress, false, true, false);
1073+
1074+
assertEquals(sourceMacAddress, nicProfile.getMacAddress());
1075+
verify(testOrchestrator._networkModel, never()).getNextAvailableMacAddressInNetwork(1L);
1076+
}
1077+
1078+
@SuppressWarnings("unchecked")
1079+
private NicProfile importNicWithMacPolicy(String macAddress, boolean macAddressExists, boolean forced,
1080+
boolean allowDuplicateMacAddress) throws Exception {
1081+
long networkId = 1L;
1082+
long vmId = 1L;
1083+
DataCenter dataCenter = mock(DataCenter.class);
1084+
Network network = mock(Network.class);
1085+
VirtualMachine vm = mock(VirtualMachine.class);
1086+
when(network.getId()).thenReturn(networkId);
1087+
when(network.getGuestType()).thenReturn(GuestType.L2);
1088+
when(vm.getId()).thenReturn(vmId);
1089+
when(vm.getType()).thenReturn(Type.User);
1090+
when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
1091+
if (macAddressExists) {
1092+
when(testOrchestrator._nicDao.findByNetworkIdAndMacAddress(networkId, macAddress)).thenReturn(mock(NicVO.class));
1093+
}
1094+
when(testOrchestrator._nicDao.persist(any(NicVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
1095+
1096+
try (MockedStatic<Transaction> transactionMocked = Mockito.mockStatic(Transaction.class)) {
1097+
transactionMocked.when(() -> Transaction.execute(any(TransactionCallback.class))).thenAnswer(invocation -> {
1098+
TransactionCallback<NicVO> callback = invocation.getArgument(0);
1099+
return callback.doInTransaction(mock(TransactionStatus.class));
1100+
});
1101+
return testOrchestrator.importNic(macAddress, 0, network, true, vm, null, dataCenter,
1102+
forced, allowDuplicateMacAddress).first();
1103+
}
1104+
}
1105+
10241106
// -----------------------------------------------------------------------
10251107
// Tests for getNetworkElementsIncludingExtensions
10261108
// -----------------------------------------------------------------------

‎server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2648,6 +2648,17 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) {
26482648
Long poolId = cmd.getStoragePoolId();
26492649
Long networkId = cmd.getNetworkId();
26502650

2651+
boolean forced = false;
2652+
boolean allowDuplicateMacAddress = false;
2653+
if (ImportSource.EXTERNAL == importSource) {
2654+
Boolean requestedForced = cmd.getForced();
2655+
allowDuplicateMacAddress = cmd.isAllowDuplicateMacAddresses();
2656+
if (BooleanUtils.isTrue(requestedForced) && allowDuplicateMacAddress) {
2657+
throw new InvalidParameterValueException(String.format("%s and %s are mutually exclusive", ApiConstants.FORCED, ApiConstants.ALLOW_DUPLICATE_MAC_ADDRESSES));
2658+
}
2659+
forced = !allowDuplicateMacAddress && (requestedForced == null || requestedForced);
2660+
}
2661+
26512662
UnmanagedInstanceTO unmanagedInstanceTO = null;
26522663
if (ImportSource.EXTERNAL == importSource) {
26532664
if (StringUtils.isBlank(cmd.getUsername())) {
@@ -2715,7 +2726,8 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) {
27152726
userVm = importExternalKvmVirtualMachine(unmanagedInstanceTO, instanceName, zone,
27162727
template, displayName, hostName, caller, owner, userId,
27172728
serviceOffering, dataDiskOfferingMap,
2718-
nicNetworkMap, nicIpAddressMap, remoteUrl, username, password, tmpPath, details);
2729+
nicNetworkMap, nicIpAddressMap, remoteUrl, username, password, tmpPath, details,
2730+
forced, allowDuplicateMacAddress);
27192731
} else if (ImportSource.SHARED == importSource || ImportSource.LOCAL == importSource) {
27202732
try {
27212733
userVm = importKvmVirtualMachineFromDisk(importSource, instanceName, zone,
@@ -2747,7 +2759,8 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag
27472759
final VirtualMachineTemplate template, final String displayName, final String hostName, final Account caller, final Account owner, final Long userId,
27482760
final ServiceOfferingVO serviceOffering, final Map<String, Long> dataDiskOfferingMap,
27492761
final Map<String, Long> nicNetworkMap, final Map<String, Network.IpAddresses> callerNicIpAddressMap,
2750-
final String remoteUrl, String username, String password, String tmpPath, final Map<String, String> details) throws ResourceAllocationException {
2762+
final String remoteUrl, String username, String password, String tmpPath, final Map<String, String> details,
2763+
final boolean forced, final boolean allowDuplicateMacAddress) throws ResourceAllocationException {
27512764
UserVm userVm = null;
27522765

27532766
Map<String, String> allDetails = new HashMap<>(details);
@@ -2845,7 +2858,7 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag
28452858
for (UnmanagedInstanceTO.Nic nic : unmanagedInstance.getNics()) {
28462859
Network network = networkDao.findById(allNicNetworkMap.get(nic.getNicId()));
28472860
Network.IpAddresses ipAddresses = nicIpAddressMap.get(nic.getNicId());
2848-
importNic(nic, userVm, network, ipAddresses, nicIndex, nicIndex==0, true, false);
2861+
importNic(nic, userVm, network, ipAddresses, nicIndex, nicIndex == 0, forced, allowDuplicateMacAddress);
28492862
nicIndex++;
28502863
}
28512864
} catch (Exception e) {

‎server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java‎

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static org.mockito.ArgumentMatchers.nullable;
2727
import static org.mockito.Mockito.doNothing;
2828
import static org.mockito.Mockito.mock;
29+
import static org.mockito.Mockito.never;
2930
import static org.mockito.Mockito.verify;
3031
import static org.mockito.Mockito.when;
3132

@@ -467,6 +468,24 @@ public void importUnmanagedInstanceTest() {
467468
}
468469
}
469470

471+
@Test
472+
public void importUnmanagedVmwareInstanceForwardsDuplicateMacPolicy() {
473+
ImportUnmanagedInstanceCmd cmd = Mockito.mock(ImportUnmanagedInstanceCmd.class);
474+
when(cmd.getName()).thenReturn("TestInstance");
475+
when(cmd.getDomainId()).thenReturn(null);
476+
when(cmd.isAllowDuplicateMacAddresses()).thenReturn(true);
477+
when(volumeApiService.doesStoragePoolSupportDiskOffering(any(StoragePool.class), any())).thenReturn(true);
478+
479+
try (MockedStatic<UsageEventUtils> ignored = Mockito.mockStatic(UsageEventUtils.class);
480+
MockedConstruction<CheckedReservation> mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) {
481+
unmanagedVMsManager.importUnmanagedInstance(cmd);
482+
}
483+
484+
verify(networkOrchestrationService).importNic(Mockito.eq(instance.getNics().get(0).getMacAddress()), anyInt(),
485+
any(Network.class), anyBoolean(), any(VirtualMachine.class), nullable(Network.IpAddresses.class),
486+
any(DataCenter.class), Mockito.eq(false), Mockito.eq(true));
487+
}
488+
470489
@Test(expected = InvalidParameterValueException.class)
471490
public void importUnmanagedInstanceInvalidHostnameTest() {
472491
ImportUnmanagedInstanceCmd importUnmanageInstanceCmd = Mockito.mock(ImportUnmanagedInstanceCmd.class);
@@ -653,15 +672,63 @@ public void testListRemoteInstancesTestNonKVM() {
653672
unmanagedVMsManager.listVmsForImport(cmd);
654673
}
655674
@Test
656-
public void testImportFromExternalTest() throws InsufficientServerCapacityException {
657-
String vmname = "TestInstance";
675+
public void testImportFromExternalDefaultsToReplacingConflictingMac() throws InsufficientServerCapacityException {
676+
ImportVmCmd cmd = createExternalImportCmd(null, false);
677+
importExternalKvmVm(cmd);
678+
verifyExternalKvmMacPolicy(true, false);
679+
}
680+
681+
@Test
682+
public void testImportFromExternalForwardsExplicitForcedTrue() throws InsufficientServerCapacityException {
683+
ImportVmCmd cmd = createExternalImportCmd(true, false);
684+
importExternalKvmVm(cmd);
685+
verifyExternalKvmMacPolicy(true, false);
686+
}
687+
688+
@Test
689+
public void testImportFromExternalForwardsExplicitForcedFalse() throws InsufficientServerCapacityException {
690+
ImportVmCmd cmd = createExternalImportCmd(false, false);
691+
importExternalKvmVm(cmd);
692+
verifyExternalKvmMacPolicy(false, false);
693+
}
694+
695+
@Test
696+
public void testImportFromExternalForwardsDuplicateMacAllowance() throws InsufficientServerCapacityException {
697+
ImportVmCmd cmd = createExternalImportCmd(false, true);
698+
importExternalKvmVm(cmd);
699+
verifyExternalKvmMacPolicy(false, true);
700+
}
701+
702+
@Test
703+
public void testImportFromExternalRejectsIncompatibleMacOptionsBeforeImportSideEffects() {
704+
ImportVmCmd cmd = createExternalImportCmd(true, true);
705+
706+
try {
707+
unmanagedVMsManager.importVm(cmd);
708+
Assert.fail("Expected incompatible MAC address conflict options to be rejected");
709+
} catch (InvalidParameterValueException expected) {
710+
Assert.assertTrue(expected.getMessage().contains(ApiConstants.FORCED));
711+
Assert.assertTrue(expected.getMessage().contains(ApiConstants.ALLOW_DUPLICATE_MAC_ADDRESSES));
712+
}
713+
714+
verify(agentManager, never()).easySend(anyLong(), any(GetRemoteVmsCommand.class));
715+
verify(volumeManager, never()).allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean());
716+
}
717+
718+
private ImportVmCmd createExternalImportCmd(Boolean forced, boolean allowDuplicateMacAddresses) {
658719
ImportVmCmd cmd = Mockito.mock(ImportVmCmd.class);
659720
when(cmd.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM.toString());
660-
when(cmd.getName()).thenReturn(vmname);
721+
when(cmd.getName()).thenReturn("TestInstance");
661722
when(cmd.getUsername()).thenReturn("user");
662723
when(cmd.getPassword()).thenReturn("pass");
663724
when(cmd.getImportSource()).thenReturn("external");
664725
when(cmd.getDomainId()).thenReturn(null);
726+
when(cmd.getForced()).thenReturn(forced);
727+
when(cmd.isAllowDuplicateMacAddresses()).thenReturn(allowDuplicateMacAddresses);
728+
return cmd;
729+
}
730+
731+
private void importExternalKvmVm(ImportVmCmd cmd) throws InsufficientServerCapacityException {
665732
HostVO host = Mockito.mock(HostVO.class);
666733
DeployDestination mockDest = Mockito.mock(DeployDestination.class);
667734
when(deploymentPlanningManager.planDeployment(any(), any(), any(), any())).thenReturn(mockDest);
@@ -684,6 +751,12 @@ public void testImportFromExternalTest() throws InsufficientServerCapacityExcept
684751
}
685752
}
686753

754+
private void verifyExternalKvmMacPolicy(boolean forced, boolean allowDuplicateMacAddresses) {
755+
verify(networkOrchestrationService).importNic(Mockito.eq(instance.getNics().get(0).getMacAddress()), anyInt(),
756+
any(Network.class), anyBoolean(), any(VirtualMachine.class), nullable(Network.IpAddresses.class),
757+
any(DataCenter.class), Mockito.eq(forced), Mockito.eq(allowDuplicateMacAddresses));
758+
}
759+
687760
private void baseBasicParametersCheckForImportInstance(String name, Long domainId, String accountName) {
688761
unmanagedVMsManager.basicParametersCheckForImportInstance(name, domainId, accountName);
689762
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
export const getDefaultMacAddressConflictOptions = (isExternalKvmImport) => ({
19+
forced: Boolean(isExternalKvmImport),
20+
allowDuplicateMacAddresses: false
21+
})
22+
23+
export const selectForcedMacAddressConflictOption = (options, forced) => ({
24+
forced,
25+
allowDuplicateMacAddresses: forced ? false : options.allowDuplicateMacAddresses
26+
})
27+
28+
export const selectAllowDuplicateMacAddressesOption = (options, allowDuplicateMacAddresses) => ({
29+
forced: allowDuplicateMacAddresses ? false : options.forced,
30+
allowDuplicateMacAddresses
31+
})
32+
33+
export const getMacAddressConflictApiParams = (options, isExternalKvmImport, showForced, showAllowDuplicateMacAddresses) => {
34+
const params = {}
35+
if (showForced && (isExternalKvmImport || options.forced)) {
36+
params.forced = Boolean(options.forced)
37+
}
38+
if (showAllowDuplicateMacAddresses && options.allowDuplicateMacAddresses) {
39+
params.allowduplicatemacaddresses = true
40+
}
41+
return params
42+
}

0 commit comments

Comments
 (0)