2323import static com .cloud .network .NetworkModel .PASSWORD_FILE ;
2424import static com .cloud .network .NetworkModel .USERDATA_FILE ;
2525
26- import java .io .BufferedWriter ;
2726import java .io .File ;
28- import java .io .FileWriter ;
2927import java .io .IOException ;
3028import java .nio .charset .StandardCharsets ;
3129import java .nio .file .Files ;
4240import com .cloud .network .NetworkModel ;
4341import com .cloud .utils .exception .CloudRuntimeException ;
4442import com .cloud .utils .script .Script ;
45- import com .google .common .base .Strings ;
4643import com .google .gson .JsonArray ;
4744import com .google .gson .JsonElement ;
4845import 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