Skip to content

Commit 729e6d1

Browse files
authored
userdata: fix append scenarios (apache#7741)
Fixes case of appending userdata when both template and vm data are either shellscript or cloudconfig Fixes error when appending gzip userdata Fixes case when userdata manual text from VM is not getting decoded-encoded correctly. Fixes case of appending multipart data when both template and vm data contain same format types. Refactor - moved validateUserData method to UserDataManager class Refactor userdata test to check resultant multipart userdata thoroughly Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent 6bb95c0 commit 729e6d1

16 files changed

Lines changed: 443 additions & 275 deletions

File tree

api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
// under the License.
1717
package org.apache.cloudstack.userdata;
1818

19-
import com.cloud.utils.component.Manager;
19+
import org.apache.cloudstack.api.BaseCmd;
2020
import org.apache.cloudstack.framework.config.Configurable;
2121

22+
import com.cloud.utils.component.Manager;
23+
2224
public interface UserDataManager extends Manager, Configurable {
2325
String concatenateUserData(String userdata1, String userdata2, String userdataProvider);
26+
String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod);
2427
}

engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Map;
2121
import java.util.Set;
2222

23+
import org.apache.cloudstack.framework.config.ConfigKey;
2324
import org.apache.cloudstack.framework.config.impl.ConfigurationSubGroupVO;
2425

2526
import com.cloud.dc.ClusterVO;
@@ -59,6 +60,10 @@ public interface ConfigurationManager {
5960
public static final String MESSAGE_CREATE_VLAN_IP_RANGE_EVENT = "Message.CreateVlanIpRange.Event";
6061
public static final String MESSAGE_DELETE_VLAN_IP_RANGE_EVENT = "Message.DeleteVlanIpRange.Event";
6162

63+
static final String VM_USERDATA_MAX_LENGTH_STRING = "vm.userdata.max.length";
64+
static final ConfigKey<Integer> VM_USERDATA_MAX_LENGTH = new ConfigKey<>("Advanced", Integer.class, VM_USERDATA_MAX_LENGTH_STRING, "32768",
65+
"Max length of vm userdata after base64 decoding. Default is 32768 and maximum is 1048576", true);
66+
6267
/**
6368
* @param offering
6469
* @return

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

Lines changed: 106 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.io.ByteArrayInputStream;
2020
import java.io.ByteArrayOutputStream;
2121
import java.io.IOException;
22-
import java.nio.charset.StandardCharsets;
22+
import java.io.InputStream;
2323
import java.util.Arrays;
2424
import java.util.List;
2525
import java.util.Map;
@@ -35,12 +35,14 @@
3535
import javax.mail.internet.MimeMessage;
3636
import javax.mail.internet.MimeMultipart;
3737

38+
import org.apache.commons.codec.binary.Base64;
3839
import org.apache.commons.collections.CollectionUtils;
3940
import org.apache.commons.lang3.StringUtils;
4041
import org.apache.log4j.Logger;
4142

4243
import com.cloud.utils.component.AdapterBase;
4344
import com.cloud.utils.exception.CloudRuntimeException;
45+
import com.sun.mail.util.BASE64DecoderStream;
4446

4547
public class CloudInitUserDataProvider extends AdapterBase implements UserDataProvider {
4648

@@ -69,11 +71,11 @@ public String getName() {
6971
return "cloud-init";
7072
}
7173

72-
protected boolean isGZipped(String userdata) {
73-
if (StringUtils.isEmpty(userdata)) {
74+
protected boolean isGZipped(String encodedUserdata) {
75+
if (StringUtils.isEmpty(encodedUserdata)) {
7476
return false;
7577
}
76-
byte[] data = userdata.getBytes(StandardCharsets.ISO_8859_1);
78+
byte[] data = Base64.decodeBase64(encodedUserdata);
7779
if (data.length < 2) {
7880
return false;
7981
}
@@ -82,9 +84,6 @@ protected boolean isGZipped(String userdata) {
8284
}
8385

8486
protected String extractUserDataHeader(String userdata) {
85-
if (isGZipped(userdata)) {
86-
throw new CloudRuntimeException("Gzipped user data can not be used together with other user data formats");
87-
}
8887
List<String> lines = Arrays.stream(userdata.split("\n"))
8988
.filter(x -> (x.startsWith("#") && !x.startsWith("##")) || (x.startsWith("Content-Type:")))
9089
.collect(Collectors.toList());
@@ -131,7 +130,7 @@ protected FormatType getUserDataFormatType(String userdata) {
131130

132131
private String getContentType(String userData, FormatType formatType) throws MessagingException {
133132
if (formatType == FormatType.MIME) {
134-
MimeMessage msg = new MimeMessage(session, new ByteArrayInputStream(userData.getBytes()));
133+
NoIdMimeMessage msg = new NoIdMimeMessage(session, new ByteArrayInputStream(userData.getBytes()));
135134
return msg.getContentType();
136135
}
137136
if (!formatContentTypeMap.containsKey(formatType)) {
@@ -141,15 +140,35 @@ private String getContentType(String userData, FormatType formatType) throws Mes
141140
return formatContentTypeMap.get(formatType);
142141
}
143142

144-
protected MimeBodyPart generateBodyPartMIMEMessage(String userData, FormatType formatType) throws MessagingException {
143+
protected String getBodyPartContentAsString(BodyPart bodyPart) throws MessagingException, IOException {
144+
Object content = bodyPart.getContent();
145+
if (content instanceof BASE64DecoderStream) {
146+
return new String(((BASE64DecoderStream)bodyPart.getContent()).readAllBytes());
147+
} else if (content instanceof ByteArrayInputStream) {
148+
return new String(((ByteArrayInputStream)bodyPart.getContent()).readAllBytes());
149+
} else if (content instanceof String) {
150+
return (String)bodyPart.getContent();
151+
}
152+
throw new CloudRuntimeException(String.format("Failed to get content for multipart data with content type: %s", getBodyPartContentType(bodyPart)));
153+
}
154+
155+
private String getBodyPartContentType(BodyPart bodyPart) throws MessagingException {
156+
String contentType = StringUtils.defaultString(bodyPart.getDataHandler().getContentType(), bodyPart.getContentType());
157+
return contentType.contains(";") ? contentType.substring(0, contentType.indexOf(';')) : contentType;
158+
}
159+
160+
protected MimeBodyPart generateBodyPartMimeMessage(String userData, String contentType) throws MessagingException {
145161
MimeBodyPart bodyPart = new MimeBodyPart();
146-
String contentType = getContentType(userData, formatType);
147162
bodyPart.setContent(userData, contentType);
148163
bodyPart.addHeader("Content-Transfer-Encoding", "base64");
149164
return bodyPart;
150165
}
151166

152-
private Multipart getMessageContent(MimeMessage message) {
167+
protected MimeBodyPart generateBodyPartMimeMessage(String userData, FormatType formatType) throws MessagingException {
168+
return generateBodyPartMimeMessage(userData, getContentType(userData, formatType));
169+
}
170+
171+
private Multipart getMessageContent(NoIdMimeMessage message) {
153172
Multipart messageContent;
154173
try {
155174
messageContent = (MimeMultipart) message.getContent();
@@ -159,40 +178,83 @@ private Multipart getMessageContent(MimeMessage message) {
159178
return messageContent;
160179
}
161180

162-
private void addBodyPartsToMessageContentFromUserDataContent(Multipart messageContent,
163-
MimeMessage msgFromUserdata) throws MessagingException, IOException {
164-
Multipart msgFromUserdataParts = (MimeMultipart) msgFromUserdata.getContent();
165-
int count = msgFromUserdataParts.getCount();
166-
int i = 0;
167-
while (i < count) {
168-
BodyPart bodyPart = msgFromUserdataParts.getBodyPart(0);
169-
messageContent.addBodyPart(bodyPart);
170-
i++;
181+
private void addBodyPartToMultipart(Multipart existingMultipart, MimeBodyPart bodyPart) throws MessagingException, IOException {
182+
boolean added = false;
183+
final int existingCount = existingMultipart.getCount();
184+
for (int j = 0; j < existingCount; ++j) {
185+
MimeBodyPart existingBodyPart = (MimeBodyPart)existingMultipart.getBodyPart(j);
186+
String existingContentType = getBodyPartContentType(existingBodyPart);
187+
String newContentType = getBodyPartContentType(bodyPart);
188+
if (existingContentType.equals(newContentType)) {
189+
String existingContent = getBodyPartContentAsString(existingBodyPart);
190+
String newContent = getBodyPartContentAsString(bodyPart);
191+
// generating a combined content MimeBodyPart to replace
192+
MimeBodyPart combinedBodyPart = generateBodyPartMimeMessage(
193+
simpleAppendSameFormatTypeUserData(existingContent, newContent), existingContentType);
194+
existingMultipart.removeBodyPart(j);
195+
existingMultipart.addBodyPart(combinedBodyPart, j);
196+
added = true;
197+
break;
198+
}
199+
}
200+
if (!added) {
201+
existingMultipart.addBodyPart(bodyPart);
202+
}
203+
}
204+
205+
private void addBodyPartsToMessageContentFromUserDataContent(Multipart existingMultipart,
206+
NoIdMimeMessage msgFromUserdata) throws MessagingException, IOException {
207+
MimeMultipart newMultipart = (MimeMultipart)msgFromUserdata.getContent();
208+
final int existingCount = existingMultipart.getCount();
209+
final int newCount = newMultipart.getCount();
210+
for (int i = 0; i < newCount; ++i) {
211+
BodyPart bodyPart = newMultipart.getBodyPart(i);
212+
if (existingCount == 0) {
213+
existingMultipart.addBodyPart(bodyPart);
214+
continue;
215+
}
216+
addBodyPartToMultipart(existingMultipart, (MimeBodyPart)bodyPart);
171217
}
172218
}
173219

174-
private MimeMessage createMultipartMessageAddingUserdata(String userData, FormatType formatType,
175-
MimeMessage message) throws MessagingException, IOException {
176-
MimeMessage newMessage = new MimeMessage(session);
220+
private NoIdMimeMessage createMultipartMessageAddingUserdata(String userData, FormatType formatType,
221+
NoIdMimeMessage message) throws MessagingException, IOException {
222+
NoIdMimeMessage newMessage = new NoIdMimeMessage(session);
177223
Multipart messageContent = getMessageContent(message);
178224

179225
if (formatType == FormatType.MIME) {
180-
MimeMessage msgFromUserdata = new MimeMessage(session, new ByteArrayInputStream(userData.getBytes()));
226+
NoIdMimeMessage msgFromUserdata = new NoIdMimeMessage(session, new ByteArrayInputStream(userData.getBytes()));
181227
addBodyPartsToMessageContentFromUserDataContent(messageContent, msgFromUserdata);
182228
} else {
183-
MimeBodyPart part = generateBodyPartMIMEMessage(userData, formatType);
184-
messageContent.addBodyPart(part);
229+
MimeBodyPart part = generateBodyPartMimeMessage(userData, formatType);
230+
addBodyPartToMultipart(messageContent, part);
185231
}
186232
newMessage.setContent(messageContent);
187233
return newMessage;
188234
}
189235

236+
private String simpleAppendSameFormatTypeUserData(String userData1, String userData2) {
237+
return String.format("%s\n\n%s", userData1, userData2.substring(userData2.indexOf('\n')+1));
238+
}
239+
240+
private void checkGzipAppend(String encodedUserData1, String encodedUserData2) {
241+
if (isGZipped(encodedUserData1) || isGZipped(encodedUserData2)) {
242+
throw new CloudRuntimeException("Gzipped user data can not be used together with other user data formats");
243+
}
244+
}
245+
190246
@Override
191-
public String appendUserData(String userData1, String userData2) {
247+
public String appendUserData(String encodedUserData1, String encodedUserData2) {
192248
try {
249+
checkGzipAppend(encodedUserData1, encodedUserData2);
250+
String userData1 = new String(Base64.decodeBase64(encodedUserData1));
251+
String userData2 = new String(Base64.decodeBase64(encodedUserData2));
193252
FormatType formatType1 = getUserDataFormatType(userData1);
194253
FormatType formatType2 = getUserDataFormatType(userData2);
195-
MimeMessage message = new MimeMessage(session);
254+
if (formatType1.equals(formatType2) && List.of(FormatType.CLOUD_CONFIG, FormatType.BASH_SCRIPT).contains(formatType1)) {
255+
return simpleAppendSameFormatTypeUserData(userData1, userData2);
256+
}
257+
NoIdMimeMessage message = new NoIdMimeMessage(session);
196258
message = createMultipartMessageAddingUserdata(userData1, formatType1, message);
197259
message = createMultipartMessageAddingUserdata(userData2, formatType2, message);
198260
ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -205,4 +267,20 @@ public String appendUserData(String userData1, String userData2) {
205267
throw new CloudRuntimeException(msg, e);
206268
}
207269
}
270+
271+
/* This is a wrapper class just to remove Message-ID header from the resultant
272+
multipart data which may contain server details.
273+
*/
274+
private class NoIdMimeMessage extends MimeMessage {
275+
NoIdMimeMessage (Session session) {
276+
super(session);
277+
}
278+
NoIdMimeMessage (Session session, InputStream is) throws MessagingException {
279+
super(session, is);
280+
}
281+
@Override
282+
protected void updateMessageID() throws MessagingException {
283+
removeHeader("Message-ID");
284+
}
285+
}
208286
}

0 commit comments

Comments
 (0)