This is an automated email from the ASF dual-hosted git repository.
dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new b1fc2798726 Generate cloud-init multipart user data for template
append policy (#7643)
b1fc2798726 is described below
commit b1fc2798726bd5ca0782bce59749e8ee30399916
Author: Nicolas Vazquez <[email protected]>
AuthorDate: Mon Jul 10 04:47:03 2023 -0300
Generate cloud-init multipart user data for template append policy (#7643)
Signed-off-by: Abhishek Kumar <[email protected]>
Co-authored-by: Abhishek Kumar <[email protected]>
---
.../cloudstack/userdata/UserDataManager.java | 24 +++
client/pom.xml | 10 +
...ing-core-lifecycle-core-context-inheritable.xml | 7 +-
.../core/spring-core-registry-core-context.xml | 4 +
engine/pom.xml | 2 +
engine/userdata/cloud-init/pom.xml | 36 ++++
.../userdata/CloudInitUserDataProvider.java | 208 +++++++++++++++++++++
.../core/spring-userdata-cloud-init-context.xml | 27 +++
.../userdata/CloudInitUserDataProviderTest.java | 139 ++++++++++++++
engine/userdata/pom.xml | 47 +++++
.../cloudstack/userdata/UserDataManagerImpl.java | 82 ++++++++
.../cloudstack/userdata/UserDataProvider.java | 28 +++
.../core/spring-engine-userdata-core-context.xml | 34 ++++
.../main/java/com/cloud/vm/UserVmManagerImpl.java | 18 +-
.../java/com/cloud/vm/UserVmManagerImplTest.java | 27 +--
test/integration/smoke/test_register_userdata.py | 19 +-
16 files changed, 669 insertions(+), 43 deletions(-)
diff --git
a/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java
b/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java
new file mode 100644
index 00000000000..2fc3acd45d1
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.userdata;
+
+import com.cloud.utils.component.Manager;
+import org.apache.cloudstack.framework.config.Configurable;
+
+public interface UserDataManager extends Manager, Configurable {
+ String concatenateUserData(String userdata1, String userdata2, String
userdataProvider);
+}
diff --git a/client/pom.xml b/client/pom.xml
index a548d676a67..cbe45d88b6c 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -352,6 +352,16 @@
<artifactId>cloud-plugin-outofbandmanagement-driver-redfish</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-engine-userdata-cloud-init</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-engine-userdata</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-mom-rabbitmq</artifactId>
diff --git
a/core/src/main/resources/META-INF/cloudstack/core/spring-core-lifecycle-core-context-inheritable.xml
b/core/src/main/resources/META-INF/cloudstack/core/spring-core-lifecycle-core-context-inheritable.xml
index b754d6bfe62..3e57a01e211 100644
---
a/core/src/main/resources/META-INF/cloudstack/core/spring-core-lifecycle-core-context-inheritable.xml
+++
b/core/src/main/resources/META-INF/cloudstack/core/spring-core-lifecycle-core-context-inheritable.xml
@@ -39,5 +39,10 @@
<property name="typeClass"
value="com.cloud.utils.component.PluggableService" />
</bean>
-
+
+ <bean
class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
+ <property name="registry" ref="userDataProvidersRegistry" />
+ <property name="typeClass"
value="org.apache.cloudstack.userdata.UserDataProvider" />
+ </bean>
+
</beans>
\ No newline at end of file
diff --git
a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
index cb559131a3d..a7f384c76a9 100644
---
a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
+++
b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
@@ -342,4 +342,8 @@
<bean id="kubernetesClusterHelperRegistry"
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
</bean>
+
+ <bean id="userDataProvidersRegistry"
+
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
+ </bean>
</beans>
diff --git a/engine/pom.xml b/engine/pom.xml
index 608affec392..91f430ba300 100644
--- a/engine/pom.xml
+++ b/engine/pom.xml
@@ -58,6 +58,8 @@
<module>storage/image</module>
<module>storage/snapshot</module>
<module>storage/volume</module>
+ <module>userdata/cloud-init</module>
+ <module>userdata</module>
</modules>
<profiles>
<profile>
diff --git a/engine/userdata/cloud-init/pom.xml
b/engine/userdata/cloud-init/pom.xml
new file mode 100644
index 00000000000..7e641bd6b5e
--- /dev/null
+++ b/engine/userdata/cloud-init/pom.xml
@@ -0,0 +1,36 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<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">
+<modelVersion>4.0.0</modelVersion>
+<artifactId>cloud-engine-userdata-cloud-init</artifactId>
+<name>Apache CloudStack Engine Cloud-Init Userdata Component</name>
+<parent>
+ <artifactId>cloud-engine</artifactId>
+ <groupId>org.apache.cloudstack</groupId>
+ <version>4.19.0.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+</parent>
+<dependencies>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-engine-userdata</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+</dependencies>
+</project>
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
new file mode 100644
index 00000000000..c61f37a1896
--- /dev/null
+++
b/engine/userdata/cloud-init/src/main/java/org/apache/cloudstack/userdata/CloudInitUserDataProvider.java
@@ -0,0 +1,208 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.userdata;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import java.util.zip.GZIPInputStream;
+
+import javax.mail.BodyPart;
+import javax.mail.MessagingException;
+import javax.mail.Multipart;
+import javax.mail.Session;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.Logger;
+
+import com.cloud.utils.component.AdapterBase;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class CloudInitUserDataProvider extends AdapterBase implements
UserDataProvider {
+
+ protected enum FormatType {
+ CLOUD_CONFIG, BASH_SCRIPT, MIME, CLOUD_BOOTHOOK, INCLUDE_FILE
+ }
+
+ private static final String CLOUD_CONFIG_CONTENT_TYPE =
"text/cloud-config";
+ private static final String BASH_SCRIPT_CONTENT_TYPE =
"text/x-shellscript";
+ private static final String INCLUDE_FILE_CONTENT_TYPE =
"text/x-include-url";
+ private static final String CLOUD_BOOTHOOK_CONTENT_TYPE =
"text/cloud-boothook";
+
+ private static final Map<FormatType, String> formatContentTypeMap =
Map.ofEntries(
+ Map.entry(FormatType.CLOUD_CONFIG, CLOUD_CONFIG_CONTENT_TYPE),
+ Map.entry(FormatType.BASH_SCRIPT, BASH_SCRIPT_CONTENT_TYPE),
+ Map.entry(FormatType.CLOUD_BOOTHOOK, CLOUD_BOOTHOOK_CONTENT_TYPE),
+ Map.entry(FormatType.INCLUDE_FILE, INCLUDE_FILE_CONTENT_TYPE)
+ );
+
+ private static final Logger LOGGER =
Logger.getLogger(CloudInitUserDataProvider.class);
+
+ private static final Session session = Session.getDefaultInstance(new
Properties());
+
+ @Override
+ public String getName() {
+ return "cloud-init";
+ }
+
+ protected boolean isGZipped(String userdata) {
+ if (StringUtils.isEmpty(userdata)) {
+ return false;
+ }
+ byte[] data = userdata.getBytes(StandardCharsets.ISO_8859_1);
+ if (data.length < 2) {
+ return false;
+ }
+ int magic = data[0] & 0xff | ((data[1] << 8) & 0xff00);
+ return magic == GZIPInputStream.GZIP_MAGIC;
+ }
+
+ protected String extractUserDataHeader(String userdata) {
+ if (isGZipped(userdata)) {
+ throw new CloudRuntimeException("Gzipped user data can not be used
together with other user data formats");
+ }
+ List<String> lines = Arrays.stream(userdata.split("\n"))
+ .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");
+ }
+ return lines.get(0);
+ }
+
+ protected FormatType mapUserDataHeaderToFormatType(String header) {
+ if (header.equalsIgnoreCase("#cloud-config")) {
+ return FormatType.CLOUD_CONFIG;
+ } else if (header.startsWith("#!")) {
+ return FormatType.BASH_SCRIPT;
+ } else if (header.equalsIgnoreCase("#cloud-boothook")) {
+ return FormatType.CLOUD_BOOTHOOK;
+ } else if (header.startsWith("#include")) {
+ return FormatType.INCLUDE_FILE;
+ } else if (header.startsWith("Content-Type:")) {
+ return FormatType.MIME;
+ } else {
+ String msg = String.format("Cannot recognise the user data format
type from the header line: %s." +
+ "Supported types are: cloud-config, bash script,
cloud-boothook, include file or MIME", header);
+ LOGGER.error(msg);
+ throw new CloudRuntimeException(msg);
+ }
+ }
+
+ /**
+ * Detect the user data type
+ * Reference: <a
href="https://canonical-cloud-init.readthedocs-hosted.com/en/latest/explanation/format.html#user-data-formats"
/>
+ */
+ protected FormatType getUserDataFormatType(String userdata) {
+ if (StringUtils.isBlank(userdata)) {
+ String msg = "User data expected but provided empty user data";
+ LOGGER.error(msg);
+ throw new CloudRuntimeException(msg);
+ }
+
+ String header = extractUserDataHeader(userdata);
+ return mapUserDataHeaderToFormatType(header);
+ }
+
+ private String getContentType(String userData, FormatType formatType)
throws MessagingException {
+ if (formatType == FormatType.MIME) {
+ MimeMessage msg = new MimeMessage(session, new
ByteArrayInputStream(userData.getBytes()));
+ return msg.getContentType();
+ }
+ if (!formatContentTypeMap.containsKey(formatType)) {
+ throw new CloudRuntimeException(String.format("Cannot get the user
data content type as " +
+ "its format type %s is invalid", formatType.name()));
+ }
+ return formatContentTypeMap.get(formatType);
+ }
+
+ protected MimeBodyPart generateBodyPartMIMEMessage(String userData,
FormatType formatType) throws MessagingException {
+ MimeBodyPart bodyPart = new MimeBodyPart();
+ String contentType = getContentType(userData, formatType);
+ bodyPart.setContent(userData, contentType);
+ bodyPart.addHeader("Content-Transfer-Encoding", "base64");
+ return bodyPart;
+ }
+
+ private Multipart getMessageContent(MimeMessage message) {
+ Multipart messageContent;
+ try {
+ messageContent = (MimeMultipart) message.getContent();
+ } catch (IOException | MessagingException e) {
+ messageContent = new MimeMultipart();
+ }
+ return messageContent;
+ }
+
+ private void addBodyPartsToMessageContentFromUserDataContent(Multipart
messageContent,
+ MimeMessage
msgFromUserdata) throws MessagingException, IOException {
+ Multipart msgFromUserdataParts = (MimeMultipart)
msgFromUserdata.getContent();
+ int count = msgFromUserdataParts.getCount();
+ int i = 0;
+ while (i < count) {
+ BodyPart bodyPart = msgFromUserdataParts.getBodyPart(0);
+ messageContent.addBodyPart(bodyPart);
+ i++;
+ }
+ }
+
+ private MimeMessage createMultipartMessageAddingUserdata(String userData,
FormatType formatType,
+ MimeMessage
message) throws MessagingException, IOException {
+ MimeMessage newMessage = new MimeMessage(session);
+ Multipart messageContent = getMessageContent(message);
+
+ if (formatType == FormatType.MIME) {
+ MimeMessage msgFromUserdata = new MimeMessage(session, new
ByteArrayInputStream(userData.getBytes()));
+ addBodyPartsToMessageContentFromUserDataContent(messageContent,
msgFromUserdata);
+ } else {
+ MimeBodyPart part = generateBodyPartMIMEMessage(userData,
formatType);
+ messageContent.addBodyPart(part);
+ }
+ newMessage.setContent(messageContent);
+ return newMessage;
+ }
+
+ @Override
+ public String appendUserData(String userData1, String userData2) {
+ try {
+ FormatType formatType1 = getUserDataFormatType(userData1);
+ FormatType formatType2 = getUserDataFormatType(userData2);
+ MimeMessage message = new MimeMessage(session);
+ message = createMultipartMessageAddingUserdata(userData1,
formatType1, message);
+ message = createMultipartMessageAddingUserdata(userData2,
formatType2, message);
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ message.writeTo(output);
+ return output.toString();
+ } catch (MessagingException | IOException | CloudRuntimeException e) {
+ String msg = String.format("Error attempting to merge user data as
a multipart user data. " +
+ "Reason: %s", e.getMessage());
+ LOGGER.error(msg, e);
+ throw new CloudRuntimeException(msg, e);
+ }
+ }
+}
diff --git
a/engine/userdata/cloud-init/src/main/resources/META-INF/cloudstack/core/spring-userdata-cloud-init-context.xml
b/engine/userdata/cloud-init/src/main/resources/META-INF/cloudstack/core/spring-userdata-cloud-init-context.xml
new file mode 100644
index 00000000000..742398e0b86
--- /dev/null
+++
b/engine/userdata/cloud-init/src/main/resources/META-INF/cloudstack/core/spring-userdata-cloud-init-context.xml
@@ -0,0 +1,27 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
+>
+ <bean id="cloudInitUserDataProvider"
class="org.apache.cloudstack.userdata.CloudInitUserDataProvider">
+ <property name="name" value="cloud-init" />
+ </bean>
+</beans>
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
new file mode 100644
index 00000000000..b91438c5a36
--- /dev/null
+++
b/engine/userdata/cloud-init/src/test/java/org/apache/cloudstack/userdata/CloudInitUserDataProviderTest.java
@@ -0,0 +1,139 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.userdata;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.GZIPOutputStream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class CloudInitUserDataProviderTest {
+
+ private final CloudInitUserDataProvider provider = new
CloudInitUserDataProvider();
+ private final static String CLOUD_CONFIG_USERDATA = "## template: jinja\n"
+
+ "#cloud-config\n" +
+ "runcmd:\n" +
+ " - echo 'TestVariable {{ ds.meta_data.variable1 }}' >>
/tmp/variable\n" +
+ " - echo 'Hostname {{ ds.meta_data.public_hostname }}' >
/tmp/hostname";
+
+ @Test
+ public void testGetUserDataFormatType() {
+ CloudInitUserDataProvider.FormatType type =
provider.getUserDataFormatType(CLOUD_CONFIG_USERDATA);
+ 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);
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testGetUserDataFormatTypeInvalidType() {
+ String userdata = "#invalid-type\n" +
+ "password: password\nchpasswd: { expire: False }\nssh_pwauth:
True";
+ provider.getUserDataFormatType(userdata);
+ }
+
+ @Test
+ public void testAppendUserData() {
+ String templateData = "#cloud-config\n" +
+ "password: atomic\n" +
+ "chpasswd: { expire: False }\n" +
+ "ssh_pwauth: True";
+ String vmData = "#!/bin/bash\n" +
+ "date > /provisioned";
+ String multipartUserData = provider.appendUserData(templateData,
vmData);
+ Assert.assertTrue(multipartUserData.contains("Content-Type:
multipart"));
+ }
+
+ @Test
+ public void testAppendUserDataMIMETemplateData() {
+ String templateData = "Content-Type: multipart/mixed;
boundary=\"//\"\n" +
+ "MIME-Version: 1.0\n" +
+ "\n" +
+ "--//\n" +
+ "Content-Type: text/cloud-config; charset=\"us-ascii\"\n" +
+ "MIME-Version: 1.0\n" +
+ "Content-Transfer-Encoding: 7bit\n" +
+ "Content-Disposition: attachment;
filename=\"cloud-config.txt\"\n" +
+ "\n" +
+ "#cloud-config\n" +
+ "\n" +
+ "# Upgrade the instance on first boot\n" +
+ "# (ie run apt-get upgrade)\n" +
+ "#\n" +
+ "# Default: false\n" +
+ "# Aliases: apt_upgrade\n" +
+ "package_upgrade: true";
+ String vmData = "#!/bin/bash\n" +
+ "date > /provisioned";
+ String multipartUserData = provider.appendUserData(templateData,
vmData);
+ Assert.assertTrue(multipartUserData.contains("Content-Type:
multipart"));
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testAppendUserDataInvalidUserData() {
+ String templateData = "password: atomic\n" +
+ "chpasswd: { expire: False }\n" +
+ "ssh_pwauth: True";
+ String vmData = "#!/bin/bash\n" +
+ "date > /provisioned";
+ provider.appendUserData(templateData, vmData);
+ }
+
+ @Test
+ public void testIsGzippedUserDataWithCloudConfigData() {
+ Assert.assertFalse(provider.isGZipped(CLOUD_CONFIG_USERDATA));
+ }
+
+ private String createGzipDataAsString() throws IOException {
+ byte[] input =
CLOUD_CONFIG_USERDATA.getBytes(StandardCharsets.ISO_8859_1);
+
+ ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
+ GZIPOutputStream outputStream = new
GZIPOutputStream(arrayOutputStream);
+ outputStream.write(input,0, input.length);
+ outputStream.close();
+
+ return arrayOutputStream.toString(StandardCharsets.ISO_8859_1);
+ }
+
+ @Test
+ public void testIsGzippedUserDataWithValidGzipData() {
+ try {
+ String gzipped = createGzipDataAsString();
+ Assert.assertTrue(provider.isGZipped(gzipped));
+ } catch (IOException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testAppendUserDataWithGzippedData() {
+ try {
+ provider.appendUserData(CLOUD_CONFIG_USERDATA,
createGzipDataAsString());
+ Assert.fail("Gzipped data shouldn't be appended with other data");
+ } catch (IOException e) {
+ Assert.fail("Exception encountered: " + e.getMessage());
+ }
+ }
+}
diff --git a/engine/userdata/pom.xml b/engine/userdata/pom.xml
new file mode 100644
index 00000000000..2e00ebd9786
--- /dev/null
+++ b/engine/userdata/pom.xml
@@ -0,0 +1,47 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<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">
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>cloud-engine-userdata</artifactId>
+ <name>Apache CloudStack Engine Userdata Component</name>
+ <parent>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-engine</artifactId>
+ <version>4.19.0.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-utils</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.activation</groupId>
+ <artifactId>activation</artifactId>
+ <version>1.1.1</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java
b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java
new file mode 100644
index 00000000000..b2ee9dfd607
--- /dev/null
+++
b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java
@@ -0,0 +1,82 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.userdata;
+
+import com.cloud.utils.component.ManagerBase;
+import com.cloud.utils.exception.CloudRuntimeException;
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class UserDataManagerImpl extends ManagerBase implements
UserDataManager {
+ private List<UserDataProvider> userDataProviders;
+ private static Map<String, UserDataProvider> userDataProvidersMap = new
HashMap<>();
+
+ public void setUserDataProviders(final List<UserDataProvider>
userDataProviders) {
+ this.userDataProviders = userDataProviders;
+ }
+
+ private void initializeUserdataProvidersMap() {
+ if (userDataProviders != null) {
+ for (final UserDataProvider provider : userDataProviders) {
+ userDataProvidersMap.put(provider.getName().toLowerCase(),
provider);
+ }
+ }
+ }
+
+ @Override
+ public boolean start() {
+ initializeUserdataProvidersMap();
+ return true;
+ }
+
+ @Override
+ public String getConfigComponentName() {
+ return UserDataManagerImpl.class.getSimpleName();
+ }
+
+ @Override
+ public ConfigKey<?>[] getConfigKeys() {
+ return new ConfigKey[] {};
+ }
+
+ protected UserDataProvider getUserdataProvider(String name) {
+ if (StringUtils.isEmpty(name)) {
+ // Use cloud-init as the default userdata provider
+ name = "cloud-init";
+ }
+ if (!userDataProvidersMap.containsKey(name)) {
+ throw new CloudRuntimeException("Failed to find userdata provider
by the name: " + name);
+ }
+ return userDataProvidersMap.get(name);
+ }
+
+ @Override
+ public String concatenateUserData(String userdata1, String userdata2,
String userdataProvider) {
+ byte[] userdata1Bytes = Base64.decodeBase64(userdata1.getBytes());
+ byte[] userdata2Bytes = Base64.decodeBase64(userdata2.getBytes());
+ String userData1Str = new String(userdata1Bytes);
+ String userData2Str = new String(userdata2Bytes);
+ UserDataProvider provider = getUserdataProvider(userdataProvider);
+ String appendUserData = provider.appendUserData(userData1Str,
userData2Str);
+ return Base64.encodeBase64String(appendUserData.getBytes());
+ }
+}
diff --git
a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataProvider.java
b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataProvider.java
new file mode 100644
index 00000000000..9ac577b54ef
--- /dev/null
+++
b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataProvider.java
@@ -0,0 +1,28 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.userdata;
+
+public interface UserDataProvider {
+ String getName();
+
+ /**
+ * Append user data into a single user data.
+ * NOTE: userData1 and userData2 are decoded user data strings
+ * @return a non-encrypted string containing both user data inputs
+ */
+ String appendUserData(String userData1, String userData2);
+}
diff --git
a/engine/userdata/src/main/resources/META-INF/cloudstack/core/spring-engine-userdata-core-context.xml
b/engine/userdata/src/main/resources/META-INF/cloudstack/core/spring-engine-userdata-core-context.xml
new file mode 100644
index 00000000000..3e067044e53
--- /dev/null
+++
b/engine/userdata/src/main/resources/META-INF/cloudstack/core/spring-engine-userdata-core-context.xml
@@ -0,0 +1,34 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:aop="http://www.springframework.org/schema/aop"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+
http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
+ http://www.springframework.org/schema/context
+
http://www.springframework.org/schema/context/spring-context.xsd"
+ >
+
+ <bean id="userDataManager"
class="org.apache.cloudstack.userdata.UserDataManagerImpl">
+ <property name="userDataProviders"
value="#{userDataProvidersRegistry.registered}" />
+ </bean>
+
+</beans>
\ No newline at end of file
diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
index a3d36d6bfb6..6d3b7acaa9c 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
@@ -123,6 +123,7 @@ import
org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
+import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
import org.apache.cloudstack.utils.security.ParserUtils;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
@@ -614,6 +615,9 @@ public class UserVmManagerImpl extends ManagerBase
implements UserVmManager, Vir
@Inject
private ManagementService _mgr;
+ @Inject
+ private UserDataManager userDataManager;
+
private static final ConfigKey<Integer> VmIpFetchWaitInterval = new
ConfigKey<Integer>("Advanced", Integer.class,
"externaldhcp.vmip.retrieval.interval", "180",
"Wait Interval (in seconds) for shared network vm dhcp ip addr
fetch for next iteration ", true);
@@ -5731,9 +5735,9 @@ public class UserVmManagerImpl extends ManagerBase
implements UserVmManager, Vir
}
if (userDataId != null) {
UserData apiUserDataVO =
userDataDao.findById(userDataId);
- return
doConcateUserDatas(templateUserDataVO.getUserData(),
apiUserDataVO.getUserData());
+ return
userDataManager.concatenateUserData(templateUserDataVO.getUserData(),
apiUserDataVO.getUserData(), null);
} else if (StringUtils.isNotEmpty(userData)) {
- return
doConcateUserDatas(templateUserDataVO.getUserData(), userData);
+ return
userDataManager.concatenateUserData(templateUserDataVO.getUserData(), userData,
null);
} else {
return templateUserDataVO.getUserData();
}
@@ -5751,16 +5755,6 @@ public class UserVmManagerImpl extends ManagerBase
implements UserVmManager, Vir
return null;
}
- private String doConcateUserDatas(String userdata1, String userdata2) {
- byte[] userdata1Bytes = Base64.decodeBase64(userdata1.getBytes());
- byte[] userdata2Bytes = Base64.decodeBase64(userdata2.getBytes());
- byte[] finalUserDataBytes = new byte[userdata1Bytes.length +
userdata2Bytes.length];
- System.arraycopy(userdata1Bytes, 0, finalUserDataBytes, 0,
userdata1Bytes.length);
- System.arraycopy(userdata2Bytes, 0, finalUserDataBytes,
userdata1Bytes.length, userdata2Bytes.length);
-
- return Base64.encodeBase64String(finalUserDataBytes);
- }
-
@Override
public UserVm createVirtualMachine(DeployVMCmd cmd) throws
InsufficientCapacityException, ResourceUnavailableException,
ConcurrentOperationException,
StorageUnavailableException, ResourceAllocationException {
diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
index 93995bc860a..f91b52b867b 100644
--- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
+++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
@@ -43,6 +43,7 @@ import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd;
import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd;
import org.apache.cloudstack.context.CallContext;
import
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
+import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.junit.After;
@@ -217,6 +218,9 @@ public class UserVmManagerImplTest {
@Mock
private ServiceOfferingVO serviceOffering;
+ @Mock
+ UserDataManager userDataManager;
+
private static final long vmId = 1l;
private static final long zoneId = 2L;
private static final long accountId = 3L;
@@ -713,29 +717,6 @@ public class UserVmManagerImplTest {
Assert.assertEquals(finalUserdata, templateUserData);
}
- @Test
- public void testUserDataAppend() {
- String userData = "testUserdata";
- String templateUserData = "testTemplateUserdata";
- Long userDataId = 1L;
-
- VirtualMachineTemplate template =
Mockito.mock(VirtualMachineTemplate.class);
- when(template.getUserDataId()).thenReturn(2L);
-
when(template.getUserDataOverridePolicy()).thenReturn(UserData.UserDataOverridePolicy.APPEND);
-
- UserDataVO templateUserDataVO = Mockito.mock(UserDataVO.class);
- doReturn(templateUserDataVO).when(userDataDao).findById(2L);
- when(templateUserDataVO.getUserData()).thenReturn(templateUserData);
-
- UserDataVO apiUserDataVO = Mockito.mock(UserDataVO.class);
- doReturn(apiUserDataVO).when(userDataDao).findById(userDataId);
- when(apiUserDataVO.getUserData()).thenReturn(userData);
-
- String finalUserdata = userVmManagerImpl.finalizeUserData(null,
userDataId, template);
-
- Assert.assertEquals(finalUserdata, templateUserData+userData);
- }
-
@Test
public void testUserDataWithoutTemplate() {
String userData = "testUserdata";
diff --git a/test/integration/smoke/test_register_userdata.py
b/test/integration/smoke/test_register_userdata.py
index bc38cd989c0..5c954a876ec 100644
--- a/test/integration/smoke/test_register_userdata.py
+++ b/test/integration/smoke/test_register_userdata.py
@@ -589,21 +589,27 @@ class TestRegisteredUserdata(cloudstackTestCase):
2. Link a userdata to template with override policy is append
3. Deploy a VM with that template and also by passing another
userdata id
4. Since the override policy is append, userdata passed during VM
deployment will be appended to template's
- userdata and configured to VM. Verify the same by SSH into VM.
+ userdata and configured to VM as a multipart MIME userdata. Verify
the same by SSH into VM.
"""
+ # #!/bin/bash
+ # date > /provisioned
self.apiUserdata = UserData.register(
self.apiclient,
name="ApiUserdata",
- userdata="QVBJdXNlcmRhdGE=", #APIuserdata
+ userdata="IyEvYmluL2Jhc2gKZGF0ZSA+IC9wcm92aXNpb25lZA==",
account=self.account.name,
domainid=self.account.domainid
)
+ # #cloud-config
+ # password: atomic
+ # chpasswd: { expire: False }
+ # ssh_pwauth: True
self.templateUserdata = UserData.register(
self.apiclient,
name="TemplateUserdata",
- userdata="VGVtcGxhdGVVc2VyRGF0YQ==", #TemplateUserData
+
userdata="I2Nsb3VkLWNvbmZpZwpwYXNzd29yZDogYXRvbWljCmNocGFzc3dkOiB7IGV4cGlyZTogRmFsc2UgfQpzc2hfcHdhdXRoOiBUcnVl",
account=self.account.name,
domainid=self.account.domainid
)
@@ -700,10 +706,9 @@ class TestRegisteredUserdata(cloudstackTestCase):
cmd = "curl http://%s/latest/user-data" % vr_ip
res = ssh.execute(cmd)
self.debug("Verifying userdata in the VR")
- self.assertEqual(
- str(res[0]),
- "TemplateUserDataAPIuserdata",
- "Failed to match userdata"
+ self.assertTrue(
+ "Content-Type: multipart" in str(res[2]),
+ "Failed to match multipart userdata"
)
@attr(tags=['advanced', 'simulator', 'basic', 'sg', 'testnow'],
required_hardware=True)