Skip to content

Commit

Permalink
Add support for network data in Config Drive (#9329)
Browse files Browse the repository at this point in the history
  • Loading branch information
vishesh92 committed Aug 26, 2024
1 parent c9f1c57 commit bc28665
Show file tree
Hide file tree
Showing 12 changed files with 1,025 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -144,6 +145,8 @@ void prepare(VirtualMachineProfile profile, DeployDestination dest, ReservationC

List<NicProfile> getNicProfiles(VirtualMachine vm);

List<NicProfile> getNicProfiles(Long vmId, Hypervisor.HypervisorType hypervisorType);

Map<String, String> getSystemVMAccessDetails(VirtualMachine vm);

Pair<? extends NetworkGuru, ? extends Network> implementNetwork(long networkId, DeployDestination dest, ReservationContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -4443,25 +4456,30 @@ private boolean getNicProfileDefaultNic(NicProfile nicProfile) {
}

@Override
public List<NicProfile> getNicProfiles(final VirtualMachine vm) {
final List<NicVO> nics = _nicDao.listByVmId(vm.getId());
public List<NicProfile> getNicProfiles(final Long vmId, HypervisorType hypervisorType) {
final List<NicVO> nics = _nicDao.listByVmId(vmId);
final List<NicProfile> profiles = new ArrayList<NicProfile>();

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);
}
}
return profiles;
}

@Override
public List<NicProfile> getNicProfiles(final VirtualMachine vm) {
return getNicProfiles(vm.getId(), vm.getHypervisorType());
}

@Override
public Map<String, String> getSystemVMAccessDetails(final VirtualMachine vm) {
final Map<String, String> accessDetails = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -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<String[]> vmData, String isoFileName, String driveLabel, Map<String, String> customUserdataParams) {
if (vmData == null) {
throw new CloudRuntimeException("No VM metadata provided");
public static String buildConfigDrive(List<NicProfile> nics, List<String[]> vmData, String isoFileName, String driveLabel, Map<String, String> customUserdataParams, Map<Long, List<Network.Service>> supportedServices) {
if (vmData == null && nics == null) {
throw new CloudRuntimeException("No VM metadata and nic profile provided");
}

Path tempDir = null;
Expand All @@ -121,10 +126,19 @@ public static String buildConfigDrive(List<String[]> 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) {
Expand Down Expand Up @@ -212,18 +226,36 @@ static void writeVmMetadata(List<String[]> vmData, String tempDirName, File open
}

/**
* Writes the following empty JSON files:
* <ul>
* <li> vendor_data.json
* <li> network_data.json
* </ul>
* 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<NicProfile> nics, Map<Long, List<Network.Service>> supportedServices, File openStackFolder) {
JsonObject finalNetworkData = new JsonObject();
if (needForGeneratingNetworkData(supportedServices)) {
for (NicProfile nic : nics) {
List<Network.Service> 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<Long, List<Network.Service>> 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);
}
Expand All @@ -250,6 +282,120 @@ static JsonObject createJsonObjectWithVmData(List<String[]> vmData, String tempD
return metaData;
}

/**
* Creates the {@link JsonObject} using @param nic's metadata. We expect the JSONObject to have the following entries:
* <ul>
* <li> links </li>
* <li> networks </li>
* <li> services </li>
* </ul>
*/
static JsonObject getNetworkDataJsonObjectForNic(NicProfile nic, List<Network.Service> 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<String, String> customUserdataParams) {
if (StringUtils.isBlank(dataType)) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}

}
Loading

0 comments on commit bc28665

Please sign in to comment.