Skip to content

Commit 62fce86

Browse files
nvazquezshwstppr
authored andcommitted
Generate cloud-init multipart user data for template append policy (apache#7643)
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> Co-authored-by: Abhishek Kumar <abhishek.mrt22@gmail.com> (cherry picked from commit b1fc279) Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
1 parent c6c3277 commit 62fce86

File tree

16 files changed

+669
-43
lines changed

16 files changed

+669
-43
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.userdata;
18+
19+
import com.cloud.utils.component.Manager;
20+
import org.apache.cloudstack.framework.config.Configurable;
21+
22+
public interface UserDataManager extends Manager, Configurable {
23+
String concatenateUserData(String userdata1, String userdata2, String userdataProvider);
24+
}

client/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,16 @@
352352
<artifactId>cloud-plugin-outofbandmanagement-driver-redfish</artifactId>
353353
<version>${project.version}</version>
354354
</dependency>
355+
<dependency>
356+
<groupId>org.apache.cloudstack</groupId>
357+
<artifactId>cloud-engine-userdata-cloud-init</artifactId>
358+
<version>${project.version}</version>
359+
</dependency>
360+
<dependency>
361+
<groupId>org.apache.cloudstack</groupId>
362+
<artifactId>cloud-engine-userdata</artifactId>
363+
<version>${project.version}</version>
364+
</dependency>
355365
<dependency>
356366
<groupId>org.apache.cloudstack</groupId>
357367
<artifactId>cloud-mom-rabbitmq</artifactId>

core/src/main/resources/META-INF/cloudstack/core/spring-core-lifecycle-core-context-inheritable.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,10 @@
3939
<property name="typeClass"
4040
value="com.cloud.utils.component.PluggableService" />
4141
</bean>
42-
42+
43+
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
44+
<property name="registry" ref="userDataProvidersRegistry" />
45+
<property name="typeClass" value="org.apache.cloudstack.userdata.UserDataProvider" />
46+
</bean>
47+
4348
</beans>

core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,8 @@
342342
<bean id="kubernetesClusterHelperRegistry"
343343
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
344344
</bean>
345+
346+
<bean id="userDataProvidersRegistry"
347+
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
348+
</bean>
345349
</beans>

engine/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
<module>storage/image</module>
5959
<module>storage/snapshot</module>
6060
<module>storage/volume</module>
61+
<module>userdata/cloud-init</module>
62+
<module>userdata</module>
6163
</modules>
6264
<profiles>
6365
<profile>

engine/userdata/cloud-init/pom.xml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
20+
<modelVersion>4.0.0</modelVersion>
21+
<artifactId>cloud-engine-userdata-cloud-init</artifactId>
22+
<name>Apache CloudStack Engine Cloud-Init Userdata Component</name>
23+
<parent>
24+
<artifactId>cloud-engine</artifactId>
25+
<groupId>org.apache.cloudstack</groupId>
26+
<version>4.18.1.0</version>
27+
<relativePath>../../pom.xml</relativePath>
28+
</parent>
29+
<dependencies>
30+
<dependency>
31+
<groupId>org.apache.cloudstack</groupId>
32+
<artifactId>cloud-engine-userdata</artifactId>
33+
<version>${project.version}</version>
34+
</dependency>
35+
</dependencies>
36+
</project>
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.userdata;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.ByteArrayOutputStream;
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.Arrays;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Properties;
27+
import java.util.stream.Collectors;
28+
import java.util.zip.GZIPInputStream;
29+
30+
import javax.mail.BodyPart;
31+
import javax.mail.MessagingException;
32+
import javax.mail.Multipart;
33+
import javax.mail.Session;
34+
import javax.mail.internet.MimeBodyPart;
35+
import javax.mail.internet.MimeMessage;
36+
import javax.mail.internet.MimeMultipart;
37+
38+
import org.apache.commons.collections.CollectionUtils;
39+
import org.apache.commons.lang3.StringUtils;
40+
import org.apache.log4j.Logger;
41+
42+
import com.cloud.utils.component.AdapterBase;
43+
import com.cloud.utils.exception.CloudRuntimeException;
44+
45+
public class CloudInitUserDataProvider extends AdapterBase implements UserDataProvider {
46+
47+
protected enum FormatType {
48+
CLOUD_CONFIG, BASH_SCRIPT, MIME, CLOUD_BOOTHOOK, INCLUDE_FILE
49+
}
50+
51+
private static final String CLOUD_CONFIG_CONTENT_TYPE = "text/cloud-config";
52+
private static final String BASH_SCRIPT_CONTENT_TYPE = "text/x-shellscript";
53+
private static final String INCLUDE_FILE_CONTENT_TYPE = "text/x-include-url";
54+
private static final String CLOUD_BOOTHOOK_CONTENT_TYPE = "text/cloud-boothook";
55+
56+
private static final Map<FormatType, String> formatContentTypeMap = Map.ofEntries(
57+
Map.entry(FormatType.CLOUD_CONFIG, CLOUD_CONFIG_CONTENT_TYPE),
58+
Map.entry(FormatType.BASH_SCRIPT, BASH_SCRIPT_CONTENT_TYPE),
59+
Map.entry(FormatType.CLOUD_BOOTHOOK, CLOUD_BOOTHOOK_CONTENT_TYPE),
60+
Map.entry(FormatType.INCLUDE_FILE, INCLUDE_FILE_CONTENT_TYPE)
61+
);
62+
63+
private static final Logger LOGGER = Logger.getLogger(CloudInitUserDataProvider.class);
64+
65+
private static final Session session = Session.getDefaultInstance(new Properties());
66+
67+
@Override
68+
public String getName() {
69+
return "cloud-init";
70+
}
71+
72+
protected boolean isGZipped(String userdata) {
73+
if (StringUtils.isEmpty(userdata)) {
74+
return false;
75+
}
76+
byte[] data = userdata.getBytes(StandardCharsets.ISO_8859_1);
77+
if (data.length < 2) {
78+
return false;
79+
}
80+
int magic = data[0] & 0xff | ((data[1] << 8) & 0xff00);
81+
return magic == GZIPInputStream.GZIP_MAGIC;
82+
}
83+
84+
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+
}
88+
List<String> lines = Arrays.stream(userdata.split("\n"))
89+
.filter(x -> (x.startsWith("#") && !x.startsWith("##")) || (x.startsWith("Content-Type:")))
90+
.collect(Collectors.toList());
91+
if (CollectionUtils.isEmpty(lines)) {
92+
throw new CloudRuntimeException("Failed to detect the user data format type as it " +
93+
"does not contain a header");
94+
}
95+
return lines.get(0);
96+
}
97+
98+
protected FormatType mapUserDataHeaderToFormatType(String header) {
99+
if (header.equalsIgnoreCase("#cloud-config")) {
100+
return FormatType.CLOUD_CONFIG;
101+
} else if (header.startsWith("#!")) {
102+
return FormatType.BASH_SCRIPT;
103+
} else if (header.equalsIgnoreCase("#cloud-boothook")) {
104+
return FormatType.CLOUD_BOOTHOOK;
105+
} else if (header.startsWith("#include")) {
106+
return FormatType.INCLUDE_FILE;
107+
} else if (header.startsWith("Content-Type:")) {
108+
return FormatType.MIME;
109+
} else {
110+
String msg = String.format("Cannot recognise the user data format type from the header line: %s." +
111+
"Supported types are: cloud-config, bash script, cloud-boothook, include file or MIME", header);
112+
LOGGER.error(msg);
113+
throw new CloudRuntimeException(msg);
114+
}
115+
}
116+
117+
/**
118+
* Detect the user data type
119+
* Reference: <a href="https://canonical-cloud-init.readthedocs-hosted.com/en/latest/explanation/format.html#user-data-formats" />
120+
*/
121+
protected FormatType getUserDataFormatType(String userdata) {
122+
if (StringUtils.isBlank(userdata)) {
123+
String msg = "User data expected but provided empty user data";
124+
LOGGER.error(msg);
125+
throw new CloudRuntimeException(msg);
126+
}
127+
128+
String header = extractUserDataHeader(userdata);
129+
return mapUserDataHeaderToFormatType(header);
130+
}
131+
132+
private String getContentType(String userData, FormatType formatType) throws MessagingException {
133+
if (formatType == FormatType.MIME) {
134+
MimeMessage msg = new MimeMessage(session, new ByteArrayInputStream(userData.getBytes()));
135+
return msg.getContentType();
136+
}
137+
if (!formatContentTypeMap.containsKey(formatType)) {
138+
throw new CloudRuntimeException(String.format("Cannot get the user data content type as " +
139+
"its format type %s is invalid", formatType.name()));
140+
}
141+
return formatContentTypeMap.get(formatType);
142+
}
143+
144+
protected MimeBodyPart generateBodyPartMIMEMessage(String userData, FormatType formatType) throws MessagingException {
145+
MimeBodyPart bodyPart = new MimeBodyPart();
146+
String contentType = getContentType(userData, formatType);
147+
bodyPart.setContent(userData, contentType);
148+
bodyPart.addHeader("Content-Transfer-Encoding", "base64");
149+
return bodyPart;
150+
}
151+
152+
private Multipart getMessageContent(MimeMessage message) {
153+
Multipart messageContent;
154+
try {
155+
messageContent = (MimeMultipart) message.getContent();
156+
} catch (IOException | MessagingException e) {
157+
messageContent = new MimeMultipart();
158+
}
159+
return messageContent;
160+
}
161+
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++;
171+
}
172+
}
173+
174+
private MimeMessage createMultipartMessageAddingUserdata(String userData, FormatType formatType,
175+
MimeMessage message) throws MessagingException, IOException {
176+
MimeMessage newMessage = new MimeMessage(session);
177+
Multipart messageContent = getMessageContent(message);
178+
179+
if (formatType == FormatType.MIME) {
180+
MimeMessage msgFromUserdata = new MimeMessage(session, new ByteArrayInputStream(userData.getBytes()));
181+
addBodyPartsToMessageContentFromUserDataContent(messageContent, msgFromUserdata);
182+
} else {
183+
MimeBodyPart part = generateBodyPartMIMEMessage(userData, formatType);
184+
messageContent.addBodyPart(part);
185+
}
186+
newMessage.setContent(messageContent);
187+
return newMessage;
188+
}
189+
190+
@Override
191+
public String appendUserData(String userData1, String userData2) {
192+
try {
193+
FormatType formatType1 = getUserDataFormatType(userData1);
194+
FormatType formatType2 = getUserDataFormatType(userData2);
195+
MimeMessage message = new MimeMessage(session);
196+
message = createMultipartMessageAddingUserdata(userData1, formatType1, message);
197+
message = createMultipartMessageAddingUserdata(userData2, formatType2, message);
198+
ByteArrayOutputStream output = new ByteArrayOutputStream();
199+
message.writeTo(output);
200+
return output.toString();
201+
} catch (MessagingException | IOException | CloudRuntimeException e) {
202+
String msg = String.format("Error attempting to merge user data as a multipart user data. " +
203+
"Reason: %s", e.getMessage());
204+
LOGGER.error(msg, e);
205+
throw new CloudRuntimeException(msg, e);
206+
}
207+
}
208+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
<beans xmlns="http://www.springframework.org/schema/beans"
20+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21+
xsi:schemaLocation="http://www.springframework.org/schema/beans
22+
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
23+
>
24+
<bean id="cloudInitUserDataProvider" class="org.apache.cloudstack.userdata.CloudInitUserDataProvider">
25+
<property name="name" value="cloud-init" />
26+
</bean>
27+
</beans>

0 commit comments

Comments
 (0)