From bc2866567968f187ac1c2ea1293f5c50df00f917 Mon Sep 17 00:00:00 2001 From: Vishesh Date: Mon, 26 Aug 2024 14:23:42 +0530 Subject: [PATCH] Add support for network data in Config Drive (#9329) --- .../service/NetworkOrchestrationService.java | 3 + .../orchestration/NetworkOrchestrator.java | 26 +- .../configdrive/ConfigDriveBuilder.java | 178 +++++++++- .../storage/configdrive/ConfigDriveUtils.java | 54 +++ .../configdrive/ConfigDriveBuilderTest.java | 190 ++++++++++- .../configdrive/ConfigDriveUtilsTest.java | 108 ++++++ .../com/cloud/network/NetworkModelImpl.java | 13 +- .../element/ConfigDriveNetworkElement.java | 102 +++++- .../ConfigDriveNetworkElementTest.java | 29 +- .../com/cloud/vpc/MockNetworkManagerImpl.java | 6 + test/integration/smoke/test_network.py | 320 +++++++++++++++++- tools/marvin/marvin/config/test_data.py | 50 +++ 12 files changed, 1025 insertions(+), 54 deletions(-) create mode 100644 engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveUtils.java create mode 100644 engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveUtilsTest.java diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 41bd74f11924..84098bbc6541 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -21,6 +21,7 @@ import java.util.Map; import com.cloud.dc.DataCenter; +import com.cloud.hypervisor.Hypervisor; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.ConfigKey.Scope; @@ -144,6 +145,8 @@ void prepare(VirtualMachineProfile profile, DeployDestination dest, ReservationC List getNicProfiles(VirtualMachine vm); + List getNicProfiles(Long vmId, Hypervisor.HypervisorType hypervisorType); + Map getSystemVMAccessDetails(VirtualMachine vm); Pair implementNetwork(long networkId, DeployDestination dest, ReservationContext context) diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 5c01bb4f288a..ce4c6bab94a3 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -1835,6 +1835,19 @@ protected boolean prepareElement(final NetworkElement element, final Network net return false; } } + if (element instanceof ConfigDriveNetworkElement && (( + _networkModel.areServicesSupportedInNetwork(network.getId(), Service.Dhcp) && + _networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.Dhcp, element.getProvider()) + ) || ( + _networkModel.areServicesSupportedInNetwork(network.getId(), Service.Dns) && + _networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.Dns, element.getProvider()) + ) || ( + _networkModel.areServicesSupportedInNetwork(network.getId(), Service.UserData) && + _networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.UserData, element.getProvider()) + ))) { + final ConfigDriveNetworkElement sp = (ConfigDriveNetworkElement) element; + return sp.createConfigDriveIso(profile, vmProfile, dest, null); + } } return true; } @@ -4443,18 +4456,18 @@ private boolean getNicProfileDefaultNic(NicProfile nicProfile) { } @Override - public List getNicProfiles(final VirtualMachine vm) { - final List nics = _nicDao.listByVmId(vm.getId()); + public List getNicProfiles(final Long vmId, HypervisorType hypervisorType) { + final List nics = _nicDao.listByVmId(vmId); final List profiles = new ArrayList(); if (nics != null) { for (final Nic nic : nics) { final NetworkVO network = _networksDao.findById(nic.getNetworkId()); - final Integer networkRate = _networkModel.getNetworkRate(network.getId(), vm.getId()); + final Integer networkRate = _networkModel.getNetworkRate(network.getId(), vmId); final NetworkGuru guru = AdapterBase.getAdapterByName(networkGurus, network.getGuruName()); final NicProfile profile = new NicProfile(nic, network, nic.getBroadcastUri(), nic.getIsolationUri(), networkRate, - _networkModel.isSecurityGroupSupportedInNetwork(network), _networkModel.getNetworkTag(vm.getHypervisorType(), network)); + _networkModel.isSecurityGroupSupportedInNetwork(network), _networkModel.getNetworkTag(hypervisorType, network)); guru.updateNicProfile(profile, network); profiles.add(profile); } @@ -4462,6 +4475,11 @@ public List getNicProfiles(final VirtualMachine vm) { return profiles; } + @Override + public List getNicProfiles(final VirtualMachine vm) { + return getNicProfiles(vm.getId(), vm.getHypervisorType()); + } + @Override public Map getSystemVMAccessDetails(final VirtualMachine vm) { final Map accessDetails = new HashMap<>(); diff --git a/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java b/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java index e1d51120efa3..58cc341a87ba 100644 --- a/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java +++ b/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java @@ -22,6 +22,8 @@ import static com.cloud.network.NetworkModel.CONFIGDATA_FILE; import static com.cloud.network.NetworkModel.PASSWORD_FILE; import static com.cloud.network.NetworkModel.USERDATA_FILE; +import static com.cloud.network.NetworkService.DEFAULT_MTU; +import static org.apache.cloudstack.storage.configdrive.ConfigDriveUtils.mergeJsonArraysAndUpdateObject; import java.io.File; import java.io.IOException; @@ -33,6 +35,9 @@ import java.util.Map; import java.util.Set; +import com.cloud.network.Network; +import com.cloud.vm.NicProfile; +import com.googlecode.ipv6.IPv6Network; import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.MapUtils; import org.apache.commons.io.FileUtils; @@ -81,7 +86,7 @@ static void writeFile(File folder, String file, String content) { /** * Read the content of a {@link File} and convert it to a String in base 64. - * We expect the content of the file to be encoded using {@link StandardCharsets#US_ASC} + * We expect the content of the file to be encoded using {@link StandardCharsets#US_ASCII} */ public static String fileToBase64String(File isoFile) throws IOException { byte[] encoded = Base64.encodeBase64(FileUtils.readFileToByteArray(isoFile)); @@ -108,9 +113,9 @@ public static File base64StringToFile(String encodedIsoData, String folder, Stri * This method will build the metadata files required by OpenStack driver. Then, an ISO is going to be generated and returned as a String in base 64. * If vmData is null, we throw a {@link CloudRuntimeException}. Moreover, {@link IOException} are captured and re-thrown as {@link CloudRuntimeException}. */ - public static String buildConfigDrive(List vmData, String isoFileName, String driveLabel, Map customUserdataParams) { - if (vmData == null) { - throw new CloudRuntimeException("No VM metadata provided"); + public static String buildConfigDrive(List nics, List vmData, String isoFileName, String driveLabel, Map customUserdataParams, Map> supportedServices) { + if (vmData == null && nics == null) { + throw new CloudRuntimeException("No VM metadata and nic profile provided"); } Path tempDir = null; @@ -121,10 +126,19 @@ public static String buildConfigDrive(List vmData, String isoFileName, File openStackFolder = new File(tempDirName + ConfigDrive.openStackConfigDriveName); - writeVendorAndNetworkEmptyJsonFile(openStackFolder); - writeVmMetadata(vmData, tempDirName, openStackFolder, customUserdataParams); - - linkUserData(tempDirName); + writeVendorEmptyJsonFile(openStackFolder); + writeNetworkData(nics, supportedServices, openStackFolder); + for (NicProfile nic: nics) { + if (supportedServices.get(nic.getId()).contains(Network.Service.UserData)) { + if (vmData == null) { + throw new CloudRuntimeException("No VM metadata provided"); + } + writeVmMetadata(vmData, tempDirName, openStackFolder, customUserdataParams); + + linkUserData(tempDirName); + break; + } + } return generateAndRetrieveIsoAsBase64Iso(isoFileName, driveLabel, tempDirName); } catch (IOException e) { @@ -212,18 +226,36 @@ static void writeVmMetadata(List vmData, String tempDirName, File open } /** - * Writes the following empty JSON files: - *
    - *
  • vendor_data.json - *
  • network_data.json - *
+ * First we generate a JSON object using {@link #getNetworkDataJsonObjectForNic(NicProfile, List)}, then we write it to a file called "network_data.json". + */ + static void writeNetworkData(List nics, Map> supportedServices, File openStackFolder) { + JsonObject finalNetworkData = new JsonObject(); + if (needForGeneratingNetworkData(supportedServices)) { + for (NicProfile nic : nics) { + List supportedService = supportedServices.get(nic.getId()); + JsonObject networkData = getNetworkDataJsonObjectForNic(nic, supportedService); + + mergeJsonArraysAndUpdateObject(finalNetworkData, networkData, "links", "id", "type"); + mergeJsonArraysAndUpdateObject(finalNetworkData, networkData, "networks", "id", "type"); + mergeJsonArraysAndUpdateObject(finalNetworkData, networkData, "services", "address", "type"); + } + } + + writeFile(openStackFolder, "network_data.json", finalNetworkData.toString()); + } + + static boolean needForGeneratingNetworkData(Map> supportedServices) { + return supportedServices.values().stream().anyMatch(services -> services.contains(Network.Service.Dhcp) || services.contains(Network.Service.Dns)); + } + + /** + * Writes an empty JSON file named vendor_data.json in openStackFolder * - * If the folder does not exist and we cannot create it, we throw a {@link CloudRuntimeException}. + * If the folder does not exist, and we cannot create it, we throw a {@link CloudRuntimeException}. */ - static void writeVendorAndNetworkEmptyJsonFile(File openStackFolder) { + static void writeVendorEmptyJsonFile(File openStackFolder) { if (openStackFolder.exists() || openStackFolder.mkdirs()) { writeFile(openStackFolder, "vendor_data.json", "{}"); - writeFile(openStackFolder, "network_data.json", "{}"); } else { throw new CloudRuntimeException("Failed to create folder " + openStackFolder); } @@ -250,6 +282,120 @@ static JsonObject createJsonObjectWithVmData(List vmData, String tempD return metaData; } + /** + * Creates the {@link JsonObject} using @param nic's metadata. We expect the JSONObject to have the following entries: + *
    + *
  • links
  • + *
  • networks
  • + *
  • services
  • + *
+ */ + static JsonObject getNetworkDataJsonObjectForNic(NicProfile nic, List supportedServices) { + JsonObject networkData = new JsonObject(); + + JsonArray links = getLinksJsonArrayForNic(nic); + JsonArray networks = getNetworksJsonArrayForNic(nic); + if (links.size() > 0) { + networkData.add("links", links); + } + if (networks.size() > 0) { + networkData.add("networks", networks); + } + + JsonArray services = getServicesJsonArrayForNic(nic); + if (services.size() > 0) { + networkData.add("services", services); + } + + return networkData; + } + + static JsonArray getLinksJsonArrayForNic(NicProfile nic) { + JsonArray links = new JsonArray(); + if (StringUtils.isNotBlank(nic.getMacAddress())) { + JsonObject link = new JsonObject(); + link.addProperty("ethernet_mac_address", nic.getMacAddress()); + link.addProperty("id", String.format("eth%d", nic.getDeviceId())); + link.addProperty("mtu", nic.getMtu() != null ? nic.getMtu() : DEFAULT_MTU); + link.addProperty("type", "phy"); + links.add(link); + } + return links; + } + + static JsonArray getNetworksJsonArrayForNic(NicProfile nic) { + JsonArray networks = new JsonArray(); + if (StringUtils.isNotBlank(nic.getIPv4Address())) { + JsonObject ipv4Network = new JsonObject(); + ipv4Network.addProperty("id", String.format("eth%d", nic.getDeviceId())); + ipv4Network.addProperty("ip_address", nic.getIPv4Address()); + ipv4Network.addProperty("link", String.format("eth%d", nic.getDeviceId())); + ipv4Network.addProperty("netmask", nic.getIPv4Netmask()); + ipv4Network.addProperty("network_id", nic.getUuid()); + ipv4Network.addProperty("type", "ipv4"); + + JsonArray ipv4RouteArray = new JsonArray(); + JsonObject ipv4Route = new JsonObject(); + ipv4Route.addProperty("gateway", nic.getIPv4Gateway()); + ipv4Route.addProperty("netmask", "0.0.0.0"); + ipv4Route.addProperty("network", "0.0.0.0"); + ipv4RouteArray.add(ipv4Route); + + ipv4Network.add("routes", ipv4RouteArray); + + networks.add(ipv4Network); + } + + if (StringUtils.isNotBlank(nic.getIPv6Address())) { + JsonObject ipv6Network = new JsonObject(); + ipv6Network.addProperty("id", String.format("eth%d", nic.getDeviceId())); + ipv6Network.addProperty("ip_address", nic.getIPv6Address()); + ipv6Network.addProperty("link", String.format("eth%d", nic.getDeviceId())); + ipv6Network.addProperty("netmask", IPv6Network.fromString(nic.getIPv6Cidr()).getNetmask().toString()); + ipv6Network.addProperty("network_id", nic.getUuid()); + ipv6Network.addProperty("type", "ipv6"); + + JsonArray ipv6RouteArray = new JsonArray(); + JsonObject ipv6Route = new JsonObject(); + ipv6Route.addProperty("gateway", nic.getIPv6Gateway()); + ipv6Route.addProperty("netmask", "0"); + ipv6Route.addProperty("network", "::"); + ipv6RouteArray.add(ipv6Route); + + ipv6Network.add("routes", ipv6RouteArray); + + networks.add(ipv6Network); + } + return networks; + } + + static JsonArray getServicesJsonArrayForNic(NicProfile nic) { + JsonArray services = new JsonArray(); + if (StringUtils.isNotBlank(nic.getIPv4Dns1())) { + services.add(getDnsServiceObject(nic.getIPv4Dns1())); + } + + if (StringUtils.isNotBlank(nic.getIPv4Dns2())) { + services.add(getDnsServiceObject(nic.getIPv4Dns2())); + } + + if (StringUtils.isNotBlank(nic.getIPv6Dns1())) { + services.add(getDnsServiceObject(nic.getIPv6Dns1())); + } + + if (StringUtils.isNotBlank(nic.getIPv6Dns2())) { + services.add(getDnsServiceObject(nic.getIPv6Dns2())); + } + return services; + } + + private static JsonObject getDnsServiceObject(String dnsAddress) { + JsonObject dnsService = new JsonObject(); + dnsService.addProperty("address", dnsAddress); + dnsService.addProperty("type", "dns"); + return dnsService; + } + static void createFileInTempDirAnAppendOpenStackMetadataToJsonObject(String tempDirName, JsonObject metaData, String dataType, String fileName, String content, Map customUserdataParams) { if (StringUtils.isBlank(dataType)) { return; diff --git a/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveUtils.java b/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveUtils.java new file mode 100644 index 000000000000..8847497f193c --- /dev/null +++ b/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveUtils.java @@ -0,0 +1,54 @@ +// 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.storage.configdrive; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class ConfigDriveUtils { + + static void mergeJsonArraysAndUpdateObject(JsonObject finalObject, JsonObject newObj, String memberName, String... keys) { + JsonArray existingMembers = finalObject.has(memberName) ? finalObject.get(memberName).getAsJsonArray() : new JsonArray(); + JsonArray newMembers = newObj.has(memberName) ? newObj.get(memberName).getAsJsonArray() : new JsonArray(); + + if (existingMembers.size() > 0 || newMembers.size() > 0) { + JsonArray finalMembers = new JsonArray(); + Set idSet = new HashSet<>(); + for (JsonElement element : existingMembers.getAsJsonArray()) { + JsonObject elementObject = element.getAsJsonObject(); + String key = Arrays.stream(keys).map(elementObject::get).map(JsonElement::getAsString).reduce((a, b) -> a + "-" + b).orElse(""); + idSet.add(key); + finalMembers.add(element); + } + for (JsonElement element : newMembers.getAsJsonArray()) { + JsonObject elementObject = element.getAsJsonObject(); + String key = Arrays.stream(keys).map(elementObject::get).map(JsonElement::getAsString).reduce((a, b) -> a + "-" + b).orElse(""); + if (!idSet.contains(key)) { + finalMembers.add(element); + } + } + finalObject.add(memberName, finalMembers); + } + } + +} diff --git a/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java b/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java index eff881065c2d..3effdb5ba212 100644 --- a/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java +++ b/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java @@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import java.io.File; @@ -27,14 +28,21 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.cloud.network.Network; +import com.cloud.vm.NicProfile; +import com.google.gson.JsonParser; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.MockedConstruction; @@ -49,6 +57,13 @@ @RunWith(MockitoJUnitRunner.class) public class ConfigDriveBuilderTest { + private static Map> supportedServices; + + @BeforeClass + public static void beforeClass() throws Exception { + supportedServices = Map.of(1L, List.of(Network.Service.UserData, Network.Service.Dhcp, Network.Service.Dns)); + } + @Test public void writeFileTest() { try (MockedStatic fileUtilsMocked = Mockito.mockStatic(FileUtils.class)) { @@ -112,16 +127,16 @@ public void base64StringToFileTest() throws Exception { } @Test(expected = CloudRuntimeException.class) - public void buildConfigDriveTestNoVmData() { - ConfigDriveBuilder.buildConfigDrive(null, "teste", "C:", null); + public void buildConfigDriveTestNoVmDataAndNic() { + ConfigDriveBuilder.buildConfigDrive(null, null, "teste", "C:", null, null); } @Test(expected = CloudRuntimeException.class) public void buildConfigDriveTestIoException() { try (MockedStatic configDriveBuilderMocked = Mockito.mockStatic(ConfigDriveBuilder.class)) { - configDriveBuilderMocked.when(() -> ConfigDriveBuilder.writeVendorAndNetworkEmptyJsonFile(nullable(File.class))).thenThrow(CloudRuntimeException.class); - Mockito.when(ConfigDriveBuilder.buildConfigDrive(new ArrayList<>(), "teste", "C:", null)).thenCallRealMethod(); - ConfigDriveBuilder.buildConfigDrive(new ArrayList<>(), "teste", "C:", null); + configDriveBuilderMocked.when(() -> ConfigDriveBuilder.writeVendorEmptyJsonFile(nullable(File.class))).thenThrow(CloudRuntimeException.class); + Mockito.when(ConfigDriveBuilder.buildConfigDrive(null, new ArrayList<>(), "teste", "C:", null, supportedServices)).thenCallRealMethod(); + ConfigDriveBuilder.buildConfigDrive(null, new ArrayList<>(), "teste", "C:", null, supportedServices); } } @@ -129,22 +144,26 @@ public void buildConfigDriveTestIoException() { public void buildConfigDriveTest() { try (MockedStatic configDriveBuilderMocked = Mockito.mockStatic(ConfigDriveBuilder.class)) { - configDriveBuilderMocked.when(() -> ConfigDriveBuilder.writeVendorAndNetworkEmptyJsonFile(Mockito.any(File.class))).then(invocationOnMock -> null); + configDriveBuilderMocked.when(() -> ConfigDriveBuilder.writeVendorEmptyJsonFile(Mockito.any(File.class))).then(invocationOnMock -> null); configDriveBuilderMocked.when(() -> ConfigDriveBuilder.writeVmMetadata(Mockito.anyList(), Mockito.anyString(), Mockito.any(File.class), anyMap())).then(invocationOnMock -> null); configDriveBuilderMocked.when(() -> ConfigDriveBuilder.linkUserData((Mockito.anyString()))).then(invocationOnMock -> null); configDriveBuilderMocked.when(() -> ConfigDriveBuilder.generateAndRetrieveIsoAsBase64Iso(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenAnswer(invocation -> "mockIsoDataBase64"); + + NicProfile mockedNicProfile = Mockito.mock(NicProfile.class); + Mockito.when(mockedNicProfile.getId()).thenReturn(1L); + //force execution of real method - Mockito.when(ConfigDriveBuilder.buildConfigDrive(new ArrayList<>(), "teste", "C:", null)).thenCallRealMethod(); + Mockito.when(ConfigDriveBuilder.buildConfigDrive(List.of(mockedNicProfile), new ArrayList<>(), "teste", "C:", null, supportedServices)).thenCallRealMethod(); - String returnedIsoData = ConfigDriveBuilder.buildConfigDrive(new ArrayList<>(), "teste", "C:", null); + String returnedIsoData = ConfigDriveBuilder.buildConfigDrive(List.of(mockedNicProfile), new ArrayList<>(), "teste", "C:", null, supportedServices); Assert.assertEquals("mockIsoDataBase64", returnedIsoData); configDriveBuilderMocked.verify(() -> { - ConfigDriveBuilder.writeVendorAndNetworkEmptyJsonFile(Mockito.any(File.class)); + ConfigDriveBuilder.writeVendorEmptyJsonFile(Mockito.any(File.class)); ConfigDriveBuilder.writeVmMetadata(Mockito.anyList(), Mockito.anyString(), Mockito.any(File.class), anyMap()); ConfigDriveBuilder.linkUserData(Mockito.anyString()); ConfigDriveBuilder.generateAndRetrieveIsoAsBase64Iso(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); @@ -153,23 +172,23 @@ public void buildConfigDriveTest() { } @Test(expected = CloudRuntimeException.class) - public void writeVendorAndNetworkEmptyJsonFileTestCannotCreateOpenStackFolder() { + public void writeVendorEmptyJsonFileTestCannotCreateOpenStackFolder() { File folderFileMock = Mockito.mock(File.class); Mockito.doReturn(false).when(folderFileMock).mkdirs(); - ConfigDriveBuilder.writeVendorAndNetworkEmptyJsonFile(folderFileMock); + ConfigDriveBuilder.writeVendorEmptyJsonFile(folderFileMock); } @Test(expected = CloudRuntimeException.class) - public void writeVendorAndNetworkEmptyJsonFileTest() { + public void writeVendorEmptyJsonFileTest() { File folderFileMock = Mockito.mock(File.class); Mockito.doReturn(false).when(folderFileMock).mkdirs(); - ConfigDriveBuilder.writeVendorAndNetworkEmptyJsonFile(folderFileMock); + ConfigDriveBuilder.writeVendorEmptyJsonFile(folderFileMock); } @Test - public void writeVendorAndNetworkEmptyJsonFileTestCreatingFolder() { + public void writeVendorEmptyJsonFileTestCreatingFolder() { try (MockedStatic configDriveBuilderMocked = Mockito.mockStatic(ConfigDriveBuilder.class)) { File folderFileMock = Mockito.mock(File.class); @@ -177,9 +196,9 @@ public void writeVendorAndNetworkEmptyJsonFileTestCreatingFolder() { Mockito.doReturn(true).when(folderFileMock).mkdirs(); //force execution of real method - configDriveBuilderMocked.when(() -> ConfigDriveBuilder.writeVendorAndNetworkEmptyJsonFile(folderFileMock)).thenCallRealMethod(); + configDriveBuilderMocked.when(() -> ConfigDriveBuilder.writeVendorEmptyJsonFile(folderFileMock)).thenCallRealMethod(); - ConfigDriveBuilder.writeVendorAndNetworkEmptyJsonFile(folderFileMock); + ConfigDriveBuilder.writeVendorEmptyJsonFile(folderFileMock); Mockito.verify(folderFileMock).exists(); Mockito.verify(folderFileMock).mkdirs(); @@ -501,4 +520,143 @@ public void getProgramToGenerateIsoTestMkIsoMac() throws Exception { Mockito.verify(mkIsoProgramInMacOsFileMock, Mockito.times(1)).getCanonicalPath(); } } + + @Test + public void testWriteNetworkData() throws Exception { + // Setup + NicProfile nicp = mock(NicProfile.class); + Mockito.when(nicp.getId()).thenReturn(1L); + + Mockito.when(nicp.getMacAddress()).thenReturn("00:00:00:00:00:00"); + Mockito.when(nicp.getMtu()).thenReturn(2000); + + Mockito.when(nicp.getIPv4Address()).thenReturn("172.31.0.10"); + Mockito.when(nicp.getDeviceId()).thenReturn(1); + Mockito.when(nicp.getIPv4Netmask()).thenReturn("255.255.255.0"); + Mockito.when(nicp.getUuid()).thenReturn("NETWORK UUID"); + Mockito.when(nicp.getIPv4Gateway()).thenReturn("172.31.0.1"); + + + Mockito.when(nicp.getIPv6Address()).thenReturn("2001:db8:0:1234:0:567:8:1"); + Mockito.when(nicp.getIPv6Cidr()).thenReturn("2001:db8:0:1234:0:567:8:1/64"); + Mockito.when(nicp.getIPv6Gateway()).thenReturn("2001:db8:0:1234:0:567:8::1"); + + Mockito.when(nicp.getIPv4Dns1()).thenReturn("8.8.8.8"); + Mockito.when(nicp.getIPv4Dns2()).thenReturn("1.1.1.1"); + Mockito.when(nicp.getIPv6Dns1()).thenReturn("2001:4860:4860::8888"); + Mockito.when(nicp.getIPv6Dns2()).thenReturn("2001:4860:4860::8844"); + + + List services1 = Arrays.asList(Network.Service.Dhcp, Network.Service.Dns); + + Map> supportedServices = new HashMap<>(); + supportedServices.put(1L, services1); + + TemporaryFolder folder = new TemporaryFolder(); + folder.create(); + File openStackFolder = folder.newFolder("openStack"); + + // Expected JSON structure + String expectedJson = "{" + + " \"links\": [" + + " {" + + " \"ethernet_mac_address\": \"00:00:00:00:00:00\"," + + " \"id\": \"eth1\"," + + " \"mtu\": 2000," + + " \"type\": \"phy\"" + + " }" + + " ]," + + " \"networks\": [" + + " {" + + " \"id\": \"eth1\"," + + " \"ip_address\": \"172.31.0.10\"," + + " \"link\": \"eth1\"," + + " \"netmask\": \"255.255.255.0\"," + + " \"network_id\": \"NETWORK UUID\"," + + " \"type\": \"ipv4\"," + + " \"routes\": [" + + " {" + + " \"gateway\": \"172.31.0.1\"," + + " \"netmask\": \"0.0.0.0\"," + + " \"network\": \"0.0.0.0\"" + + " }" + + " ]" + + " }," + + " {" + + " \"id\": \"eth1\"," + + " \"ip_address\": \"2001:db8:0:1234:0:567:8:1\"," + + " \"link\": \"eth1\"," + + " \"netmask\": \"64\"," + + " \"network_id\": \"NETWORK UUID\"," + + " \"type\": \"ipv6\"," + + " \"routes\": [" + + " {" + + " \"gateway\": \"2001:db8:0:1234:0:567:8::1\"," + + " \"netmask\": \"0\"," + + " \"network\": \"::\"" + + " }" + + " ]" + + " }" + + " ]," + + " \"services\": [" + + " {" + + " \"address\": \"8.8.8.8\"," + + " \"type\": \"dns\"" + + " }," + + " {" + + " \"address\": \"1.1.1.1\"," + + " \"type\": \"dns\"" + + " }," + + " {" + + " \"address\": \"2001:4860:4860::8888\"," + + " \"type\": \"dns\"" + + " }," + + " {" + + " \"address\": \"2001:4860:4860::8844\"," + + " \"type\": \"dns\"" + + " }" + + " ]" + + "}"; + + // Action + ConfigDriveBuilder.writeNetworkData(Arrays.asList(nicp), supportedServices, openStackFolder); + + // Verify + File networkDataFile = new File(openStackFolder, "network_data.json"); + String content = FileUtils.readFileToString(networkDataFile, StandardCharsets.UTF_8); + JsonObject actualJson = new JsonParser().parse(content).getAsJsonObject(); + JsonObject expectedJsonObject = new JsonParser().parse(expectedJson).getAsJsonObject(); + + Assert.assertEquals(expectedJsonObject, actualJson); + folder.delete(); + } + + @Test + public void testWriteNetworkDataEmptyJson() throws Exception { + // Setup + NicProfile nicp = mock(NicProfile.class); + List services1 = Collections.emptyList(); + + Map> supportedServices = new HashMap<>(); + supportedServices.put(1L, services1); + + TemporaryFolder folder = new TemporaryFolder(); + folder.create(); + File openStackFolder = folder.newFolder("openStack"); + + // Expected JSON structure + String expectedJson = "{}"; + + // Action + ConfigDriveBuilder.writeNetworkData(Arrays.asList(nicp), supportedServices, openStackFolder); + + // Verify + File networkDataFile = new File(openStackFolder, "network_data.json"); + String content = FileUtils.readFileToString(networkDataFile, StandardCharsets.UTF_8); + JsonObject actualJson = new JsonParser().parse(content).getAsJsonObject(); + JsonObject expectedJsonObject = new JsonParser().parse(expectedJson).getAsJsonObject(); + + Assert.assertEquals(expectedJsonObject, actualJson); + folder.delete(); + } } diff --git a/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveUtilsTest.java b/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveUtilsTest.java new file mode 100644 index 000000000000..6e935b951da3 --- /dev/null +++ b/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveUtilsTest.java @@ -0,0 +1,108 @@ +// 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.storage.configdrive; + +import static org.apache.cloudstack.storage.configdrive.ConfigDriveUtils.mergeJsonArraysAndUpdateObject; + +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import com.google.gson.JsonObject; + +@RunWith(MockitoJUnitRunner.class) +public class ConfigDriveUtilsTest { + + @Test + public void testMergeJsonArraysAndUpdateObjectWithEmptyObjects() { + JsonObject finalObject = new JsonObject(); + JsonObject newObj = new JsonObject(); + mergeJsonArraysAndUpdateObject(finalObject, newObj, "links", "id", "type"); + Assert.assertEquals("{}", finalObject.toString()); + } + + @Test + public void testMergeJsonArraysAndUpdateObjectWithNewMembersAdded() { + JsonObject finalObject = new JsonObject(); + + JsonObject newObj = new JsonObject(); + JsonArray newMembers = new JsonArray(); + JsonObject newMember = new JsonObject(); + newMember.addProperty("id", "eth0"); + newMember.addProperty("type", "phy"); + newMembers.add(newMember); + newObj.add("links", newMembers); + + mergeJsonArraysAndUpdateObject(finalObject, newObj, "links", "id", "type"); + Assert.assertEquals(1, finalObject.getAsJsonArray("links").size()); + JsonObject expectedObj = new JsonParser().parse("{'links': [{'id': 'eth0', 'type': 'phy'}]}").getAsJsonObject(); + Assert.assertEquals(expectedObj, finalObject); + } + + @Test + public void testMergeJsonArraysAndUpdateObjectWithDuplicateMembersIgnored() { + JsonObject finalObject = new JsonObject(); + JsonArray existingMembers = new JsonArray(); + JsonObject existingMember = new JsonObject(); + existingMember.addProperty("id", "eth0"); + existingMember.addProperty("type", "phy"); + existingMembers.add(existingMember); + finalObject.add("links", existingMembers); + + JsonObject newObj = new JsonObject(); + newObj.add("links", existingMembers); // same as existingMembers for duplication + + mergeJsonArraysAndUpdateObject(finalObject, newObj, "links", "id", "type"); + Assert.assertEquals(1, finalObject.getAsJsonArray("links").size()); + JsonObject expectedObj = new JsonParser().parse("{'links': [{'id': 'eth0', 'type': 'phy'}]}").getAsJsonObject(); + Assert.assertEquals(expectedObj, finalObject); + } + + @Test + public void testMergeJsonArraysAndUpdateObjectWithDifferentMembers() { + JsonObject finalObject = new JsonObject(); + + JsonArray newMembers = new JsonArray(); + JsonObject newMember = new JsonObject(); + newMember.addProperty("id", "eth0"); + newMember.addProperty("type", "phy"); + newMembers.add(newMember); + finalObject.add("links", newMembers); + + JsonObject newObj = new JsonObject(); + newMembers = new JsonArray(); + newMember = new JsonObject(); + newMember.addProperty("id", "eth1"); + newMember.addProperty("type", "phy"); + newMembers.add(newMember); + newObj.add("links", newMembers); + + mergeJsonArraysAndUpdateObject(finalObject, newObj, "links", "id", "type"); + Assert.assertEquals(2, finalObject.getAsJsonArray("links").size()); + JsonObject expectedObj = new JsonParser().parse("{'links': [{'id': 'eth0', 'type': 'phy'}, {'id': 'eth1', 'type': 'phy'}]}").getAsJsonObject(); + Assert.assertEquals(expectedObj, finalObject); + } + + @Test(expected = NullPointerException.class) + public void testMergeJsonArraysAndUpdateObjectWithNullObjects() { + mergeJsonArraysAndUpdateObject(null, null, "services", "id", "type"); + } +} diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index aadce9461938..47c29f637170 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -2174,7 +2174,6 @@ public NicProfile getNicProfile(VirtualMachine vm, long networkId, String broadc NetworkVO network = _networksDao.findById(networkId); Integer networkRate = getNetworkRate(network.getId(), vm.getId()); -// NetworkGuru guru = _networkGurus.get(network.getGuruName()); NicProfile profile = new NicProfile(nic, network, nic.getBroadcastUri(), nic.getIsolationUri(), networkRate, isSecurityGroupSupportedInNetwork(network), getNetworkTag( vm.getHypervisorType(), network)); @@ -2184,7 +2183,17 @@ public NicProfile getNicProfile(VirtualMachine vm, long networkId, String broadc if (network.getTrafficType() == TrafficType.Guest && network.getPrivateMtu() != null) { profile.setMtu(network.getPrivateMtu()); } -// guru.updateNicProfile(profile, network); + + DataCenter dc = _dcDao.findById(network.getDataCenterId()); + + Pair ip4Dns = getNetworkIp4Dns(network, dc); + profile.setIPv4Dns1(ip4Dns.first()); + profile.setIPv4Dns2(ip4Dns.second()); + + Pair ip6Dns = getNetworkIp6Dns(network, dc); + profile.setIPv6Dns1(ip6Dns.first()); + profile.setIPv6Dns2(ip6Dns.second()); + return profile; } diff --git a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java index a9fa3e95275e..3449f1f5d00e 100644 --- a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java +++ b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.network.element; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -24,6 +25,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; @@ -90,7 +92,8 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; -public class ConfigDriveNetworkElement extends AdapterBase implements NetworkElement, UserDataServiceProvider, +public class ConfigDriveNetworkElement extends AdapterBase implements NetworkElement, + UserDataServiceProvider, DhcpServiceProvider, DnsServiceProvider, StateListener, NetworkMigrationResponder { private static final Map> capabilities = setCapabilities(); @@ -110,6 +113,8 @@ public class ConfigDriveNetworkElement extends AdapterBase implements NetworkEle @Inject NetworkModel _networkModel; @Inject + NetworkOrchestrationService _networkOrchestrationService; + @Inject GuestOSCategoryDao _guestOSCategoryDao; @Inject GuestOSDao _guestOSDao; @@ -197,6 +202,8 @@ public Map> getCapabilities() { private static Map> setCapabilities() { Map> capabilities = new HashMap<>(); capabilities.put(Service.UserData, null); + capabilities.put(Service.Dhcp, new HashMap<>()); + capabilities.put(Service.Dns, new HashMap<>()); return capabilities; } @@ -224,8 +231,7 @@ private String getSshKey(VirtualMachineProfile profile) { public boolean addPasswordAndUserdata(Network network, NicProfile nic, VirtualMachineProfile profile, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { return (canHandle(network.getTrafficType()) - && configureConfigDriveData(profile, nic, dest)) - && createConfigDriveIso(profile, dest, null); + && configureConfigDriveData(profile, nic, dest)); } @Override @@ -342,10 +348,13 @@ public boolean prepareMigration(NicProfile nic, Network network, VirtualMachineP configureConfigDriveData(vm, nic, dest); // Create the config drive on dest host cache - createConfigDriveIsoOnHostCache(vm, dest.getHost().getId()); + createConfigDriveIsoOnHostCache(nic, vm, dest.getHost().getId()); } else { vm.setConfigDriveLocation(getConfigDriveLocation(vm.getId())); - addPasswordAndUserdata(network, nic, vm, dest, context); + boolean result = addPasswordAndUserdata(network, nic, vm, dest, context); + if (result) { + createConfigDriveIso(nic, vm, dest, null); + } } } catch (InsufficientCapacityException | ResourceUnavailableException e) { logger.error("Failed to add config disk drive due to: ", e); @@ -398,7 +407,7 @@ private void recreateConfigDriveIso(NicProfile nic, Network network, VirtualMach vm.getUuid(), nic.getMacAddress(), userVm.getDetail("SSH.PublicKey"), (String) vm.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows, VirtualMachineManager.getHypervisorHostname(dest.getHost() != null ? dest.getHost().getName() : "")); vm.setVmData(vmData); vm.setConfigDriveLabel(VirtualMachineManager.VmConfigDriveLabel.value()); - createConfigDriveIso(vm, dest, diskToUse); + createConfigDriveIso(nic, vm, dest, diskToUse); } } } @@ -528,7 +537,7 @@ private boolean isConfigDriveIsoOnHostCache(long vmId) { return false; } - private boolean createConfigDriveIsoOnHostCache(VirtualMachineProfile profile, Long hostId) throws ResourceUnavailableException { + private boolean createConfigDriveIsoOnHostCache(NicProfile nic, VirtualMachineProfile profile, Long hostId) throws ResourceUnavailableException { if (hostId == null) { throw new ResourceUnavailableException("Config drive iso creation failed, dest host not available", ConfigDriveNetworkElement.class, 0L); @@ -540,7 +549,9 @@ private boolean createConfigDriveIsoOnHostCache(VirtualMachineProfile profile, L final String isoFileName = ConfigDrive.configIsoFileName(profile.getInstanceName()); final String isoPath = ConfigDrive.createConfigDrivePath(profile.getInstanceName()); - final String isoData = ConfigDriveBuilder.buildConfigDrive(profile.getVmData(), isoFileName, profile.getConfigDriveLabel(), customUserdataParamMap); + List nicProfiles = _networkOrchestrationService.getNicProfiles(nic.getVirtualMachineId(), profile.getHypervisorType()); + final Map> supportedServices = getSupportedServicesByElementForNetwork(nicProfiles); + final String isoData = ConfigDriveBuilder.buildConfigDrive(nicProfiles, profile.getVmData(), isoFileName, profile.getConfigDriveLabel(), customUserdataParamMap, supportedServices); final HandleConfigDriveIsoCommand configDriveIsoCommand = new HandleConfigDriveIsoCommand(isoPath, isoData, null, false, true, true); final HandleConfigDriveIsoAnswer answer = (HandleConfigDriveIsoAnswer) agentManager.easySend(hostId, configDriveIsoCommand); @@ -590,7 +601,27 @@ private boolean deleteConfigDriveIsoOnHostCache(final VirtualMachine vm, final L return true; } - private boolean createConfigDriveIso(VirtualMachineProfile profile, DeployDestination dest, DiskTO disk) throws ResourceUnavailableException { + private Map> getSupportedServicesByElementForNetwork(List nics) { + + Map> supportedServices = new HashMap<>(); + for (NicProfile nic: nics) { + ArrayList serviceList = new ArrayList<>(); + if (_networkModel.isProviderSupportServiceInNetwork(nic.getNetworkId(), Service.Dns, getProvider())) { + serviceList.add(Service.Dns); + } + if (_networkModel.isProviderSupportServiceInNetwork(nic.getNetworkId(), Service.UserData, getProvider())) { + serviceList.add(Service.UserData); + } + if (_networkModel.isProviderSupportServiceInNetwork(nic.getNetworkId(), Service.Dhcp, getProvider())) { + serviceList.add(Service.Dhcp); + } + supportedServices.put(nic.getId(), serviceList); + } + + return supportedServices; + } + + public boolean createConfigDriveIso(NicProfile nic, VirtualMachineProfile profile, DeployDestination dest, DiskTO disk) throws ResourceUnavailableException { DataStore dataStore = getDatastoreForConfigDriveIso(disk, profile, dest); final Long agentId = findAgentId(profile, dest, dataStore); @@ -605,7 +636,10 @@ private boolean createConfigDriveIso(VirtualMachineProfile profile, DeployDestin final String isoFileName = ConfigDrive.configIsoFileName(profile.getInstanceName()); final String isoPath = ConfigDrive.createConfigDrivePath(profile.getInstanceName()); - final String isoData = ConfigDriveBuilder.buildConfigDrive(profile.getVmData(), isoFileName, profile.getConfigDriveLabel(), customUserdataParamMap); + List nicProfiles = _networkOrchestrationService.getNicProfiles(nic.getVirtualMachineId(), profile.getHypervisorType()); + final Map> supportedServices = getSupportedServicesByElementForNetwork(nicProfiles); + final String isoData = ConfigDriveBuilder.buildConfigDrive( + nicProfiles, profile.getVmData(), isoFileName, profile.getConfigDriveLabel(), customUserdataParamMap, supportedServices); boolean useHostCacheOnUnsupportedPool = VirtualMachineManager.VmConfigDriveUseHostCacheOnUnsupportedPool.valueIn(dest.getDataCenter().getId()); boolean preferHostCache = VirtualMachineManager.VmConfigDriveForceHostCacheUse.valueIn(dest.getDataCenter().getId()); final HandleConfigDriveIsoCommand configDriveIsoCommand = new HandleConfigDriveIsoCommand(isoPath, isoData, dataStore.getTO(), useHostCacheOnUnsupportedPool, preferHostCache, true); @@ -758,4 +792,52 @@ private boolean configureConfigDriveData(final VirtualMachineProfile profile, fi return true; } + @Override + public boolean addDhcpEntry(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, + ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + // Update nic profile with required information. + // Add network checks + return true; + } + + @Override + public boolean configDhcpSupportForSubnet(Network network, NicProfile nic, VirtualMachineProfile vm, + DeployDestination dest, + ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + return false; + } + + @Override + public boolean removeDhcpSupportForSubnet(Network network) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean setExtraDhcpOptions(Network network, long nicId, Map dhcpOptions) { + return false; + } + + @Override + public boolean removeDhcpEntry(Network network, NicProfile nic, + VirtualMachineProfile vmProfile) throws ResourceUnavailableException { + return true; + } + + @Override + public boolean addDnsEntry(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, + ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + return true; + } + + @Override + public boolean configDnsSupportForSubnet(Network network, NicProfile nic, VirtualMachineProfile vm, + DeployDestination dest, + ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + return true; + } + + @Override + public boolean removeDnsSupportForSubnet(Network network) throws ResourceUnavailableException { + return true; + } } diff --git a/server/src/test/java/com/cloud/network/element/ConfigDriveNetworkElementTest.java b/server/src/test/java/com/cloud/network/element/ConfigDriveNetworkElementTest.java index d83120d75f37..8c8dc33d7ec6 100644 --- a/server/src/test/java/com/cloud/network/element/ConfigDriveNetworkElementTest.java +++ b/server/src/test/java/com/cloud/network/element/ConfigDriveNetworkElementTest.java @@ -61,6 +61,7 @@ import com.cloud.vm.dao.VMInstanceDao; import com.google.common.collect.Maps; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; @@ -83,6 +84,7 @@ import java.lang.reflect.Field; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -148,6 +150,7 @@ public class ConfigDriveNetworkElementTest { @Mock private AgentManager agentManager; @Mock private CallContext callContextMock; @Mock private DomainVO domainVO; + @Mock private NetworkOrchestrationService _networkOrchestrationService; @Spy @InjectMocks private ConfigDriveNetworkElement _configDrivesNetworkElement = new ConfigDriveNetworkElement(); @@ -264,13 +267,9 @@ public void testAddPasswordAndUserData() throws Exception { try (MockedStatic ignored1 = Mockito.mockStatic(ConfigDriveBuilder.class); MockedStatic ignored2 = Mockito.mockStatic(CallContext.class)) { Mockito.when(CallContext.current()).thenReturn(callContextMock); Mockito.doReturn(Mockito.mock(Account.class)).when(callContextMock).getCallingAccount(); - Mockito.when(ConfigDriveBuilder.buildConfigDrive(Mockito.anyList(), Mockito.anyString(), Mockito.anyString(), Mockito.anyMap())).thenReturn("content"); final HandleConfigDriveIsoAnswer answer = mock(HandleConfigDriveIsoAnswer.class); final UserVmDetailVO userVmDetailVO = mock(UserVmDetailVO.class); - when(agentManager.easySend(Mockito.anyLong(), Mockito.any(HandleConfigDriveIsoCommand.class))).thenReturn(answer); - when(answer.getResult()).thenReturn(true); - when(answer.getConfigDriveLocation()).thenReturn(NetworkElement.Location.PRIMARY); when(network.getTrafficType()).thenReturn(Networks.TrafficType.Guest); when(virtualMachine.getUuid()).thenReturn("vm-uuid"); when(userVmDetailVO.getValue()).thenReturn(PUBLIC_KEY); @@ -288,6 +287,28 @@ public void testAddPasswordAndUserData() throws Exception { profile.setConfigDriveLabel("testlabel"); assertTrue(_configDrivesNetworkElement.addPasswordAndUserdata( network, nicp, profile, deployDestination, null)); + } + } + + @Test + public void testCreateConfigDriveIso() throws Exception { + try (MockedStatic ignored1 = Mockito.mockStatic(ConfigDriveBuilder.class); MockedStatic ignored2 = Mockito.mockStatic(CallContext.class)) { + Mockito.when(CallContext.current()).thenReturn(callContextMock); + Mockito.when(ConfigDriveBuilder.buildConfigDrive(Mockito.anyList(), Mockito.anyList(), Mockito.anyString(), Mockito.anyString(), Mockito.anyMap(), Mockito.anyMap())).thenReturn("content"); + + final HandleConfigDriveIsoAnswer answer = mock(HandleConfigDriveIsoAnswer.class); + when(agentManager.easySend(Mockito.anyLong(), Mockito.any(HandleConfigDriveIsoCommand.class))).thenReturn(answer); + when(answer.getResult()).thenReturn(true); + when(answer.getConfigDriveLocation()).thenReturn(NetworkElement.Location.PRIMARY); + when(virtualMachine.getUuid()).thenReturn("vm-uuid"); + + Map parms = Maps.newHashMap(); + parms.put(VirtualMachineProfile.Param.VmPassword, PASSWORD); + parms.put(VirtualMachineProfile.Param.VmSshPubKey, PUBLIC_KEY); + VirtualMachineProfile profile = new VirtualMachineProfileImpl(virtualMachine, null, serviceOfferingVO, null, parms); + profile.setConfigDriveLabel("testlabel"); + profile.setVmData(Collections.emptyList()); + assertTrue(_configDrivesNetworkElement.createConfigDriveIso(nicp, profile, deployDestination, null)); ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(HandleConfigDriveIsoCommand.class); verify(agentManager, times(1)).easySend(Mockito.anyLong(), commandCaptor.capture()); diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index 8355648ad1de..68ad250a95e5 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -25,6 +25,7 @@ import javax.naming.ConfigurationException; import com.cloud.dc.DataCenter; +import com.cloud.hypervisor.Hypervisor; import com.cloud.network.PublicIpQuarantine; import com.cloud.network.VirtualRouterProvider; import com.cloud.utils.fsm.NoTransitionException; @@ -640,6 +641,11 @@ public List getNicProfiles(VirtualMachine vm) { return null; } + @Override + public List getNicProfiles(Long vmId, Hypervisor.HypervisorType hypervisorType) { + return List.of(); + } + @Override public Map getSystemVMAccessDetails(VirtualMachine vm) { return null; diff --git a/test/integration/smoke/test_network.py b/test/integration/smoke/test_network.py index 8f3f4f533dd7..b3e7fd3e42f4 100644 --- a/test/integration/smoke/test_network.py +++ b/test/integration/smoke/test_network.py @@ -17,6 +17,8 @@ # under the License. """ BVT tests for Network Life Cycle """ +import json + # Import Local Modules from marvin.codes import (FAILED, STATIC_NAT_RULE, LB_RULE, NAT_RULE, PASS) @@ -24,7 +26,7 @@ from marvin.cloudstackException import CloudstackAPIException from marvin.cloudstackAPI import rebootRouter from marvin.sshClient import SshClient -from marvin.lib.utils import cleanup_resources, get_process_status, get_host_credentials +from marvin.lib.utils import cleanup_resources, get_process_status, get_host_credentials, random_gen from marvin.lib.base import (Account, VirtualMachine, ServiceOffering, @@ -37,7 +39,9 @@ LoadBalancerRule, Router, NIC, - Cluster) + Template, + Cluster, + SSHKeyPair) from marvin.lib.common import (get_domain, get_free_vlan, get_zone, @@ -58,9 +62,11 @@ from ddt import ddt, data import unittest # Import System modules +import os import time import logging import random +import tempfile _multiprocess_shared_ = True @@ -2113,3 +2119,313 @@ def test_03_destroySharedNetwork(self): 0, "Failed to find the placeholder IP" ) + + +class TestSharedNetworkWithConfigDrive(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.testClient = super(TestSharedNetworkWithConfigDrive, cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + + cls.services = cls.testClient.getParsedTestDataConfig() + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.hv = cls.testClient.getHypervisorInfo() + + if cls.hv.lower() == 'simulator': + cls.skip = True + return + else: + cls.skip = False + + cls._cleanup = [] + + template = Template.register( + cls.apiclient, + cls.services["test_templates_cloud_init"][cls.hv.lower()], + zoneid=cls.zone.id, + hypervisor=cls.hv, + ) + template.download(cls.apiclient) + cls._cleanup.append(template) + + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + cls.services["virtual_machine"]["template"] = template.id + cls.services["virtual_machine"]["username"] = "ubuntu" + # Create Network Offering + cls.services["shared_network_offering_configdrive"]["specifyVlan"] = "True" + cls.services["shared_network_offering_configdrive"]["specifyIpRanges"] = "True" + cls.shared_network_offering = NetworkOffering.create(cls.apiclient, + cls.services["shared_network_offering_configdrive"], + conservemode=True) + + cls.isolated_network_offering = NetworkOffering.create( + cls.apiclient, + cls.services["isolated_network_offering"], + conservemode=True + ) + + # Update network offering state from disabled to enabled. + NetworkOffering.update( + cls.isolated_network_offering, + cls.apiclient, + id=cls.isolated_network_offering.id, + state="enabled" + ) + + # Update network offering state from disabled to enabled. + NetworkOffering.update(cls.shared_network_offering, cls.apiclient, state="enabled") + + cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offering"]) + physical_network, vlan = get_free_vlan(cls.apiclient, cls.zone.id) + # create network using the shared network offering created + + cls.services["shared_network"]["acltype"] = "domain" + cls.services["shared_network"]["vlan"] = vlan + cls.services["shared_network"]["networkofferingid"] = cls.shared_network_offering.id + cls.services["shared_network"]["physicalnetworkid"] = physical_network.id + + cls.setSharedNetworkParams("shared_network") + cls.shared_network = Network.create(cls.apiclient, + cls.services["shared_network"], + networkofferingid=cls.shared_network_offering.id, + zoneid=cls.zone.id) + + cls.isolated_network = Network.create( + cls.apiclient, + cls.services["isolated_network"], + networkofferingid=cls.isolated_network_offering.id, + zoneid=cls.zone.id + ) + + cls._cleanup.extend([ + cls.service_offering, + cls.shared_network, + cls.shared_network_offering, + cls.isolated_network, + cls.isolated_network_offering, + ]) + cls.tmp_files = [] + cls.keypair = cls.generate_ssh_keys() + return + + @classmethod + def generate_ssh_keys(cls): + """Generates ssh key pair + + Writes the private key into a temp file and returns the file name + + :returns: generated keypair + :rtype: MySSHKeyPair + """ + cls.keypair = SSHKeyPair.create( + cls.apiclient, + name=random_gen() + ".pem") + + cls._cleanup.append(SSHKeyPair(cls.keypair.__dict__, None)) + cls.debug("Created keypair with name: %s" % cls.keypair.name) + cls.debug("Writing the private key to local file") + pkfile = tempfile.gettempdir() + os.sep + cls.keypair.name + cls.keypair.private_key_file = pkfile + cls.tmp_files.append(pkfile) + cls.debug("File path: %s" % pkfile) + with open(pkfile, "w+") as f: + f.write(cls.keypair.privatekey) + os.chmod(pkfile, 0o400) + + return cls.keypair + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + if self.skip: + self.skipTest("Hypervisor is simulator - skipping Test..") + self.cleanup = [] + + @classmethod + def tearDownClass(cls): + try: + # Cleanup resources used + cleanup_resources(cls.apiclient, cls._cleanup) + for tmp_file in cls.tmp_files: + os.remove(tmp_file) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def tearDown(self): + cleanup_resources(self.apiclient, self.cleanup) + return + + @classmethod + def setSharedNetworkParams(cls, network, range=20): + + # @range: range decides the endip. Pass the range as "x" if you want the difference between the startip + # and endip as "x" + # Set the subnet number of shared networks randomly prior to execution + # of each test case to avoid overlapping of ip addresses + shared_network_subnet_number = random.randrange(1, 254) + cls.services[network]["gateway"] = "172.16." + str(shared_network_subnet_number) + ".1" + cls.services[network]["startip"] = "172.16." + str(shared_network_subnet_number) + ".2" + cls.services[network]["endip"] = "172.16." + str(shared_network_subnet_number) + "." + str(range + 1) + cls.services[network]["netmask"] = "255.255.255.0" + logger.debug("Executing command '%s'" % cls.services[network]) + + def _mount_config_drive(self, ssh): + """ + This method is to verify whether configdrive iso + is attached to vm or not + Returns mount path if config drive is attached else None + """ + mountdir = "/root/iso" + cmd = "sudo blkid -t LABEL='config-2' " \ + "/dev/sr? /dev/hd? /dev/sd? /dev/xvd? -o device" + tmp_cmd = [ + 'sudo bash -c "if [ ! -d {0} ]; then mkdir {0}; fi"'.format(mountdir), + "sudo umount %s" % mountdir] + self.debug("Unmounting drive from %s" % mountdir) + for tcmd in tmp_cmd: + ssh.execute(tcmd) + + self.debug("Trying to find ConfigDrive device") + configDrive = ssh.execute(cmd) + if not configDrive: + self.warn("ConfigDrive is not attached") + return None + + res = ssh.execute("sudo mount {} {}".format(str(configDrive[0]), mountdir)) + if str(res).lower().find("read-only") > -1: + self.debug("ConfigDrive iso is mounted at location %s" % mountdir) + return mountdir + else: + return None + + def _umount_config_drive(self, ssh, mount_path): + """unmount config drive inside guest vm + + :param ssh: SSH connection to the VM + :type ssh: marvin.sshClient.SshClient + :type mount_path: str + """ + ssh.execute("sudo umount -d %s" % mount_path) + # Give the VM time to unlock the iso device + time.sleep(0.5) + # Verify umount + result = ssh.execute("sudo ls %s" % mount_path) + self.assertTrue(len(result) == 0, + "After umount directory should be empty " + "but contains: %s" % result) + + def _get_config_drive_data(self, ssh, file, name, fail_on_missing=True): + """Fetches the content of a file file on the config drive + + :param ssh: SSH connection to the VM + :param file: path to the file to fetch + :param name: description of the file + :param fail_on_missing: + whether the test should fail if the file is missing + :type ssh: marvin.sshClient.SshClient + :type file: str + :type name: str + :type fail_on_missing: bool + :returns: the content of the file + :rtype: str + """ + cmd = "sudo cat %s" % file + res = ssh.execute(cmd) + content = '\n'.join(res) + + if fail_on_missing and "No such file or directory" in content: + self.debug("{} is not found".format(name)) + self.fail("{} is not found".format(name)) + + return content + + def _get_ip_address_output(self, ssh): + cmd = "ip address" + res = ssh.execute(cmd) + return '\n'.join(res) + + @attr(tags=["advanced", "shared"], required_hardware="true") + def test_01_deployVMInSharedNetwork(self): + try: + self.virtual_machine = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], + networkids=[self.shared_network.id, self.isolated_network.id], + serviceofferingid=self.service_offering.id, + keypair=self.keypair.name + ) + self.cleanup.append(self.virtual_machine) + except Exception as e: + self.fail("Exception while deploying virtual machine: %s" % e) + + public_ips = list_publicIP( + self.apiclient, + associatednetworkid=self.isolated_network.id + ) + public_ip = public_ips[0] + FireWallRule.create( + self.apiclient, + ipaddressid=public_ip.id, + protocol=self.services["natrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["natrule"]["publicport"], + endport=self.services["natrule"]["publicport"] + ) + + nat_rule = NATRule.create( + self.apiclient, + self.virtual_machine, + self.services["natrule"], + public_ip.id + ) + + private_key_file_location = self.keypair.private_key_file if self.keypair else None + ssh = self.virtual_machine.get_ssh_client(ipaddress=nat_rule.ipaddress, + keyPairFileLocation=private_key_file_location, retries=5) + + mount_path = self._mount_config_drive(ssh) + + network_data_content = self._get_config_drive_data(ssh, mount_path + "/openstack/latest/network_data.json", + "network_data") + + network_data = json.loads(network_data_content) + + self._umount_config_drive(ssh, mount_path) + + ip_address_output = self._get_ip_address_output(ssh) + + self.assertTrue('links' in network_data, "network_data.json doesn't contain links") + self.assertTrue('networks' in network_data, "network_data.json doesn't contain networks") + self.assertTrue('services' in network_data, "network_data.json doesn't contain services") + + for x in ['links', 'networks', 'services']: + self.assertTrue(x in network_data, "network_data.json doesn't contain " + x) + self.assertEqual(len(network_data[x]), 2, "network_data.json doesn't contain 2 " + x) + + self.assertIn(network_data['links'][0]['ethernet_mac_address'], + [self.virtual_machine.nic[0].macaddress, self.virtual_machine.nic[1].macaddress], + "macaddress doesn't match") + self.assertIn(network_data['links'][1]['ethernet_mac_address'], + [self.virtual_machine.nic[0].macaddress, self.virtual_machine.nic[1].macaddress], + "macaddress doesn't match") + + self.assertIn(network_data['networks'][0]['ip_address'], + [self.virtual_machine.nic[0].ipaddress, self.virtual_machine.nic[1].ipaddress], + "ip address doesn't match") + self.assertIn(network_data['networks'][1]['ip_address'], + [self.virtual_machine.nic[0].ipaddress, self.virtual_machine.nic[1].ipaddress], + "ip address doesn't match") + self.assertIn(network_data['networks'][0]['netmask'], + [self.virtual_machine.nic[0].netmask, self.virtual_machine.nic[1].netmask], + "netmask doesn't match") + self.assertIn(network_data['networks'][1]['netmask'], + [self.virtual_machine.nic[0].netmask, self.virtual_machine.nic[1].netmask], + "netmask doesn't match") + + self.assertEqual(network_data['services'][0]['type'], 'dns', "network_data.json doesn't contain dns service") + self.assertEqual(network_data['services'][1]['type'], 'dns', "network_data.json doesn't contain dns service") + + self.assertTrue(self.virtual_machine.nic[0].ipaddress in ip_address_output, "ip address doesn't match") + self.assertTrue(self.virtual_machine.nic[1].ipaddress in ip_address_output, "ip address doesn't match") diff --git a/tools/marvin/marvin/config/test_data.py b/tools/marvin/marvin/config/test_data.py index e96dba1c4d54..3485eeb8b184 100644 --- a/tools/marvin/marvin/config/test_data.py +++ b/tools/marvin/marvin/config/test_data.py @@ -450,6 +450,21 @@ "UserData": "VirtualRouter" } }, + "shared_network_offering_configdrive": { + "name": "MySharedOfferingWithConfigDrive-shared", + "displaytext": "MySharedOfferingWithConfigDrive", + "guestiptype": "Shared", + "supportedservices": "Dhcp,Dns,UserData", + "specifyVlan": "False", + "specifyIpRanges": "False", + "traffictype": "GUEST", + "tags": "native", + "serviceProviderList": { + "Dhcp": "ConfigDrive", + "Dns": "ConfigDrive", + "UserData": "ConfigDrive" + } + }, "shared_network_offering_all_services": { "name": "shared network offering with services enabled", "displaytext": "Shared network offering", @@ -1047,6 +1062,41 @@ "isextractable": "True" }, }, + "test_templates_cloud_init": { + "kvm": { + "name": "ubuntu 22.04 kvm", + "displaytext": "ubuntu 22.04 kvm", + "format": "raw", + "hypervisor": "kvm", + "ostype": "Other Linux (64-bit)", + "url": "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img", + "requireshvm": "True", + "ispublic": "True", + "isextractable": "False" + }, + "xenserver": { + "name": "ubuntu 22.04 xen", + "displaytext": "ubuntu 22.04 xen", + "format": "vhd", + "hypervisor": "xenserver", + "ostype": "Other Linux (64-bit)", + "url": "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64-azure.vhd.tar.gz", + "requireshvm": "True", + "ispublic": "True", + "isextractable": "True" + }, + "vmware": { + "name": "ubuntu 22.04 vmware", + "displaytext": "ubuntu 22.04 vmware", + "format": "ova", + "hypervisor": "vmware", + "ostype": "Other Linux (64-bit)", + "url": "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.ova", + "requireshvm": "True", + "ispublic": "True", + "deployasis": "True" + }, + }, "test_ovf_templates": [ { "name": "test-ovf",