blob: c7c63f80494b817f068368003b874f00fe175a33 [file] [log] [blame]
// 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 com.cloud.network.NetworkModel.CONFIGDATA_CONTENT;
import static com.cloud.network.NetworkModel.CONFIGDATA_DIR;
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 java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.joda.time.Duration;
import com.cloud.network.NetworkModel;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
public class ConfigDriveBuilder {
public static final Logger LOG = Logger.getLogger(ConfigDriveBuilder.class);
/**
* Writes a content {@link String} to a file that is going to be created in a folder. We will not append to the file if it already exists. Therefore, its content will be overwritten.
* Moreover, the charset used is {@link com.cloud.utils.StringUtils#getPreferredCharset()}.
*
* We expect the folder object and the file not to be null/empty.
*/
static void writeFile(File folder, String file, String content) {
try {
FileUtils.write(new File(folder, file), content, com.cloud.utils.StringUtils.getPreferredCharset(), false);
} catch (IOException ex) {
throw new CloudRuntimeException("Failed to create config drive file " + file, ex);
}
}
/**
* 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}
*/
public static String fileToBase64String(File isoFile) throws IOException {
byte[] encoded = Base64.encodeBase64(FileUtils.readFileToByteArray(isoFile));
return new String(encoded, StandardCharsets.US_ASCII);
}
/**
* Writes a String encoded in base 64 to a file in the given folder.
* The content will be decoded and then written to the file. Be aware that we will overwrite the content of the file if it already exists.
* Moreover, the content will must be encoded in {@link StandardCharsets#US_ASCII} before it is encoded in base 64.
*/
public static File base64StringToFile(String encodedIsoData, String folder, String fileName) throws IOException {
byte[] decoded = Base64.decodeBase64(encodedIsoData.getBytes(StandardCharsets.US_ASCII));
Path destPath = Paths.get(folder, fileName);
try {
Files.createDirectories(destPath.getParent());
} catch (final IOException e) {
LOG.warn("Exception hit while trying to recreate directory: " + destPath.getParent().toString());
}
return Files.write(destPath, decoded).toFile();
}
/**
* 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");
}
Path tempDir = null;
String tempDirName = null;
try {
tempDir = Files.createTempDirectory(ConfigDrive.CONFIGDRIVEDIR);
tempDirName = tempDir.toString();
File openStackFolder = new File(tempDirName + ConfigDrive.openStackConfigDriveName);
writeVendorAndNetworkEmptyJsonFile(openStackFolder);
writeVmMetadata(vmData, tempDirName, openStackFolder, customUserdataParams);
linkUserData(tempDirName);
return generateAndRetrieveIsoAsBase64Iso(isoFileName, driveLabel, tempDirName);
} catch (IOException e) {
throw new CloudRuntimeException("Failed due to", e);
} finally {
deleteTempDir(tempDir);
}
}
private static void deleteTempDir(Path tempDir) {
try {
if (tempDir != null) {
FileUtils.deleteDirectory(tempDir.toFile());
}
} catch (IOException ioe) {
LOG.warn("Failed to delete ConfigDrive temporary directory: " + tempDir.toString(), ioe);
}
}
/**
* Generates the ISO file that has the tempDir content.
*
* Max allowed file size of config drive is 64MB [1]. Therefore, if the ISO is bigger than that, we throw a {@link CloudRuntimeException}.
* [1] https://docs.openstack.org/project-install-guide/baremetal/draft/configdrive.html
*/
static String generateAndRetrieveIsoAsBase64Iso(String isoFileName, String driveLabel, String tempDirName) throws IOException {
File tmpIsoStore = new File(tempDirName, isoFileName);
Script command = new Script(getProgramToGenerateIso(), Duration.standardSeconds(300), LOG);
command.add("-o", tmpIsoStore.getAbsolutePath());
command.add("-ldots");
command.add("-allow-lowercase");
command.add("-allow-multidot");
command.add("-cache-inodes"); // Enable caching inode and device numbers to find hard links to files.
command.add("-l");
command.add("-quiet");
command.add("-J");
command.add("-r");
command.add("-V", driveLabel);
command.add(tempDirName);
LOG.debug("Executing config drive creation command: " + command.toString());
String result = command.execute();
if (StringUtils.isNotBlank(result)) {
String errMsg = "Unable to create iso file: " + isoFileName + " due to ge" + result;
LOG.warn(errMsg);
throw new CloudRuntimeException(errMsg);
}
File tmpIsoFile = new File(tmpIsoStore.getAbsolutePath());
if (tmpIsoFile.length() > (64L * 1024L * 1024L)) {
throw new CloudRuntimeException("Config drive file exceeds maximum allowed size of 64MB");
}
return fileToBase64String(tmpIsoFile);
}
/**
* Checks if the 'genisoimage' or 'mkisofs' is available and return the full qualified path for the program.
* The path checked are the following:
* <ul>
* <li> /usr/bin/genisoimage
* <li> /usr/bin/mkisofs
* </ul> /usr/local/bin/mkisofs
*/
static String getProgramToGenerateIso() throws IOException {
File isoCreator = new File("/usr/bin/genisoimage");
if (!isoCreator.exists()) {
isoCreator = new File("/usr/bin/mkisofs");
if (!isoCreator.exists()) {
isoCreator = new File("/usr/local/bin/mkisofs");
}
}
if (!isoCreator.exists()) {
throw new CloudRuntimeException("Cannot create iso for config drive using any know tool. Known paths [/usr/bin/genisoimage, /usr/bin/mkisofs, /usr/local/bin/mkisofs]");
}
if (!isoCreator.canExecute()) {
throw new CloudRuntimeException("Cannot create iso for config drive using: " + isoCreator.getCanonicalPath());
}
return isoCreator.getCanonicalPath();
}
/**
* First we generate a JSON object using {@link #createJsonObjectWithVmData(List, String, Map)}, then we write it to a file called "meta_data.json".
*/
static void writeVmMetadata(List<String[]> vmData, String tempDirName, File openStackFolder, Map<String, String> customUserdataParams) {
JsonObject metaData = createJsonObjectWithVmData(vmData, tempDirName, customUserdataParams);
writeFile(openStackFolder, "meta_data.json", metaData.toString());
}
/**
* Writes the following empty JSON files:
* <ul>
* <li> vendor_data.json
* <li> network_data.json
* </ul>
*
* If the folder does not exist and we cannot create it, we throw a {@link CloudRuntimeException}.
*/
static void writeVendorAndNetworkEmptyJsonFile(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);
}
}
/**
* Creates the {@link JsonObject} with VM's metadata. The vmData is a list of arrays; we expect this list to have the following entries:
* <ul>
* <li> [0]: config data type
* <li> [1]: config data file name
* <li> [2]: config data file content
* </ul>
*/
static JsonObject createJsonObjectWithVmData(List<String[]> vmData, String tempDirName, Map<String, String> customUserdataParams) {
JsonObject metaData = new JsonObject();
for (String[] item : vmData) {
String dataType = item[CONFIGDATA_DIR];
String fileName = item[CONFIGDATA_FILE];
String content = item[CONFIGDATA_CONTENT];
LOG.debug(String.format("[createConfigDriveIsoForVM] dataType=%s, filename=%s, content=%s", dataType, fileName, (PASSWORD_FILE.equals(fileName) ? "********" : content)));
createFileInTempDirAnAppendOpenStackMetadataToJsonObject(tempDirName, metaData, dataType, fileName, content, customUserdataParams);
}
return metaData;
}
static void createFileInTempDirAnAppendOpenStackMetadataToJsonObject(String tempDirName, JsonObject metaData, String dataType, String fileName, String content, Map<String, String> customUserdataParams) {
if (StringUtils.isBlank(dataType)) {
return;
}
//create folder
File typeFolder = new File(tempDirName + ConfigDrive.cloudStackConfigDriveName + dataType);
if (!typeFolder.exists() && !typeFolder.mkdirs()) {
throw new CloudRuntimeException("Failed to create folder: " + typeFolder);
}
if (StringUtils.isNotBlank(content)) {
File file = new File(typeFolder, fileName + ".txt");
try {
if (fileName.equals(USERDATA_FILE)) {
// User Data is passed as a base64 encoded string
FileUtils.writeByteArrayToFile(file, Base64.decodeBase64(content));
} else {
FileUtils.write(file, content, com.cloud.utils.StringUtils.getPreferredCharset());
}
} catch (IOException ex) {
throw new CloudRuntimeException("Failed to create file ", ex);
}
}
//now write the file to the OpenStack directory
buildOpenStackMetaData(metaData, dataType, fileName, content);
buildCustomUserdataParamsMetaData(metaData, dataType, fileName, content, customUserdataParams);
}
protected static void buildCustomUserdataParamsMetaData(JsonObject metaData, String dataType, String fileName, String content, Map<String, String> customUserdataParams) {
if (!NetworkModel.METATDATA_DIR.equals(dataType)) {
return;
}
if (StringUtils.isEmpty(content)) {
return;
}
if (MapUtils.isNotEmpty(customUserdataParams)) {
Set<String> userdataVariableFileNames = customUserdataParams.keySet();
if (userdataVariableFileNames.contains(fileName)) {
metaData.addProperty(fileName, content);
}
}
}
/**
* Hard link the user_data.txt file with the user_data file in the OpenStack directory.
*/
static void linkUserData(String tempDirName) {
String userDataFilePath = tempDirName + ConfigDrive.cloudStackConfigDriveName + "userdata/user_data.txt";
File file = new File(userDataFilePath);
if (file.exists()) {
Script hardLink = new Script("ln", Duration.standardSeconds(300), LOG);
hardLink.add(userDataFilePath);
hardLink.add(tempDirName + ConfigDrive.openStackConfigDriveName + "user_data");
LOG.debug("execute command: " + hardLink.toString());
String executionResult = hardLink.execute();
if (StringUtils.isNotBlank(executionResult)) {
throw new CloudRuntimeException("Unable to create user_data link due to " + executionResult);
}
}
}
private static JsonArray arrayOf(JsonElement... elements) {
JsonArray array = new JsonArray();
for (JsonElement element : elements) {
array.add(element);
}
return array;
}
private static void buildOpenStackMetaData(JsonObject metaData, String dataType, String fileName, String content) {
if (!NetworkModel.METATDATA_DIR.equals(dataType)) {
return;
}
if (StringUtils.isEmpty(content)) {
return;
}
//keys are a special case in OpenStack format
if (NetworkModel.PUBLIC_KEYS_FILE.equals(fileName)) {
String[] keyArray = content.replace("\\n", "").split(" ");
String keyName = "key";
if (keyArray.length > 3 && StringUtils.isNotEmpty(keyArray[2])) {
keyName = keyArray[2];
}
JsonObject keyLegacy = new JsonObject();
keyLegacy.addProperty("type", "ssh");
keyLegacy.addProperty("data", content.replace("\\n", ""));
keyLegacy.addProperty("name", keyName);
metaData.add("keys", arrayOf(keyLegacy));
JsonObject key = new JsonObject();
key.addProperty(keyName, content);
metaData.add("public_keys", key);
} else if (NetworkModel.openStackFileMapping.get(fileName) != null) {
metaData.addProperty(NetworkModel.openStackFileMapping.get(fileName), content);
}
}
}