diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index f0640abf879..fc450e9179c 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -92,6 +92,8 @@ public interface AccountService { Account getAccount(long accountId); + Account getAccountByUuid(String accountUuid); + User getActiveUser(long userId); User getOneActiveUserForAccount(Account account); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java index 4387bdd6e05..94300ed0381 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java @@ -127,6 +127,7 @@ import org.apache.cloudstack.veeam.api.dto.VmAction; import org.apache.cloudstack.veeam.api.dto.VnicProfile; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; @@ -192,6 +193,7 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.NicVO; import com.cloud.vm.UserVmManager; import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; @@ -207,6 +209,7 @@ public class ServerAdapter extends ManagerBase { ); private static final String VM_TA_KEY = "veeam_tag"; private static final String WORKER_VM_GUEST_CPU_MODE = "host-passthrough"; + private static final String RESTORE_CONFIG = "restore.config"; @Inject AccountService accountService; @@ -512,7 +515,7 @@ public class ServerAdapter extends ManagerBase { return template; } - protected Vm createInstance(com.cloud.dc.DataCenter zone, Long clusterId, Account owner, Long domainId, + protected Pair createInstance(com.cloud.dc.DataCenter zone, Long clusterId, Account owner, Long domainId, String accountName, Long projectId, String name, String displayName, String serviceOfferingUuid, int cpu, int memory, String templateUuid, String userdata, ApiConstants.BootType bootType, ApiConstants.BootMode bootMode, String affinityGroupId, String userDataId, Map details) { @@ -582,8 +585,10 @@ public class ServerAdapter extends ManagerBase { UserVm vm = userVmManager.createVirtualMachine(cmd); vm = userVmManager.finalizeCreateVirtualMachine(vm.getId()); UserVmJoinVO vo = userVmJoinDao.findById(vm.getId()); - return UserVmJoinVOToVmConverter.toVm(vo, this::getHostById, this::getDetailsByInstanceId, - this::listTagsByInstanceId, this::listDiskAttachmentsByInstanceId, this::listNicsByInstance, false); + Vm vmObj = UserVmJoinVOToVmConverter.toVm(vo, this::getHostById, this::getDetailsByInstanceId, + this::listTagsByInstanceId, this::listDiskAttachmentsByInstanceId, this::listNicsByInstance, + false); + return new Pair<>(vmObj, vm); } catch (InsufficientCapacityException | ResourceUnavailableException | ResourceAllocationException | CloudRuntimeException e) { throw new CloudRuntimeException("Failed to create VM: " + e.getMessage(), e); } @@ -618,6 +623,63 @@ public class ServerAdapter extends ManagerBase { return details; } + protected void saveInstanceRestoreConfig(Vm request, UserVm vm) { + if (StringUtils.isBlank(request.getAccountId())) { + return; + } + if (accountService.getAccountByUuid(request.getAccountId()) == null) { + return; + } + String restoreConfig = OvfXmlUtil.getConfigMetadataXml(request, logger); + if (StringUtils.isBlank(restoreConfig)) { + return; + } + vmInstanceDetailsDao.addDetail(vm.getId(), RESTORE_CONFIG, restoreConfig, false); + } + + protected void removeInstanceRestoreConfig(UserVm vm) { + vmInstanceDetailsDao.removeDetail(vm.getId(), RESTORE_CONFIG); + } + + protected Pair getValidatedInstanceNicDetails(final UserVmVO vm, final NetworkVO network) { + if (ObjectUtils.anyNull(vm, network)) { + return new Pair<>(null, null); + } + VMInstanceDetailVO detail = vmInstanceDetailsDao.findDetail(vm.getId(), RESTORE_CONFIG); + if (detail == null || StringUtils.isBlank(detail.getValue())) { + return new Pair<>(null, null); + } + Pair result = OvfXmlUtil.getVmNicDetailFromStoredConfig(detail.getValue(), network.getUuid(), logger); + String mac = StringUtils.trimToNull(result.first()); + String ip4Address = StringUtils.trimToNull(result.second()); + NicVO nic = null; + if (mac != null) { + nic = nicDao.findByNetworkIdAndMacAddress(network.getId(), mac); + if (nic != null) { + logger.warn("MAC address {} specified in the restore config for {} is already in use by {}, ignoring it", + mac, network, nic); + mac = null; + if (!Objects.equals(ip4Address, nic.getIPv4Address())) { + nic = null; + } + } + } + if (ip4Address != null) { + if (nic == null) { + nic = nicDao.findNonPlaceHolderByIp4AddressAndNetworkId(ip4Address, network.getId()); + } + if (nic != null) { + logger.warn("IPv4 address {} specified in the restore config for {} is already in use by {}, ignoring it", + ip4Address, network, nic); + mac = null; + if (Objects.equals(ip4Address, nic.getIPv4Address())) { + ip4Address = null; + } + } + } + return new Pair<>(mac, ip4Address); + } + protected static long getProvisionedSizeInGb(String sizeStr) { long provisionedSizeInGb; try { @@ -968,10 +1030,12 @@ public class ServerAdapter extends ManagerBase { if (request.getTemplate() != null && StringUtils.isNotEmpty(request.getTemplate().getId())) { templateUuid = request.getTemplate().getId(); } - return createInstance(zone, clusterId, owner, ownerDetails.first(), ownerDetails.second(), + Pair result = createInstance(zone, clusterId, owner, ownerDetails.first(), ownerDetails.second(), ownerDetails.third(), name, displayName, serviceOfferingUuid, cpu, memoryMB, templateUuid, userdata, bootOptions.first(), bootOptions.second(), request.getAffinityGroupId(), request.getUserDataId(), request.getDetails()); + saveInstanceRestoreConfig(request, result.second()); + return result.first(); } @ApiAccess(command = UpdateVMCmd.class) @@ -1175,6 +1239,7 @@ public class ServerAdapter extends ManagerBase { } accountService.checkAccess(CallContext.current().getCallingAccount(), SecurityChecker.AccessType.OperateEntry, false, vmVo); + removeInstanceRestoreConfig(vmVo); if (vmVo.getAccountId() != volumeVO.getAccountId()) { if (VeeamControlService.InstanceRestoreAssignOwner.value()) { assignVolumeToAccount(volumeVO, vmVo.getAccountId()); @@ -1296,10 +1361,13 @@ public class ServerAdapter extends ManagerBase { accountCannotAccessNetwork(networkVO, vmVo.getAccountId())) { assignVmToAccount(vmVo, networkVO.getAccountId()); } + Pair nicDetails = getValidatedInstanceNicDetails(vmVo, networkVO); AddNicToVMCmd cmd = new AddNicToVMCmd(); ComponentContext.inject(cmd); cmd.setVmId(vmVo.getId()); cmd.setNetworkId(networkVO.getId()); + cmd.setMacAddress(nicDetails.first()); + cmd.setIpaddr(nicDetails.second()); if (request.getMac() != null && StringUtils.isNotBlank(request.getMac().getAddress())) { cmd.setMacAddress(request.getMac().getAddress()); } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/NicVOToNicConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/NicVOToNicConverter.java index b55201327ea..3af2f7c3139 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/NicVOToNicConverter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/NicVOToNicConverter.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.veeam.api.converter; +import java.util.ArrayList; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; @@ -79,23 +80,34 @@ public class NicVOToNicConverter { device.setDescription(String.format("%s device", vo.getReserver())); device.setMac(mac); if (ObjectUtils.anyNotNull(vo.getIPv4Address(), vo.getIPv6Address())) { - Ip ip = new Ip(); - if (vo.getIPv4Address() != null) { - ip.setAddress(vo.getIPv4Address()); - ip.setGateway(vo.getIPv4Gateway()); - ip.setVersion("v4"); - } else if (vo.getIPv6Address() != null) { - ip.setAddress(vo.getIPv6Address()); - ip.setGateway(vo.getIPv6Gateway()); - ip.setVersion("v6"); - } - device.setIps(NamedList.of("ip", List.of(ip))); + List ips = getIps(vo); + device.setIps(NamedList.of("ip", ips)); } device.setHref(vm.getHref() + "/reporteddevices/" + vo.getUuid()); device.setVm(vm); return device; } + @NotNull + private static List getIps(NicVO vo) { + List ips = new ArrayList<>(); + if (vo.getIPv4Address() != null) { + Ip ip = new Ip(); + ip.setAddress(vo.getIPv4Address()); + ip.setGateway(vo.getIPv4Gateway()); + ip.setVersion("v4"); + ips.add(ip); + } + if (vo.getIPv6Address() != null) { + Ip ip6 = new Ip(); + ip6.setAddress(vo.getIPv6Address()); + ip6.setGateway(vo.getIPv6Gateway()); + ip6.setVersion("v6"); + ips.add(ip6); + } + return ips; + } + public static List toNicList(final List vos, final String vmUuid, final Function networkResolver) { return vos.stream() .map(vo -> toNic(vo, vmUuid, networkResolver)) diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java index 2c06b83de40..0eafdc824fa 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.veeam.api.dto; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Date; @@ -32,6 +33,7 @@ import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; @@ -41,11 +43,15 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; import com.cloud.api.query.vo.UserVmJoinVO; +import com.cloud.utils.Pair; public class OvfXmlUtil { @@ -108,8 +114,8 @@ public class OvfXmlUtil { final String bootTime = vm.getStartTime() != null ? formatDate(vm.getStartTime()) : creationDate; // Memory: Vm.memory is bytes (string) - final long memBytes = parseLong(vm.getMemory(), 1024L * 1024L * 1024L); - final long memMb = Math.max(128, memBytes / (1024L * 1024L)); + final long memBytes = parseLong(vm.getMemory(), MemoryAllocationUnit.Gigabytes.getBytesMultiplier()); + final long memMb = Math.max(128, memBytes / MemoryAllocationUnit.Megabytes.getBytesMultiplier()); // CPU: topology cores/sockets/threads. We default sockets=1 threads=1. final int vcpu = Math.max(1, Integer.parseInt(vm.getCpu().getTopology().getCores())); @@ -126,6 +132,8 @@ public class OvfXmlUtil { // Snapshot id (stable per VM id) final String snapshotId = UUID.nameUUIDFromBytes(("ovf-snap-" + vmId).getBytes(StandardCharsets.UTF_8)).toString(); + final List nics = nics(vm); + final StringBuilder sb = new StringBuilder(16_384); sb.append(""); sb.append("").append(escapeText(vo.getAffinityGroupUuid())).append(""); } + if (vm.getNics() != null && CollectionUtils.isNotEmpty(vm.getNics().getItems())) { + sb.append(""); + for (Nic nic : nics(vm)) { + if (nic == null || StringUtils.isBlank(nic.getId())) { + continue; + } + String networkId = nicNetworkId(nic); + if (networkId == null) { + continue; + } + sb.append(""); + sb.append("").append(escapeText(nic.getId())).append(""); + sb.append("").append(escapeText(networkId)).append(""); + sb.append("").append(escapeText(nicMac(nic))).append(""); + sb.append("").append(escapeText(nicIp(nic, "v4"))).append(""); + sb.append("").append(escapeText(nicIp(nic, "v6"))).append(""); + sb.append(""); + } + sb.append(""); + } sb.append(""); sb.append(""); } @@ -349,7 +377,7 @@ public class OvfXmlUtil { if (da == null || da.getDisk() == null || StringUtils.isBlank(da.getDisk().getId())) { continue; } - final org.apache.cloudstack.veeam.api.dto.Disk d = da.getDisk(); + final Disk d = da.getDisk(); final String diskId = d.getId(); final String storageDomainId = firstStorageDomainId(d); final String href = storageDomainId + "/" + diskId; @@ -380,16 +408,17 @@ public class OvfXmlUtil { // NICs as Items int nicSlot = 0; - for (Nic nic : nics(vm)) { + for (Nic nic : nics) { if (nic == null) { continue; } final String nicId = firstNonBlank(nic.getId(), UUID.nameUUIDFromBytes(("nic-" + vmId + "-" + nicSlot).getBytes(StandardCharsets.UTF_8)).toString()); final String nicName = firstNonBlank(nic.getName(), "nic" + (nicSlot + 1)); final String mac = nic.getMac() != null ? defaultString(nic.getMac().getAddress()) : ""; + final String elementName = nic.getVnicProfile() != null ? defaultString(nic.getVnicProfile().getId()) : nicName; sb.append(""); - sb.append("Ethernet adapter on [No Network]"); + sb.append("Ethernet adapter - ").append(nic.getName()).append(""); sb.append("").append(escapeText(nicId)).append(""); sb.append("10"); sb.append(""); @@ -397,7 +426,7 @@ public class OvfXmlUtil { sb.append("").append(escapeText(defaultString(inferNetworkName(nic)))).append(""); sb.append("").append(escapeText(booleanString(nic.getLinked(), "true"))).append(""); sb.append("").append(escapeText(nicName)).append(""); - sb.append("").append(escapeText(nicName)).append(""); + sb.append("").append(escapeText(elementName)).append(""); sb.append("").append(escapeText(mac)).append(""); sb.append("10000"); sb.append("interface"); @@ -441,16 +470,153 @@ public class OvfXmlUtil { return sb.toString(); } - public static void updateFromConfiguration(Vm vm) { + protected static String getVmConfigurationData(Vm vm) { Vm.Initialization initialization = vm.getInitialization(); if (initialization == null) { - return; + return null; } Vm.Initialization.Configuration configuration = vm.getInitialization().getConfiguration(); if (configuration == null) { + return null; + } + return configuration.getData(); + } + + public static void updateFromConfiguration(Vm vm) { + String configurationData = getVmConfigurationData(vm); + OvfXmlUtil.updateFromXml(vm, configurationData); + } + + public static String getConfigMetadataXml(Vm vm, Logger logger) { + String configurationData = getVmConfigurationData(vm); + if (StringUtils.isBlank(configurationData)) { + return null; + } + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(new ByteArrayInputStream(configurationData.getBytes(StandardCharsets.UTF_8))); + + XPathFactory xpf = XPathFactory.newInstance(); + XPath xpath = xpf.newXPath(); + + // Persist only the CloudStack metadata section from the source OVF. + Node metadataSection = (Node) xpath.evaluate( + "//*[local-name()='Section' and @*[local-name()='type']='ovf:CloudStackMetadata_Type']", + doc, + XPathConstants.NODE + ); + + if (metadataSection == null) { + return null; + } + + // Wrap section payload so it remains standalone XML with namespace declarations. + StringBuilder sb = new StringBuilder(2048); + sb.append(""); + sb.append(""); + sb.append(nodeToString(metadataSection)); + sb.append(""); + + return sb.toString(); + } catch (ParserConfigurationException | XPathExpressionException | IOException | SAXException e) { + logger.error("Failed to parse VM configuration data for VM id {}: {}", vm.getId(), e.getMessage()); + return null; + } + } + + public static Pair getVmNicDetailFromStoredConfig(String xmlConfig, String networkId, Logger logger) { + if (StringUtils.isAnyBlank(xmlConfig, networkId)) { + return new Pair<>(null, null); + } + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(new ByteArrayInputStream(xmlConfig.getBytes(StandardCharsets.UTF_8))); + + XPathFactory xpf = XPathFactory.newInstance(); + XPath xpath = xpf.newXPath(); + + // Preferred format: CloudStack metadata section with CloudStack/Nics/Nic records. + NodeList nicNodes = (NodeList) xpath.evaluate( + "//*[local-name()='Section' and @*[local-name()='type']='ovf:CloudStackMetadata_Type']/*[local-name()='CloudStack']/*[local-name()='Nics']/*[local-name()='Nic']", + doc, + XPathConstants.NODESET + ); + if (nicNodes != null && nicNodes.getLength() > 0) { + for (int i = 0; i < nicNodes.getLength(); i++) { + Node nicNode = nicNodes.item(i); + String nicNetworkId = xpathString(xpath, nicNode, "./*[local-name()='NetworkId']/text()"); + if (StringUtils.equals(nicNetworkId, networkId)) { + return new Pair<>( + xpathString(xpath, nicNode, "./*[local-name()='MACAddress' or local-name()='MACAddress']/text()"), + xpathString(xpath, nicNode, "./*[local-name()='Ip4Address' or local-name()='Ip4Address']/text()") + ); + } + } + } + } catch (ParserConfigurationException | XPathExpressionException | IOException | SAXException e) { + logger.error("Failed to parse VM configuration XML to retrieve details for NIC for network ID {}: {}", + networkId, e.getMessage()); + } + return new Pair<>(null, null); + } + + private static String nodeToString(Node node) { + try { + // Implementation using string manipulation + StringBuilder sb = new StringBuilder(); + serializeNodeToString(node, sb); + return sb.toString(); + } catch (Exception e) { + return ""; + } + } + + private static void serializeNodeToString(Node node, StringBuilder sb) { + if (node == null) { return; } - OvfXmlUtil.updateFromXml(vm, configuration.getData()); + + short nodeType = node.getNodeType(); + switch (nodeType) { + case Node.ELEMENT_NODE: + sb.append("<").append(node.getNodeName()); + NamedNodeMap attrs = node.getAttributes(); + if (attrs != null) { + for (int i = 0; i < attrs.getLength(); i++) { + Node attr = attrs.item(i); + sb.append(" ").append(attr.getNodeName()).append("=\"") + .append(escapeAttr(attr.getNodeValue())).append("\""); + } + } + sb.append(">"); + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + serializeNodeToString(children.item(i), sb); + } + sb.append(""); + break; + case Node.TEXT_NODE: + String text = node.getNodeValue(); + if (StringUtils.isNotBlank(text)) { + sb.append(escapeText(text)); + } + break; + case Node.CDATA_SECTION_NODE: + sb.append(""); + break; + default: + break; + } } protected static void updateFromXml(Vm vm, String ovfXml) { @@ -663,6 +829,40 @@ public class OvfXmlUtil { return vm.getNics().getItems(); } + private static String nicNetworkId(Nic nic) { + if (nic == null || nic.getVnicProfile() == null || StringUtils.isEmpty(nic.getVnicProfile().getId())) { + return null; + } + return nic.getVnicProfile().getId(); + } + + private static ReportedDevice getNicReportedDevice(Nic nic) { + if (nic == null || nic.getReportedDevices() == null || CollectionUtils.isEmpty(nic.getReportedDevices().getItems())) { + return null; + } + return nic.getReportedDevices().getItems().get(0); + } + + private static String nicMac(Nic nic) { + if (nic == null || nic.getMac() == null || StringUtils.isBlank(nic.getMac().getAddress())) { + return ""; + } + return nic.getMac().getAddress(); + } + + private static String nicIp(Nic nic, String version) { + ReportedDevice device = getNicReportedDevice(nic); + if (device == null || device.getIps() == null || CollectionUtils.isEmpty(device.getIps().getItems())) { + return ""; + } + for (Ip ip : device.getIps().getItems()) { + if (version.equalsIgnoreCase(ip.getVersion())) { + return ip.getAddress(); + } + } + return ""; + } + private static String inferOsDescription(Vm vm) { if (vm.getOs() == null) { return "other"; @@ -740,7 +940,7 @@ public class OvfXmlUtil { if (vm.getMemoryPolicy() == null || vm.getMemoryPolicy().getBallooning() == null) { return "true"; } - return "true".equalsIgnoreCase(vm.getMemoryPolicy().getBallooning()) ? "true" : "false"; + return Boolean.toString("true".equalsIgnoreCase(vm.getMemoryPolicy().getBallooning())); } private static int mapNicResourceSubType(String iface) { @@ -795,7 +995,7 @@ public class OvfXmlUtil { if (bytes <= 0) { return 0; } - final long gib = 1024L * 1024L * 1024L; + final long gib = MemoryAllocationUnit.Gigabytes.getBytesMultiplier(); return (bytes + gib - 1) / gib; } diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java index 0faf1bfebd2..027d6e09160 100644 --- a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java @@ -48,14 +48,17 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.veeam.api.dto.DataCenter; import org.apache.cloudstack.veeam.api.dto.Disk; +import org.apache.cloudstack.veeam.api.dto.OvfXmlUtil; import org.apache.cloudstack.veeam.api.dto.Tag; import org.apache.cloudstack.veeam.api.dto.Vm; +import org.apache.logging.log4j.Logger; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -96,10 +99,14 @@ import com.cloud.user.User; import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.NicVO; import com.cloud.vm.UserVmManager; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.UserVmVO; import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.snapshot.dao.VMSnapshotDao; @RunWith(MockitoJUnitRunner.class) @@ -117,6 +124,7 @@ public class ServerAdapterTest { @Mock NetworkDao networkDao; @Mock UserVmDao userVmDao; @Mock UserVmJoinDao userVmJoinDao; + @Mock VMInstanceDetailsDao vmInstanceDetailsDao; @Mock VolumeDao volumeDao; @Mock VolumeJoinDao volumeJoinDao; // kept minimal: only mocks used directly by tests @@ -126,6 +134,7 @@ public class ServerAdapterTest { @Mock ServiceOfferingDao serviceOfferingDao; @Mock VMTemplateDao templateDao; @Mock UserVmManager userVmManager; + @Mock NicDao nicDao; @Mock AsyncJobDao asyncJobDao; @Mock AsyncJobJoinDao asyncJobJoinDao; @Mock VMSnapshotDao vmSnapshotDao; @@ -266,6 +275,185 @@ public class ServerAdapterTest { assertEquals("3000", result.get(VmDetailConstants.CPU_SPEED)); } + @Test + public void testGetValidatedInstanceNicDetails_NullVm_ReturnsNullPair() { + NetworkVO network = mock(NetworkVO.class); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(null, network); + + assertNull(result.first()); + assertNull(result.second()); + } + + @Test + public void testGetValidatedInstanceNicDetails_NullNetwork_ReturnsNullPair() { + UserVmVO vm = mock(UserVmVO.class); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(vm, null); + + assertNull(result.first()); + assertNull(result.second()); + } + + @Test + public void testGetValidatedInstanceNicDetails_NoRestoreConfig_ReturnsNullPair() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(10L); + when(vmInstanceDetailsDao.findDetail(10L, "restore.config")).thenReturn(null); + NetworkVO network = mock(NetworkVO.class); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(vm, network); + + assertNull(result.first()); + assertNull(result.second()); + } + + @Test + public void testGetValidatedInstanceNicDetails_BlankRestoreConfig_ReturnsNullPair() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(10L); + VMInstanceDetailVO detail = mock(VMInstanceDetailVO.class); + when(detail.getValue()).thenReturn(" "); + when(vmInstanceDetailsDao.findDetail(10L, "restore.config")).thenReturn(detail); + NetworkVO network = mock(NetworkVO.class); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(vm, network); + + assertNull(result.first()); + assertNull(result.second()); + } + + @Test + public void testGetValidatedInstanceNicDetails_BlankMacAndIpFromConfig_ReturnsNullPair() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(11L); + VMInstanceDetailVO detail = mock(VMInstanceDetailVO.class); + when(detail.getValue()).thenReturn("restore-xml"); + when(vmInstanceDetailsDao.findDetail(11L, "restore.config")).thenReturn(detail); + + NetworkVO network = mock(NetworkVO.class); + when(network.getUuid()).thenReturn("network-uuid"); + + try (MockedStatic ovfXmlUtil = Mockito.mockStatic(OvfXmlUtil.class)) { + ovfXmlUtil.when(() -> OvfXmlUtil.getVmNicDetailFromStoredConfig(eq("restore-xml"), eq("network-uuid"), any(Logger.class))) + .thenReturn(new Pair<>(" ", "\t")); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(vm, network); + + assertNull(result.first()); + assertNull(result.second()); + } + } + + @Test + public void testGetValidatedInstanceNicDetails_NoConflicts_ReturnsMacAndIp() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(20L); + VMInstanceDetailVO detail = mock(VMInstanceDetailVO.class); + when(detail.getValue()).thenReturn("restore-xml"); + when(vmInstanceDetailsDao.findDetail(20L, "restore.config")).thenReturn(detail); + + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(30L); + when(network.getUuid()).thenReturn("network-uuid"); + + when(nicDao.findByNetworkIdAndMacAddress(30L, "02:00:00:00:00:01")).thenReturn(null); + when(nicDao.findNonPlaceHolderByIp4AddressAndNetworkId("10.0.0.10", 30L)).thenReturn(null); + + try (MockedStatic ovfXmlUtil = Mockito.mockStatic(OvfXmlUtil.class)) { + ovfXmlUtil.when(() -> OvfXmlUtil.getVmNicDetailFromStoredConfig(eq("restore-xml"), eq("network-uuid"), any(Logger.class))) + .thenReturn(new Pair<>("02:00:00:00:00:01", "10.0.0.10")); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(vm, network); + + assertEquals("02:00:00:00:00:01", result.first()); + assertEquals("10.0.0.10", result.second()); + } + } + + @Test + public void testGetValidatedInstanceNicDetails_MacConflictWithSameIp_ClearsBoth() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(21L); + VMInstanceDetailVO detail = mock(VMInstanceDetailVO.class); + when(detail.getValue()).thenReturn("restore-xml"); + when(vmInstanceDetailsDao.findDetail(21L, "restore.config")).thenReturn(detail); + + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(31L); + when(network.getUuid()).thenReturn("network-uuid"); + + NicVO conflictingNic = mock(NicVO.class); + when(conflictingNic.getIPv4Address()).thenReturn("10.0.0.11"); + when(nicDao.findByNetworkIdAndMacAddress(31L, "02:00:00:00:00:02")).thenReturn(conflictingNic); + + try (MockedStatic ovfXmlUtil = Mockito.mockStatic(OvfXmlUtil.class)) { + ovfXmlUtil.when(() -> OvfXmlUtil.getVmNicDetailFromStoredConfig(eq("restore-xml"), eq("network-uuid"), any(Logger.class))) + .thenReturn(new Pair<>("02:00:00:00:00:02", "10.0.0.11")); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(vm, network); + + assertNull(result.first()); + assertNull(result.second()); + } + } + + @Test + public void testGetValidatedInstanceNicDetails_MacConflictWithDifferentIp_ClearsOnlyMac() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(22L); + VMInstanceDetailVO detail = mock(VMInstanceDetailVO.class); + when(detail.getValue()).thenReturn("restore-xml"); + when(vmInstanceDetailsDao.findDetail(22L, "restore.config")).thenReturn(detail); + + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(32L); + when(network.getUuid()).thenReturn("network-uuid"); + + NicVO conflictingNic = mock(NicVO.class); + when(conflictingNic.getIPv4Address()).thenReturn("10.0.0.99"); + when(nicDao.findByNetworkIdAndMacAddress(32L, "02:00:00:00:00:03")).thenReturn(conflictingNic); + when(nicDao.findNonPlaceHolderByIp4AddressAndNetworkId("10.0.0.12", 32L)).thenReturn(null); + + try (MockedStatic ovfXmlUtil = Mockito.mockStatic(OvfXmlUtil.class)) { + ovfXmlUtil.when(() -> OvfXmlUtil.getVmNicDetailFromStoredConfig(eq("restore-xml"), eq("network-uuid"), any(Logger.class))) + .thenReturn(new Pair<>("02:00:00:00:00:03", "10.0.0.12")); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(vm, network); + + assertNull(result.first()); + assertEquals("10.0.0.12", result.second()); + } + } + + @Test + public void testGetValidatedInstanceNicDetails_IpConflict_ClearsIpAndMac() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(23L); + VMInstanceDetailVO detail = mock(VMInstanceDetailVO.class); + when(detail.getValue()).thenReturn("restore-xml"); + when(vmInstanceDetailsDao.findDetail(23L, "restore.config")).thenReturn(detail); + + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(33L); + when(network.getUuid()).thenReturn("network-uuid"); + + when(nicDao.findByNetworkIdAndMacAddress(33L, "02:00:00:00:00:04")).thenReturn(null); + NicVO conflictingIpNic = mock(NicVO.class); + when(conflictingIpNic.getIPv4Address()).thenReturn("10.0.0.13"); + when(nicDao.findNonPlaceHolderByIp4AddressAndNetworkId("10.0.0.13", 33L)).thenReturn(conflictingIpNic); + + try (MockedStatic ovfXmlUtil = Mockito.mockStatic(OvfXmlUtil.class)) { + ovfXmlUtil.when(() -> OvfXmlUtil.getVmNicDetailFromStoredConfig(eq("restore-xml"), eq("network-uuid"), any(Logger.class))) + .thenReturn(new Pair<>("02:00:00:00:00:04", "10.0.0.13")); + + Pair result = serverAdapter.getValidatedInstanceNicDetails(vm, network); + + assertNull(result.first()); + assertNull(result.second()); + } + } + @Test public void testGetDummyTags_ContainsRootTag() { diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtilTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtilTest.java index c4b6c3ba3ed..b96d85cec92 100644 --- a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtilTest.java +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtilTest.java @@ -18,11 +18,21 @@ package org.apache.cloudstack.veeam.api.dto; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.apache.logging.log4j.Logger; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.utils.Pair; + @RunWith(MockitoJUnitRunner.class) public class OvfXmlUtilTest { @@ -42,4 +52,29 @@ public class OvfXmlUtilTest { assertEquals("1", vm.getCpu().getTopology().getCores()); assertEquals("1", vm.getCpu().getTopology().getThreads()); } + + @Test + public void test_restoreConfig_parse() throws Exception { + Vm vm = mock(Vm.class); + Vm.Initialization initialization = mock(Vm.Initialization.class); + Vm.Initialization.Configuration configMock = mock(Vm.Initialization.Configuration.class); + when(initialization.getConfiguration()).thenReturn(configMock); + when(vm.getInitialization()).thenReturn(initialization); + String ovfXml; + try (InputStream is = getClass().getClassLoader().getResourceAsStream("test-ovf.xml")) { + assertNotNull(is); + ovfXml = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + when(configMock.getData()).thenReturn(ovfXml); + + String instanceConfig = OvfXmlUtil.getConfigMetadataXml(vm, mock(Logger.class)); + assertNotNull(instanceConfig); + assertTrue(instanceConfig.contains("ovf:CloudStackMetadata_Type")); + assertTrue(instanceConfig.contains("6965c1cf-8d44-4622-82e2-4dbbe4a58355")); + + Pair result = OvfXmlUtil.getVmNicDetailFromStoredConfig(instanceConfig, "6965c1cf-8d44-4622-82e2-4dbbe4a58355", mock(Logger.class)); + assertNotNull(result); + assertEquals("1e:01:50:00:00:fd", result.first()); + assertEquals("10.1.1.103", result.second()); + } } diff --git a/plugins/integrations/veeam-control-service/src/test/resources/test-ovf.xml b/plugins/integrations/veeam-control-service/src/test/resources/test-ovf.xml new file mode 100644 index 00000000000..ed22ca63239 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/resources/test-ovf.xml @@ -0,0 +1,225 @@ + + + + + + + + List of networks + + + + +
+ List of Virtual Disks + +
+
+ CloudStack specific metadata + + 048531b6-4386-11f1-930c-525400580bd0 + e0afbb58-4385-11f1-930c-525400580bd0 + + ac3edee0-e2fb-4cd1-b6d4-d3b4555203ba + + + 2efdaae2-6c38-4ac8-ac75-562cf47adba1 + 4de70249-b89d-4f08-9ca2-05ddb4ad1b2a + + +
+ + keyboard + us + + + skip.force.disk.controller + true + + + nicAdapter + virtio + + + rootDiskController + osdefault + +
+ + + 85990692-f734-4695-afe4-aca34a21f459 + 6aff2178-a323-4148-a592-edbd47b93229 + 02:01:00:cf:00:05 + 10.1.1.40 + + + +
+
+ + test-vm1 + test-vm1 + + 2026/05/06 18:29:58 + 2026/05/06 18:47:55 + false + guest_agent + false + 1 + Etc/GMT + 0 + 11 + 4.8 + 1 + AUTO_RESUME + 512 + false + false + false + 0 + 048531b6-4386-11f1-930c-525400580bd0 + 0 + false + true + true + false + LOCK_SCREEN + 0 + + 3 + + + + 512 + true + false + false + false + 0 + + 8c367ed8-03d9-4c02-a4a4-f20b796f1b56 + 8c367ed8-03d9-4c02-a4a4-f20b796f1b56 + true + 3 + 00000000-0000-0000-0000-000000000000 + 2 + false + 8c367ed8-03d9-4c02-a4a4-f20b796f1b56 + 8c367ed8-03d9-4c02-a4a4-f20b796f1b56 + false + 2026/05/06 18:29:58 + 2026/05/06 18:29:58 + 0 +
+ Guest Operating System + linux +
+
+ 1 CPU, 512 Memory + + ENGINE 4.4.0.0 + + + 1 virtual cpu + Number of virtual CPU + 1 + 3 + 1 + 1 + 1 + 1 + 1 + + + 512 MB of memory + Memory Size + 2 + 4 + byte * 2^20 + 512 + + + ROOT-27 + 2efdaae2-6c38-4ac8-ac75-562cf47adba1 + 17 + 6af2dd24-1af2-3610-b7b8-de38c98ec958/2efdaae2-6c38-4ac8-ac75-562cf47adba1 + 00000000-0000-0000-0000-000000000000 + 8c367ed8-03d9-4c02-a4a4-f20b796f1b56 + + 6af2dd24-1af2-3610-b7b8-de38c98ec958 + 00000000-0000-0000-0000-000000000000 + 2026/05/06 18:29:58 + 2026/05/06 18:47:55 + 2026/05/06 18:47:55 + disk + disk + {type=drive, bus=0, controller=0, target=0, unit=0} + 1 + true + false + ua-6af2dd24-1af2-3610-b7b8-de38c98ec958/2efdaae2-6c38-4ac8-ac75-562cf47adba1 + + + Ethernet adapter - ExternalGuestNetworkGuru + 85990692-f734-4695-afe4-aca34a21f459 + 10 + + 3 + Network-85990692-f734-4695-afe4-aca34a21f459 + true + ExternalGuestNetworkGuru + 6aff2178-a323-4148-a592-edbd47b93229 + 02:01:00:cf:00:05 + 10000 + interface + bridge + {type=pci, slot=0x00, bus=0x01, domain=0x0000, function=0x0} + 0 + true + false + ua-85990692-f734-4695-afe4-aca34a21f459 + + + USB Controller + 3 + 23 + DISABLED + + + 0 + 373a1bbf-b292-31c9-a29c-afeb9ba84c21 + rng + virtio + {type=pci, slot=0x00, bus=0x06, domain=0x0000, function=0x0} + 0 + true + false + + + urandom + + +
+
+
diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index 4ec96636235..a0b1973fd8e 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -627,4 +627,9 @@ public class MockAccountManager extends ManagerBase implements AccountManager { public User getOneActiveUserForAccount(Account account) { return null; } + + @Override + public Account getAccountByUuid(String accountUuid) { + return null; + } } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 2c24394647a..391890ef687 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -2789,6 +2789,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return _accountDao.findByIdIncludingRemoved(accountId); } + @Override + public Account getAccountByUuid(String accountUuid) { + return _accountDao.findByUuidIncludingRemoved(accountUuid); + } + @Override public RoleType getRoleType(Account account) { if (account == null) {