diff --git a/engine/userdata/cloud-init/src/main/java/org/apache/cloudstack/userdata/CloudInitUserDataProvider.java b/engine/userdata/cloud-init/src/main/java/org/apache/cloudstack/userdata/CloudInitUserDataProvider.java index 6e1086c631e..6f633f6abb2 100644 --- a/engine/userdata/cloud-init/src/main/java/org/apache/cloudstack/userdata/CloudInitUserDataProvider.java +++ b/engine/userdata/cloud-init/src/main/java/org/apache/cloudstack/userdata/CloudInitUserDataProvider.java @@ -85,14 +85,20 @@ public class CloudInitUserDataProvider extends AdapterBase implements UserDataPr .filter(x -> (x.startsWith("#") && !x.startsWith("##")) || (x.startsWith("Content-Type:"))) .collect(Collectors.toList()); if (CollectionUtils.isEmpty(lines)) { - throw new CloudRuntimeException("Failed to detect the user data format type as it " + - "does not contain a header"); + LOGGER.debug("Failed to detect the user data format type as it does not contain a header"); + return null; } return lines.get(0); } - protected FormatType mapUserDataHeaderToFormatType(String header) { - if (header.equalsIgnoreCase("#cloud-config")) { + protected FormatType mapUserDataHeaderToFormatType(String header, FormatType defaultFormatType) { + if (StringUtils.isBlank(header)) { + if (defaultFormatType == null) { + throw new CloudRuntimeException("Failed to detect the user data format type as it does not contain a header"); + } + LOGGER.debug(String.format("Empty header for userdata, using the default format type: %s", defaultFormatType.name())); + return defaultFormatType; + } else if (header.equalsIgnoreCase("#cloud-config")) { return FormatType.CLOUD_CONFIG; } else if (header.startsWith("#!")) { return FormatType.BASH_SCRIPT; @@ -112,9 +118,11 @@ public class CloudInitUserDataProvider extends AdapterBase implements UserDataPr /** * Detect the user data type + * @param userdata the userdata string to detect the type + * @param defaultFormatType if not null, then use it in case the header does not exist in the userdata, otherwise fail * Reference: */ - protected FormatType getUserDataFormatType(String userdata) { + protected FormatType getUserDataFormatType(String userdata, FormatType defaultFormatType) { if (StringUtils.isBlank(userdata)) { String msg = "User data expected but provided empty user data"; logger.error(msg); @@ -122,7 +130,7 @@ public class CloudInitUserDataProvider extends AdapterBase implements UserDataPr } String header = extractUserDataHeader(userdata); - return mapUserDataHeaderToFormatType(header); + return mapUserDataHeaderToFormatType(header, defaultFormatType); } private String getContentType(String userData, FormatType formatType) throws MessagingException { @@ -231,7 +239,9 @@ public class CloudInitUserDataProvider extends AdapterBase implements UserDataPr } private String simpleAppendSameFormatTypeUserData(String userData1, String userData2) { - return String.format("%s\n\n%s", userData1, userData2.substring(userData2.indexOf('\n')+1)); + String userdata2Header = extractUserDataHeader(userData2); + int beginIndex = StringUtils.isNotBlank(userdata2Header) ? userData2.indexOf('\n')+1 : 0; + return String.format("%s\n\n%s", userData1, userData2.substring(beginIndex)); } private void checkGzipAppend(String encodedUserData1, String encodedUserData2) { @@ -246,8 +256,8 @@ public class CloudInitUserDataProvider extends AdapterBase implements UserDataPr checkGzipAppend(encodedUserData1, encodedUserData2); String userData1 = new String(Base64.decodeBase64(encodedUserData1)); String userData2 = new String(Base64.decodeBase64(encodedUserData2)); - FormatType formatType1 = getUserDataFormatType(userData1); - FormatType formatType2 = getUserDataFormatType(userData2); + FormatType formatType1 = getUserDataFormatType(userData1, null); + FormatType formatType2 = getUserDataFormatType(userData2, formatType1); if (formatType1.equals(formatType2) && List.of(FormatType.CLOUD_CONFIG, FormatType.BASH_SCRIPT).contains(formatType1)) { return simpleAppendSameFormatTypeUserData(userData1, userData2); } diff --git a/engine/userdata/cloud-init/src/test/java/org/apache/cloudstack/userdata/CloudInitUserDataProviderTest.java b/engine/userdata/cloud-init/src/test/java/org/apache/cloudstack/userdata/CloudInitUserDataProviderTest.java index 4ca9fb7ebd6..86b6a6fb6ea 100644 --- a/engine/userdata/cloud-init/src/test/java/org/apache/cloudstack/userdata/CloudInitUserDataProviderTest.java +++ b/engine/userdata/cloud-init/src/test/java/org/apache/cloudstack/userdata/CloudInitUserDataProviderTest.java @@ -73,21 +73,28 @@ public class CloudInitUserDataProviderTest { @Test public void testGetUserDataFormatType() { - CloudInitUserDataProvider.FormatType type = provider.getUserDataFormatType(CLOUD_CONFIG_USERDATA); + CloudInitUserDataProvider.FormatType type = provider.getUserDataFormatType(CLOUD_CONFIG_USERDATA, null); Assert.assertEquals(CloudInitUserDataProvider.FormatType.CLOUD_CONFIG, type); } @Test(expected = CloudRuntimeException.class) public void testGetUserDataFormatTypeNoHeader() { String userdata = "password: password\nchpasswd: { expire: False }\nssh_pwauth: True"; - provider.getUserDataFormatType(userdata); + provider.getUserDataFormatType(userdata, null); + } + + @Test + public void testGetUserDataFormatTypeNoHeaderDefaultFormat() { + String userdata = "password: password\nchpasswd: { expire: False }\nssh_pwauth: True"; + CloudInitUserDataProvider.FormatType defaultFormatType = CloudInitUserDataProvider.FormatType.CLOUD_CONFIG; + Assert.assertEquals(defaultFormatType, provider.getUserDataFormatType(userdata, defaultFormatType)); } @Test(expected = CloudRuntimeException.class) public void testGetUserDataFormatTypeInvalidType() { String userdata = "#invalid-type\n" + "password: password\nchpasswd: { expire: False }\nssh_pwauth: True"; - provider.getUserDataFormatType(userdata); + provider.getUserDataFormatType(userdata, null); } private MimeMultipart getCheckedMultipartFromMultipartData(String multipartUserData, int count) { @@ -111,6 +118,16 @@ public class CloudInitUserDataProviderTest { getCheckedMultipartFromMultipartData(multipartUserData, 2); } + @Test + public void testAppendUserDataSecondWithoutHeader() { + String userdataWithHeader = Base64.encodeBase64String(SHELL_SCRIPT_USERDATA1.getBytes()); + String bashScriptWithoutHeader = "echo \"without header\""; + String userdataWithoutHeader = Base64.encodeBase64String(bashScriptWithoutHeader.getBytes()); + String appended = provider.appendUserData(userdataWithHeader, userdataWithoutHeader); + String expected = String.format("%s\n\n%s", SHELL_SCRIPT_USERDATA1, bashScriptWithoutHeader); + Assert.assertEquals(expected, appended); + } + @Test public void testAppendSameShellScriptTypeUserData() { String result = SHELL_SCRIPT_USERDATA + "\n\n" + @@ -129,6 +146,22 @@ public class CloudInitUserDataProviderTest { Assert.assertEquals(result, appendUserData); } + @Test + public void testAppendCloudConfig() { + String userdata1 = "#cloud-config\n" + + "chpasswd:\n" + + " list: |\n" + + " root:password\n" + + " expire: False"; + String userdata2 = "write_files:\n" + + "- path: /root/CLOUD_INIT_WAS_HERE"; + String userdataWithHeader = Base64.encodeBase64String(userdata1.getBytes()); + String userdataWithoutHeader = Base64.encodeBase64String(userdata2.getBytes()); + String appended = provider.appendUserData(userdataWithHeader, userdataWithoutHeader); + String expected = String.format("%s\n\n%s", userdata1, userdata2); + Assert.assertEquals(expected, appended); + } + @Test public void testAppendUserDataMIMETemplateData() { String multipartUserData = provider.appendUserData( diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 5a779da182b..27bd6a2fb36 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -253,6 +253,12 @@ {{ text }} + diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index a4a598103e3..26b4f279e3d 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -1370,7 +1370,7 @@ export default { permission: ['listGuestVlans'], resourceType: 'GuestVlan', filters: ['allocatedonly', 'all'], - columns: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'zonename'], + columns: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'zonename', 'guest.networks'], details: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'isdedicated', 'zonename'], searchFilters: ['zoneid'], tabs: [{ diff --git a/ui/src/utils/plugins.js b/ui/src/utils/plugins.js index 87ad11fbd46..949ea910db3 100644 --- a/ui/src/utils/plugins.js +++ b/ui/src/utils/plugins.js @@ -495,7 +495,7 @@ export const genericUtilPlugin = { if (isBase64(text)) { return text } - return encodeURIComponent(btoa(unescape(encodeURIComponent(text)))) + return encodeURI(btoa(unescape(encodeURIComponent(text)))) } } }