Skip to content

Commit 6e988b8

Browse files
committed
Merge remote-tracking branch 'apache/4.19'
2 parents 12d9c26 + 0204cb7 commit 6e988b8

5 files changed

Lines changed: 63 additions & 14 deletions

File tree

engine/userdata/cloud-init/src/main/java/org/apache/cloudstack/userdata/CloudInitUserDataProvider.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,20 @@ protected String extractUserDataHeader(String userdata) {
8585
.filter(x -> (x.startsWith("#") && !x.startsWith("##")) || (x.startsWith("Content-Type:")))
8686
.collect(Collectors.toList());
8787
if (CollectionUtils.isEmpty(lines)) {
88-
throw new CloudRuntimeException("Failed to detect the user data format type as it " +
89-
"does not contain a header");
88+
LOGGER.debug("Failed to detect the user data format type as it does not contain a header");
89+
return null;
9090
}
9191
return lines.get(0);
9292
}
9393

94-
protected FormatType mapUserDataHeaderToFormatType(String header) {
95-
if (header.equalsIgnoreCase("#cloud-config")) {
94+
protected FormatType mapUserDataHeaderToFormatType(String header, FormatType defaultFormatType) {
95+
if (StringUtils.isBlank(header)) {
96+
if (defaultFormatType == null) {
97+
throw new CloudRuntimeException("Failed to detect the user data format type as it does not contain a header");
98+
}
99+
LOGGER.debug(String.format("Empty header for userdata, using the default format type: %s", defaultFormatType.name()));
100+
return defaultFormatType;
101+
} else if (header.equalsIgnoreCase("#cloud-config")) {
96102
return FormatType.CLOUD_CONFIG;
97103
} else if (header.startsWith("#!")) {
98104
return FormatType.BASH_SCRIPT;
@@ -112,17 +118,19 @@ protected FormatType mapUserDataHeaderToFormatType(String header) {
112118

113119
/**
114120
* Detect the user data type
121+
* @param userdata the userdata string to detect the type
122+
* @param defaultFormatType if not null, then use it in case the header does not exist in the userdata, otherwise fail
115123
* Reference: <a href="https://canonical-cloud-init.readthedocs-hosted.com/en/latest/explanation/format.html#user-data-formats" />
116124
*/
117-
protected FormatType getUserDataFormatType(String userdata) {
125+
protected FormatType getUserDataFormatType(String userdata, FormatType defaultFormatType) {
118126
if (StringUtils.isBlank(userdata)) {
119127
String msg = "User data expected but provided empty user data";
120128
logger.error(msg);
121129
throw new CloudRuntimeException(msg);
122130
}
123131

124132
String header = extractUserDataHeader(userdata);
125-
return mapUserDataHeaderToFormatType(header);
133+
return mapUserDataHeaderToFormatType(header, defaultFormatType);
126134
}
127135

128136
private String getContentType(String userData, FormatType formatType) throws MessagingException {
@@ -231,7 +239,9 @@ private NoIdMimeMessage createMultipartMessageAddingUserdata(String userData, Fo
231239
}
232240

233241
private String simpleAppendSameFormatTypeUserData(String userData1, String userData2) {
234-
return String.format("%s\n\n%s", userData1, userData2.substring(userData2.indexOf('\n')+1));
242+
String userdata2Header = extractUserDataHeader(userData2);
243+
int beginIndex = StringUtils.isNotBlank(userdata2Header) ? userData2.indexOf('\n')+1 : 0;
244+
return String.format("%s\n\n%s", userData1, userData2.substring(beginIndex));
235245
}
236246

237247
private void checkGzipAppend(String encodedUserData1, String encodedUserData2) {
@@ -246,8 +256,8 @@ public String appendUserData(String encodedUserData1, String encodedUserData2) {
246256
checkGzipAppend(encodedUserData1, encodedUserData2);
247257
String userData1 = new String(Base64.decodeBase64(encodedUserData1));
248258
String userData2 = new String(Base64.decodeBase64(encodedUserData2));
249-
FormatType formatType1 = getUserDataFormatType(userData1);
250-
FormatType formatType2 = getUserDataFormatType(userData2);
259+
FormatType formatType1 = getUserDataFormatType(userData1, null);
260+
FormatType formatType2 = getUserDataFormatType(userData2, formatType1);
251261
if (formatType1.equals(formatType2) && List.of(FormatType.CLOUD_CONFIG, FormatType.BASH_SCRIPT).contains(formatType1)) {
252262
return simpleAppendSameFormatTypeUserData(userData1, userData2);
253263
}

engine/userdata/cloud-init/src/test/java/org/apache/cloudstack/userdata/CloudInitUserDataProviderTest.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,28 @@ public class CloudInitUserDataProviderTest {
7373

7474
@Test
7575
public void testGetUserDataFormatType() {
76-
CloudInitUserDataProvider.FormatType type = provider.getUserDataFormatType(CLOUD_CONFIG_USERDATA);
76+
CloudInitUserDataProvider.FormatType type = provider.getUserDataFormatType(CLOUD_CONFIG_USERDATA, null);
7777
Assert.assertEquals(CloudInitUserDataProvider.FormatType.CLOUD_CONFIG, type);
7878
}
7979

8080
@Test(expected = CloudRuntimeException.class)
8181
public void testGetUserDataFormatTypeNoHeader() {
8282
String userdata = "password: password\nchpasswd: { expire: False }\nssh_pwauth: True";
83-
provider.getUserDataFormatType(userdata);
83+
provider.getUserDataFormatType(userdata, null);
84+
}
85+
86+
@Test
87+
public void testGetUserDataFormatTypeNoHeaderDefaultFormat() {
88+
String userdata = "password: password\nchpasswd: { expire: False }\nssh_pwauth: True";
89+
CloudInitUserDataProvider.FormatType defaultFormatType = CloudInitUserDataProvider.FormatType.CLOUD_CONFIG;
90+
Assert.assertEquals(defaultFormatType, provider.getUserDataFormatType(userdata, defaultFormatType));
8491
}
8592

8693
@Test(expected = CloudRuntimeException.class)
8794
public void testGetUserDataFormatTypeInvalidType() {
8895
String userdata = "#invalid-type\n" +
8996
"password: password\nchpasswd: { expire: False }\nssh_pwauth: True";
90-
provider.getUserDataFormatType(userdata);
97+
provider.getUserDataFormatType(userdata, null);
9198
}
9299

93100
private MimeMultipart getCheckedMultipartFromMultipartData(String multipartUserData, int count) {
@@ -111,6 +118,16 @@ public void testAppendUserData() {
111118
getCheckedMultipartFromMultipartData(multipartUserData, 2);
112119
}
113120

121+
@Test
122+
public void testAppendUserDataSecondWithoutHeader() {
123+
String userdataWithHeader = Base64.encodeBase64String(SHELL_SCRIPT_USERDATA1.getBytes());
124+
String bashScriptWithoutHeader = "echo \"without header\"";
125+
String userdataWithoutHeader = Base64.encodeBase64String(bashScriptWithoutHeader.getBytes());
126+
String appended = provider.appendUserData(userdataWithHeader, userdataWithoutHeader);
127+
String expected = String.format("%s\n\n%s", SHELL_SCRIPT_USERDATA1, bashScriptWithoutHeader);
128+
Assert.assertEquals(expected, appended);
129+
}
130+
114131
@Test
115132
public void testAppendSameShellScriptTypeUserData() {
116133
String result = SHELL_SCRIPT_USERDATA + "\n\n" +
@@ -129,6 +146,22 @@ public void testAppendSameCloudConfigTypeUserData() {
129146
Assert.assertEquals(result, appendUserData);
130147
}
131148

149+
@Test
150+
public void testAppendCloudConfig() {
151+
String userdata1 = "#cloud-config\n" +
152+
"chpasswd:\n" +
153+
" list: |\n" +
154+
" root:password\n" +
155+
" expire: False";
156+
String userdata2 = "write_files:\n" +
157+
"- path: /root/CLOUD_INIT_WAS_HERE";
158+
String userdataWithHeader = Base64.encodeBase64String(userdata1.getBytes());
159+
String userdataWithoutHeader = Base64.encodeBase64String(userdata2.getBytes());
160+
String appended = provider.appendUserData(userdataWithHeader, userdataWithoutHeader);
161+
String expected = String.format("%s\n\n%s", userdata1, userdata2);
162+
Assert.assertEquals(expected, appended);
163+
}
164+
132165
@Test
133166
public void testAppendUserDataMIMETemplateData() {
134167
String multipartUserData = provider.appendUserData(

ui/src/components/view/ListView.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@
253253
{{ text }}
254254
</router-link>
255255
</template>
256+
<template v-if="column.key === 'guest.networks' && record.network">
257+
<template v-for="(item, idx) in record.network" :key="idx">
258+
<router-link :to="{ path: '/guestnetwork/' + item.id }">{{ item.name }}</router-link>
259+
<span v-if="idx < (record.network.length - 1)">, </span>
260+
</template>
261+
</template>
256262
<template v-if="column.key === 'associatednetworkname'">
257263
<router-link :to="{ path: '/guestnetwork/' + record.associatednetworkid }">{{ text }}</router-link>
258264
</template>

ui/src/config/section/network.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1370,7 +1370,7 @@ export default {
13701370
permission: ['listGuestVlans'],
13711371
resourceType: 'GuestVlan',
13721372
filters: ['allocatedonly', 'all'],
1373-
columns: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'zonename'],
1373+
columns: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'zonename', 'guest.networks'],
13741374
details: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'isdedicated', 'zonename'],
13751375
searchFilters: ['zoneid'],
13761376
tabs: [{

ui/src/utils/plugins.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ export const genericUtilPlugin = {
495495
if (isBase64(text)) {
496496
return text
497497
}
498-
return encodeURIComponent(btoa(unescape(encodeURIComponent(text))))
498+
return encodeURI(btoa(unescape(encodeURIComponent(text))))
499499
}
500500
}
501501
}

0 commit comments

Comments
 (0)