Generate cloud-init multipart user data for template append policy (#7643)

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
Co-authored-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Nicolas Vazquez 2023-07-10 04:47:03 -03:00 committed by GitHub
parent 5383bf64f4
commit b1fc279872
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 669 additions and 43 deletions

View File

@ -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);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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());
}
}
}

47
engine/userdata/pom.xml Normal file
View File

@ -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>

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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 {

View File

@ -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";

View File

@ -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)