Skip to content

Commit c3e73c4

Browse files
Create unit test cases for 'ConfigDriveBuilder' class
1 parent 7a3a882 commit c3e73c4

5 files changed

Lines changed: 586 additions & 158 deletions

File tree

engine/storage/configdrive/src/org/apache/cloudstack/storage/configdrive/ConfigDrive.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ public class ConfigDrive {
2626
public static final String openStackConfigDriveName = "/openstack/latest/";
2727

2828
/**
29-
* This is the path to iso file relative to mount point
30-
* @return config drive iso file path
29+
* Created the path to ISO file relative to mount point.
30+
* The config driver path will have the following formatt: {@link #CONFIGDRIVEDIR} + / + instanceName + / + {@link #CONFIGDRIVEFILENAME}
31+
* @return config drive ISO file path
3132
*/
32-
public static String createConfigDrivePath(final String instanceName) {
33+
public static String createConfigDrivePath(String instanceName) {
3334
return ConfigDrive.CONFIGDRIVEDIR + "/" + instanceName + "/" + ConfigDrive.CONFIGDRIVEFILENAME;
3435
}
3536

engine/storage/configdrive/src/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java

Lines changed: 185 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
import static com.cloud.network.NetworkModel.PASSWORD_FILE;
2424
import static com.cloud.network.NetworkModel.USERDATA_FILE;
2525

26-
import java.io.BufferedWriter;
2726
import java.io.File;
28-
import java.io.FileWriter;
2927
import java.io.IOException;
3028
import java.nio.charset.StandardCharsets;
3129
import java.nio.file.Files;
@@ -42,7 +40,6 @@
4240
import com.cloud.network.NetworkModel;
4341
import com.cloud.utils.exception.CloudRuntimeException;
4442
import com.cloud.utils.script.Script;
45-
import com.google.common.base.Strings;
4643
import com.google.gson.JsonArray;
4744
import com.google.gson.JsonElement;
4845
import com.google.gson.JsonObject;
@@ -51,30 +48,45 @@ public class ConfigDriveBuilder {
5148

5249
public static final Logger LOG = Logger.getLogger(ConfigDriveBuilder.class);
5350

54-
private static void writeFile(final File folder, final String file, final String content) {
55-
if (folder == null || Strings.isNullOrEmpty(file)) {
56-
return;
57-
}
58-
final File vendorDataFile = new File(folder, file);
59-
try (final FileWriter fw = new FileWriter(vendorDataFile); final BufferedWriter bw = new BufferedWriter(fw)) {
60-
bw.write(content);
51+
/**
52+
* 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.
53+
* Moreover, the charset used is {@link StandardCharsets#US_ASCII}.
54+
*
55+
* We expect the folder object and the file not to be null/empty.
56+
*/
57+
static void writeFile(File folder, String file, String content) {
58+
try {
59+
FileUtils.write(new File(folder, file), content, StandardCharsets.US_ASCII, false);
6160
} catch (IOException ex) {
6261
throw new CloudRuntimeException("Failed to create config drive file " + file, ex);
6362
}
6463
}
6564

66-
public static String fileToBase64String(final File isoFile) throws IOException {
65+
/**
66+
* Read the content of a {@link File} and convert it to a String in base 64.
67+
* We expect the content of the file to be encoded using {@link com.cloud.utils.StringUtils#getPreferredCharset()}
68+
*/
69+
public static String fileToBase64String(File isoFile) throws IOException {
6770
byte[] encoded = Base64.encodeBase64(FileUtils.readFileToByteArray(isoFile));
68-
return new String(encoded, StandardCharsets.US_ASCII);
71+
return new String(encoded, com.cloud.utils.StringUtils.getPreferredCharset());
6972
}
7073

71-
public static File base64StringToFile(final String encodedIsoData, final String folder, final String fileName) throws IOException {
72-
byte[] decoded = Base64.decodeBase64(encodedIsoData.getBytes(StandardCharsets.US_ASCII));
74+
/**
75+
* Writes a String encoded in base 64 to a file in the given folder.
76+
* 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.
77+
* Moreover, the content will must be encoded in {@link com.cloud.utils.StringUtils#getPreferredCharset()} before it is encoded in base 64.
78+
*/
79+
public static File base64StringToFile(String encodedIsoData, String folder, String fileName) throws IOException {
80+
byte[] decoded = Base64.decodeBase64(encodedIsoData.getBytes(com.cloud.utils.StringUtils.getPreferredCharset()));
7381
Path destPath = Paths.get(folder, fileName);
7482
return Files.write(destPath, decoded).toFile();
7583
}
7684

77-
public static String buildConfigDrive(final List<String[]> vmData, final String isoFileName, final String driveLabel) {
85+
/**
86+
* 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.
87+
* If vmData is null, we throw a {@link CloudRuntimeException}. Moreover, {@link IOException} are captured and re-thrown as {@link CloudRuntimeException}.
88+
*/
89+
public static String buildConfigDrive(List<String[]> vmData, String isoFileName, String driveLabel) {
7890
if (vmData == null) {
7991
throw new CloudRuntimeException("No VM metadata provided");
8092
}
@@ -86,103 +98,156 @@ public static String buildConfigDrive(final List<String[]> vmData, final String
8698
tempDirName = tempDir.toString();
8799

88100
File openStackFolder = new File(tempDirName + ConfigDrive.openStackConfigDriveName);
89-
if (openStackFolder.exists() || openStackFolder.mkdirs()) {
90-
writeFile(openStackFolder, "vendor_data.json", "{}");
91-
writeFile(openStackFolder, "network_data.json", "{}");
92-
} else {
93-
throw new CloudRuntimeException("Failed to create folder " + openStackFolder);
94-
}
95101

96-
JsonObject metaData = new JsonObject();
97-
for (String[] item : vmData) {
98-
String dataType = item[CONFIGDATA_DIR];
99-
String fileName = item[CONFIGDATA_FILE];
100-
String content = item[CONFIGDATA_CONTENT];
101-
LOG.debug(String.format("[createConfigDriveIsoForVM] dataType=%s, filename=%s, content=%s",
102-
dataType, fileName, (fileName.equals(PASSWORD_FILE)?"********":content)));
103-
104-
// create file with content in folder
105-
if (dataType != null && !dataType.isEmpty()) {
106-
//create folder
107-
File typeFolder = new File(tempDirName + ConfigDrive.cloudStackConfigDriveName + dataType);
108-
if (typeFolder.exists() || typeFolder.mkdirs()) {
109-
if (StringUtils.isNotEmpty(content)) {
110-
File file = new File(typeFolder, fileName + ".txt");
111-
try {
112-
if (fileName.equals(USERDATA_FILE)) {
113-
// User Data is passed as a base64 encoded string
114-
FileUtils.writeByteArrayToFile(file, Base64.decodeBase64(content));
115-
} else {
116-
FileUtils.write(file, content, com.cloud.utils.StringUtils.getPreferredCharset());
117-
}
118-
} catch (IOException ex) {
119-
throw new CloudRuntimeException("Failed to create file ", ex);
120-
}
121-
}
122-
} else {
123-
throw new CloudRuntimeException("Failed to create folder: " + typeFolder);
124-
}
125-
126-
//now write the file to the OpenStack directory
127-
metaData = buildOpenStackMetaData(metaData, dataType, fileName, content);
128-
}
129-
}
130-
writeFile(openStackFolder, "meta_data.json", metaData.toString());
102+
writeVendorAndNetworkEmptyJsonFile(openStackFolder);
103+
writeVmMetadata(vmData, tempDirName, openStackFolder);
131104

132-
String linkResult = linkUserData(tempDirName);
133-
if (linkResult != null) {
134-
String errMsg = "Unable to create user_data link due to " + linkResult;
135-
throw new CloudRuntimeException(errMsg);
136-
}
105+
linkUserData(tempDirName);
137106

138-
File tmpIsoStore = new File(tempDirName, new File(isoFileName).getName());
139-
Script command = new Script("/usr/bin/genisoimage", Duration.standardSeconds(300), LOG);
140-
command.add("-o", tmpIsoStore.getAbsolutePath());
141-
command.add("-ldots");
142-
command.add("-allow-lowercase");
143-
command.add("-allow-multidot");
144-
command.add("-cache-inodes"); // Enable caching inode and device numbers to find hard links to files.
145-
command.add("-l");
146-
command.add("-quiet");
147-
command.add("-J");
148-
command.add("-r");
149-
command.add("-V", driveLabel);
150-
command.add(tempDirName);
151-
LOG.debug("Executing config drive creation command: " + command.toString());
152-
String result = command.execute();
153-
if (result != null) {
154-
String errMsg = "Unable to create iso file: " + isoFileName + " due to " + result;
155-
LOG.warn(errMsg);
156-
throw new CloudRuntimeException(errMsg);
157-
}
158-
File tmpIsoFile = new File(tmpIsoStore.getAbsolutePath());
159-
// Max allowed file size of config drive is 64MB: https://docs.openstack.org/project-install-guide/baremetal/draft/configdrive.html
160-
if (tmpIsoFile.length() > (64L * 1024L * 1024L)) {
161-
throw new CloudRuntimeException("Config drive file exceeds maximum allowed size of 64MB");
162-
}
163-
return fileToBase64String(tmpIsoFile);
107+
return generateAndRetrieveIsoAsBase64Iso(isoFileName, driveLabel, tempDirName);
164108
} catch (IOException e) {
165109
throw new CloudRuntimeException("Failed due to", e);
166110
} finally {
167-
try {
111+
deleteTempDir(tempDir);
112+
}
113+
}
114+
115+
private static void deleteTempDir(Path tempDir) {
116+
try {
117+
if (tempDir != null) {
168118
FileUtils.deleteDirectory(tempDir.toFile());
169-
} catch (IOException ioe) {
170-
LOG.warn("Failed to delete ConfigDrive temporary directory: " + tempDirName, ioe);
171119
}
120+
} catch (IOException ioe) {
121+
LOG.warn("Failed to delete ConfigDrive temporary directory: " + tempDir.toString(), ioe);
122+
}
123+
}
124+
125+
/**
126+
* Generates the ISO file that has the tempDir content.
127+
* We will use '/usr/bin/genisoimage' to create an ISO file based on tempDir content.
128+
*
129+
* Max allowed file size of config drive is 64MB [1]. Therefore, if the ISO is bigger than that, we throw a {@link CloudRuntimeException}.
130+
* [1] https://docs.openstack.org/project-install-guide/baremetal/draft/configdrive.html
131+
*/
132+
static String generateAndRetrieveIsoAsBase64Iso(String isoFileName, String driveLabel, String tempDirName) throws IOException {
133+
File tmpIsoStore = new File(tempDirName, isoFileName);
134+
Script command = new Script("/usr/bin/genisoimage", Duration.standardSeconds(300), LOG);
135+
command.add("-o", tmpIsoStore.getAbsolutePath());
136+
command.add("-ldots");
137+
command.add("-allow-lowercase");
138+
command.add("-allow-multidot");
139+
command.add("-cache-inodes"); // Enable caching inode and device numbers to find hard links to files.
140+
command.add("-l");
141+
command.add("-quiet");
142+
command.add("-J");
143+
command.add("-r");
144+
command.add("-V", driveLabel);
145+
command.add(tempDirName);
146+
LOG.debug("Executing config drive creation command: " + command.toString());
147+
String result = command.execute();
148+
if (StringUtils.isNotBlank(result)) {
149+
String errMsg = "Unable to create iso file: " + isoFileName + " due to " + result;
150+
LOG.warn(errMsg);
151+
throw new CloudRuntimeException(errMsg);
152+
}
153+
File tmpIsoFile = new File(tmpIsoStore.getAbsolutePath());
154+
if (tmpIsoFile.length() > (64L * 1024L * 1024L)) {
155+
throw new CloudRuntimeException("Config drive file exceeds maximum allowed size of 64MB");
156+
}
157+
return fileToBase64String(tmpIsoFile);
158+
}
159+
160+
/**
161+
* First we generate a JSON object using {@link #createJsonObjectWithVmData(List, String)}, then we write it to a file called "meta_data.json".
162+
*/
163+
static void writeVmMetadata(List<String[]> vmData, String tempDirName, File openStackFolder) {
164+
JsonObject metaData = createJsonObjectWithVmData(vmData, tempDirName);
165+
writeFile(openStackFolder, "meta_data.json", metaData.toString());
166+
}
167+
168+
/**
169+
* Writes the following empty JSON files:
170+
* <ul>
171+
* <li> vendor_data.json
172+
* <li> network_data.json
173+
* </ul>
174+
*
175+
* If the folder does not exist and we cannot create it, we throw a {@link CloudRuntimeException}.
176+
*/
177+
static void writeVendorAndNetworkEmptyJsonFile(File openStackFolder) {
178+
if (openStackFolder.exists() || openStackFolder.mkdirs()) {
179+
writeFile(openStackFolder, "vendor_data.json", "{}");
180+
writeFile(openStackFolder, "network_data.json", "{}");
181+
} else {
182+
throw new CloudRuntimeException("Failed to create folder " + openStackFolder);
172183
}
173184
}
174185

175-
private static String linkUserData(String tempDirName) {
176-
//Hard link the user_data.txt file with the user_data file in the OpenStack directory.
186+
/**
187+
* Creates the {@link JsonObject} with VM's metadata. The vmData is a list of arrays; we expect this list to have the following entries:
188+
* <ul>
189+
* <li> [0]: config data type
190+
* <li> [1]: config data file name
191+
* <li> [2]: config data file content
192+
* </ul>
193+
*/
194+
static JsonObject createJsonObjectWithVmData(List<String[]> vmData, String tempDirName) {
195+
JsonObject metaData = new JsonObject();
196+
for (String[] item : vmData) {
197+
String dataType = item[CONFIGDATA_DIR];
198+
String fileName = item[CONFIGDATA_FILE];
199+
String content = item[CONFIGDATA_CONTENT];
200+
LOG.debug(String.format("[createConfigDriveIsoForVM] dataType=%s, filename=%s, content=%s", dataType, fileName, (PASSWORD_FILE.equals(fileName) ? "********" : content)));
201+
202+
createFileInTempDirAnAppendOpenStackMetadataToJsonObject(tempDirName, metaData, dataType, fileName, content);
203+
}
204+
return metaData;
205+
}
206+
207+
static void createFileInTempDirAnAppendOpenStackMetadataToJsonObject(String tempDirName, JsonObject metaData, String dataType, String fileName, String content) {
208+
if (StringUtils.isBlank(dataType)) {
209+
return;
210+
}
211+
//create folder
212+
File typeFolder = new File(tempDirName + ConfigDrive.cloudStackConfigDriveName + dataType);
213+
if (!typeFolder.exists() && !typeFolder.mkdirs()) {
214+
throw new CloudRuntimeException("Failed to create folder: " + typeFolder);
215+
}
216+
if (StringUtils.isNotBlank(content)) {
217+
File file = new File(typeFolder, fileName + ".txt");
218+
try {
219+
if (fileName.equals(USERDATA_FILE)) {
220+
// User Data is passed as a base64 encoded string
221+
FileUtils.writeByteArrayToFile(file, Base64.decodeBase64(content));
222+
} else {
223+
FileUtils.write(file, content, com.cloud.utils.StringUtils.getPreferredCharset());
224+
}
225+
} catch (IOException ex) {
226+
throw new CloudRuntimeException("Failed to create file ", ex);
227+
}
228+
}
229+
230+
//now write the file to the OpenStack directory
231+
buildOpenStackMetaData(metaData, dataType, fileName, content);
232+
}
233+
234+
/**
235+
* Hard link the user_data.txt file with the user_data file in the OpenStack directory.
236+
*/
237+
static void linkUserData(String tempDirName) {
177238
String userDataFilePath = tempDirName + ConfigDrive.cloudStackConfigDriveName + "userdata/user_data.txt";
178-
if ((new File(userDataFilePath).exists())) {
239+
File file = new File(userDataFilePath);
240+
if (file.exists()) {
179241
Script hardLink = new Script("ln", Duration.standardSeconds(300), LOG);
180242
hardLink.add(userDataFilePath);
181243
hardLink.add(tempDirName + ConfigDrive.openStackConfigDriveName + "user_data");
182244
LOG.debug("execute command: " + hardLink.toString());
183-
return hardLink.execute();
245+
246+
String executionResult = hardLink.execute();
247+
if (StringUtils.isNotBlank(executionResult)) {
248+
throw new CloudRuntimeException("Unable to create user_data link due to " + executionResult);
249+
}
184250
}
185-
return null;
186251
}
187252

188253
private static JsonArray arrayOf(JsonElement... elements) {
@@ -193,30 +258,33 @@ private static JsonArray arrayOf(JsonElement... elements) {
193258
return array;
194259
}
195260

196-
private static JsonObject buildOpenStackMetaData(JsonObject metaData, String dataType, String fileName, String content) {
197-
if (dataType.equals(NetworkModel.METATDATA_DIR) && StringUtils.isNotEmpty(content)) {
198-
//keys are a special case in OpenStack format
199-
if (NetworkModel.PUBLIC_KEYS_FILE.equals(fileName)) {
200-
String[] keyArray = content.replace("\\n", "").split(" ");
201-
String keyName = "key";
202-
if (keyArray.length > 3 && StringUtils.isNotEmpty(keyArray[2])){
203-
keyName = keyArray[2];
204-
}
205-
206-
JsonObject keyLegacy = new JsonObject();
207-
keyLegacy.addProperty("type", "ssh");
208-
keyLegacy.addProperty("data", content.replace("\\n", ""));
209-
keyLegacy.addProperty("name", keyName);
210-
metaData.add("keys", arrayOf(keyLegacy));
211-
212-
JsonObject key = new JsonObject();
213-
key.addProperty(keyName, content);
214-
metaData.add("public_keys", key);
215-
} else if (NetworkModel.openStackFileMapping.get(fileName) != null) {
216-
metaData.addProperty(NetworkModel.openStackFileMapping.get(fileName), content);
261+
private static void buildOpenStackMetaData(JsonObject metaData, String dataType, String fileName, String content) {
262+
if (!NetworkModel.METATDATA_DIR.equals(dataType)) {
263+
return;
264+
}
265+
if (StringUtils.isNotBlank(content)) {
266+
return;
267+
}
268+
//keys are a special case in OpenStack format
269+
if (NetworkModel.PUBLIC_KEYS_FILE.equals(fileName)) {
270+
String[] keyArray = content.replace("\\n", "").split(" ");
271+
String keyName = "key";
272+
if (keyArray.length > 3 && StringUtils.isNotEmpty(keyArray[2])) {
273+
keyName = keyArray[2];
217274
}
275+
276+
JsonObject keyLegacy = new JsonObject();
277+
keyLegacy.addProperty("type", "ssh");
278+
keyLegacy.addProperty("data", content.replace("\\n", ""));
279+
keyLegacy.addProperty("name", keyName);
280+
metaData.add("keys", arrayOf(keyLegacy));
281+
282+
JsonObject key = new JsonObject();
283+
key.addProperty(keyName, content);
284+
metaData.add("public_keys", key);
285+
} else if (NetworkModel.openStackFileMapping.get(fileName) != null) {
286+
metaData.addProperty(NetworkModel.openStackFileMapping.get(fileName), content);
218287
}
219-
return metaData;
220288
}
221289

222290
}

0 commit comments

Comments
 (0)