Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/src/main/java/com/cloud/vm/VmDetailConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public interface VmDetailConstants {
String NETWORK = "network";
String IP4_ADDRESS = "ip4Address";
String IP6_ADDRESS = "ip6Address";
String NIC_MAC_ADDRESS = "macAddress";
String DISK = "disk";
String DISK_OFFERING = "diskOffering";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ public class ApiConstants {
public static final String NICS = "nics";
public static final String NIC_NETWORK_LIST = "nicnetworklist";
public static final String NIC_IP_ADDRESS_LIST = "nicipaddresslist";
public static final String NIC_MAC_ADDRESS_LIST = "nicmacaddresslist";
public static final String NIC_MULTIQUEUE_NUMBER = "nicmultiqueuenumber";
public static final String NIC_PACKED_VIRTQUEUES_ENABLED = "nicpackedvirtqueuesenabled";
public static final String NEW_START_IP = "newstartip";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ public class ImportUnmanagedInstanceCmd extends BaseAsyncCmd {
description = "VM NIC to ip address mapping using keys NIC, ip4Address")
private Map nicIpAddressList;

@Parameter(name = ApiConstants.NIC_MAC_ADDRESS_LIST,
type = CommandType.MAP,
description = "VM NIC to MAC address mapping using keys nic and macAddress. " +
"Example: nicmacaddresslist[0].nic=nic1&nicmacaddresslist[0].macAddress=aa:bb:cc:dd:ee:ff. " +
"Overrides the MAC address reported by the hypervisor for the given NIC.",
since = "4.21.0")
private Map nicMacAddressList;

@Parameter(name = ApiConstants.DATADISK_OFFERING_LIST,
type = CommandType.MAP,
description = "Datadisk Template to disk-offering mapping using keys disk and diskOffering")
Expand Down Expand Up @@ -234,6 +242,32 @@ public Map<String, Network.IpAddresses> getNicIpAddressList() {
return nicIpAddressMap;
}

public Map<String, String> getNicMacAddressList() {
Map<String, String> nicMacMap = new HashMap<>();
if (MapUtils.isEmpty(nicMacAddressList)) {
return nicMacMap;
}
for (Map<String, String> entry : (Collection<Map<String, String>>) nicMacAddressList.values()) {
String nic = entry.get(VmDetailConstants.NIC);
String mac = entry.get(VmDetailConstants.NIC_MAC_ADDRESS);
logger.debug("Checking if MAC address '{}' can be mapped to NIC '{}'", mac, nic);
if (StringUtils.isEmpty(nic)) {
throw new InvalidParameterValueException(String.format("NIC ID: '%s' is invalid for MAC address mapping", nic));
}
if (StringUtils.isEmpty(mac)) {
throw new InvalidParameterValueException(String.format("Empty MAC address for NIC ID: %s is invalid", nic));
}
if (!NetUtils.isValidMac(mac)) {
throw new InvalidParameterValueException(String.format("MAC address '%s' for NIC ID: %s is not valid", mac, nic));
}
if (!NetUtils.isUnicastMac(mac)) {
throw new InvalidParameterValueException(String.format("MAC address '%s' for NIC ID: %s is not a unicast address", mac, nic));
}
nicMacMap.put(nic, NetUtils.standardizeMacAddress(mac));
}
return nicMacMap;
}

public Map<String, Long> getDataDiskToDiskOfferingList() {
Map<String, Long> dataDiskToDiskOfferingMap = new HashMap<>();
if (MapUtils.isNotEmpty(dataDiskToDiskOfferingList)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.apache.cloudstack.api.command.admin.vm;

import java.util.LinkedHashMap;
import java.util.Map;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;

import com.cloud.exception.InvalidParameterValueException;
import com.cloud.vm.VmDetailConstants;

@RunWith(MockitoJUnitRunner.class)
public class ImportUnmanagedInstanceCmdTest {

private ImportUnmanagedInstanceCmd buildCmd(Map<?, ?> nicMacAddressList) {
ImportUnmanagedInstanceCmd cmd = new ImportUnmanagedInstanceCmd();
ReflectionTestUtils.setField(cmd, "nicMacAddressList", nicMacAddressList);
return cmd;
}

private Map<String, String> entry(String nic, String mac) {
Map<String, String> e = new LinkedHashMap<>();
if (nic != null) {
e.put(VmDetailConstants.NIC, nic);
}
if (mac != null) {
e.put(VmDetailConstants.NIC_MAC_ADDRESS, mac);
}
return e;
}

@Test
public void testGetNicMacAddressListNullReturnsEmpty() {
ImportUnmanagedInstanceCmd cmd = buildCmd(null);
Map<String, String> result = cmd.getNicMacAddressList();
Assert.assertNotNull(result);
Assert.assertTrue(result.isEmpty());
}

@Test
public void testGetNicMacAddressListEmptyMapReturnsEmpty() {
ImportUnmanagedInstanceCmd cmd = buildCmd(new LinkedHashMap<>());
Assert.assertTrue(cmd.getNicMacAddressList().isEmpty());
}

@Test
public void testGetNicMacAddressListValidMac() {
Map<Object, Object> outer = new LinkedHashMap<>();
outer.put("0", entry("nic1", "AA:BB:CC:DD:EE:FF"));
ImportUnmanagedInstanceCmd cmd = buildCmd(outer);

Map<String, String> result = cmd.getNicMacAddressList();

Assert.assertEquals(1, result.size());
// standardizeMacAddress lowercases the address
Assert.assertEquals("aa:bb:cc:dd:ee:ff", result.get("nic1"));
}

@Test
public void testGetNicMacAddressListMultipleNics() {
Map<Object, Object> outer = new LinkedHashMap<>();
outer.put("0", entry("nic1", "aa:bb:cc:dd:ee:01"));
outer.put("1", entry("nic2", "aa:bb:cc:dd:ee:02"));
ImportUnmanagedInstanceCmd cmd = buildCmd(outer);

Map<String, String> result = cmd.getNicMacAddressList();

Assert.assertEquals(2, result.size());
Assert.assertEquals("aa:bb:cc:dd:ee:01", result.get("nic1"));
Assert.assertEquals("aa:bb:cc:dd:ee:02", result.get("nic2"));
}

@Test(expected = InvalidParameterValueException.class)
public void testGetNicMacAddressListInvalidMacFormat() {
Map<Object, Object> outer = new LinkedHashMap<>();
outer.put("0", entry("nic1", "not-a-mac"));
buildCmd(outer).getNicMacAddressList();
}

@Test(expected = InvalidParameterValueException.class)
public void testGetNicMacAddressListBroadcastMacRejected() {
// FF:FF:FF:FF:FF:FF is a broadcast (non-unicast) address
Map<Object, Object> outer = new LinkedHashMap<>();
outer.put("0", entry("nic1", "FF:FF:FF:FF:FF:FF"));
buildCmd(outer).getNicMacAddressList();
}

@Test(expected = InvalidParameterValueException.class)
public void testGetNicMacAddressListMulticastMacRejected() {
// Bit 0 of first octet set → multicast, not unicast
Map<Object, Object> outer = new LinkedHashMap<>();
outer.put("0", entry("nic1", "01:bb:cc:dd:ee:ff"));
buildCmd(outer).getNicMacAddressList();
}

@Test(expected = InvalidParameterValueException.class)
public void testGetNicMacAddressListEmptyNicIdRejected() {
Map<Object, Object> outer = new LinkedHashMap<>();
outer.put("0", entry("", "aa:bb:cc:dd:ee:ff"));
buildCmd(outer).getNicMacAddressList();
}

@Test(expected = InvalidParameterValueException.class)
public void testGetNicMacAddressListEmptyMacRejected() {
Map<Object, Object> outer = new LinkedHashMap<>();
outer.put("0", entry("nic1", ""));
buildCmd(outer).getNicMacAddressList();
}

@Test(expected = InvalidParameterValueException.class)
public void testGetNicMacAddressListNullMacRejected() {
Map<Object, Object> outer = new LinkedHashMap<>();
// entry without mac key
Map<String, String> e = new LinkedHashMap<>();
e.put(VmDetailConstants.NIC, "nic1");
outer.put("0", e);
buildCmd(outer).getNicMacAddressList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -847,15 +847,49 @@ private Pair<DiskProfile, StoragePool> importDisk(UnmanagedInstanceTO.Disk disk,
return new Pair<DiskProfile, StoragePool>(profile, storagePool);
}

private NicProfile importNic(UnmanagedInstanceTO.Nic nic, VirtualMachine vm, Network network, Network.IpAddresses ipAddresses, int deviceId, boolean isDefaultNic, boolean forced) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException {
protected NicProfile importNic(UnmanagedInstanceTO.Nic nic, VirtualMachine vm, Network network, Network.IpAddresses ipAddresses, int deviceId, boolean isDefaultNic, boolean forced) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException {
// Prefer caller-supplied MAC (from nicmacaddresslist); fall back to hypervisor-reported MAC
String macAddress = (ipAddresses != null && StringUtils.isNotEmpty(ipAddresses.getMacAddress()))
? ipAddresses.getMacAddress()
: nic.getMacAddress();
DataCenterVO dataCenterVO = dataCenterDao.findById(network.getDataCenterId());
Pair<NicProfile, Integer> result = networkOrchestrationService.importNic(nic.getMacAddress(), deviceId, network, isDefaultNic, vm, ipAddresses, dataCenterVO, forced);
Pair<NicProfile, Integer> result = networkOrchestrationService.importNic(macAddress, deviceId, network, isDefaultNic, vm, ipAddresses, dataCenterVO, forced);
if (result == null) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("NIC ID: %s import failed", nic.getNicId()));
}
return result.first();
}


/**
* Merges caller-supplied MAC addresses into the NIC IP address map.
* For each NIC entry in {@code nicMacAddressMap}, the MAC address is set on the corresponding
* {@link Network.IpAddresses} object. If no IP address entry exists for a given NIC, a new
* {@link Network.IpAddresses} object is created to carry the MAC.
*
* @param nicIpAddressMap map of NIC ID to IP addresses (may be empty, never null after cmd getter)
* @param nicMacAddressMap map of NIC ID to validated/standardized MAC address strings
* @return merged map with MAC addresses populated where supplied by the caller
*/
protected Map<String, Network.IpAddresses> mergeNicMacAddresses(
final Map<String, Network.IpAddresses> nicIpAddressMap,
final Map<String, String> nicMacAddressMap) {
if (MapUtils.isEmpty(nicMacAddressMap)) {
return nicIpAddressMap;
}
Map<String, Network.IpAddresses> merged = new HashMap<>(nicIpAddressMap);
for (Map.Entry<String, String> entry : nicMacAddressMap.entrySet()) {
String nicId = entry.getKey();
String mac = entry.getValue();
Network.IpAddresses existing = merged.get(nicId);
if (existing != null) {
existing.setMacAddress(mac);
} else {
merged.put(nicId, new Network.IpAddresses(null, null, mac));
}
}
return merged;
}
private void cleanupFailedImportVM(final UserVm userVm) {
if (userVm == null) {
return;
Expand Down Expand Up @@ -1315,7 +1349,7 @@ private UserVmResponse baseImportInstance(ImportUnmanagedInstanceCmd cmd) {

checkVmwareInstanceNameForImportInstance(cluster.getHypervisorType(), instanceName, hostName, zone);
final Map<String, Long> nicNetworkMap = cmd.getNicNetworkList();
final Map<String, Network.IpAddresses> nicIpAddressMap = cmd.getNicIpAddressList();
final Map<String, Network.IpAddresses> nicIpAddressMap = mergeNicMacAddresses(cmd.getNicIpAddressList(), cmd.getNicMacAddressList());
final Map<String, Long> dataDiskOfferingMap = cmd.getDataDiskToDiskOfferingList();
final Map<String, String> details = cmd.getDetails();
final boolean forced = cmd.isForced();
Expand Down Expand Up @@ -2600,7 +2634,7 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) {
}

final Map<String, Long> nicNetworkMap = cmd.getNicNetworkList();
final Map<String, Network.IpAddresses> nicIpAddressMap = cmd.getNicIpAddressList();
final Map<String, Network.IpAddresses> nicIpAddressMap = mergeNicMacAddresses(cmd.getNicIpAddressList(), cmd.getNicMacAddressList());
final Map<String, Long> dataDiskOfferingMap = cmd.getDataDiskToDiskOfferingList();
final Map<String, String> details = cmd.getDetails();

Expand Down
Loading
Loading