Add support for providing userdata to system VMs (#11654)

This PR adds support for specifying user data (cloud-init) for system VMs via Zone Scoped global settings. This allows the operators to customize the System VMs and setup monitoring, logging or execute any custom commands.

We set the user data from the global setting in /var/cache/cloud/cmdline, and use the NoCloud datasource to process user data. cloud-init service is still disabled in the system VMs and it's executed as part of the cloud-postinit service which executes the postinit.sh script.

Added global settings:
systemvm.userdata.enabled - Disabled by default. Needs to be enabled to utilize the feature.
console.proxy.vm.userdata - UUID of the User data to be used for Console Proxy
secstorage.vm.userdata - UUID of the User data to be used for Secondary Storage VM
virtual.router.userdata - UUID of the User data to be used for Virtual Routers
This commit is contained in:
Vishesh 2025-10-08 10:44:26 +05:30 committed by GitHub
parent 858663fcf7
commit d2615bb142
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 840 additions and 10 deletions

View File

@ -22,6 +22,8 @@ import org.apache.cloudstack.framework.config.Configurable;
import com.cloud.utils.component.Manager;
import java.io.IOException;
public interface UserDataManager extends Manager, Configurable {
String VM_USERDATA_MAX_LENGTH_STRING = "vm.userdata.max.length";
ConfigKey<Integer> VM_USERDATA_MAX_LENGTH = new ConfigKey<>("Advanced", Integer.class, VM_USERDATA_MAX_LENGTH_STRING, "32768",
@ -29,4 +31,14 @@ public interface UserDataManager extends Manager, Configurable {
String concatenateUserData(String userdata1, String userdata2, String userdataProvider);
String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod);
/**
* This method validates the user data uuid for system VMs and returns the user data
* after compression and base64 encoding for the system VM to consume.
*
* @param userDataUuid
* @return a String containing the user data after compression and base64 encoding
* @throws IOException
*/
String validateAndGetUserDataForSystemVM(String userDataUuid) throws IOException;
}

View File

@ -106,6 +106,9 @@ public interface VirtualMachineManager extends Manager {
ConfigKey<Boolean> VmSyncPowerStateTransitioning = new ConfigKey<>("Advanced", Boolean.class, "vm.sync.power.state.transitioning", "true",
"Whether to sync power states of the transitioning and stalled VMs while processing VM power reports.", false);
ConfigKey<Boolean> SystemVmEnableUserData = new ConfigKey<>(Boolean.class, "systemvm.userdata.enabled", "Advanced", "false",
"Enable user data for system VMs. When enabled, the CPVM, SSVM, and Router system VMs will use the values from the global settings console.proxy.vm.userdata, secstorage.vm.userdata, and virtual.router.userdata, respectively, to provide cloud-init user data to the VM.",
true, ConfigKey.Scope.Zone, null);
interface Topics {
String VM_POWER_STATE = "vm.powerstate";

View File

@ -5244,7 +5244,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
VmConfigDriveLabel, VmConfigDriveOnPrimaryPool, VmConfigDriveForceHostCacheUse, VmConfigDriveUseHostCacheOnUnsupportedPool,
HaVmRestartHostUp, ResourceCountRunningVMsonly, AllowExposeHypervisorHostname, AllowExposeHypervisorHostnameAccountLevel, SystemVmRootDiskSize,
AllowExposeDomainInMetadata, MetadataCustomCloudName, VmMetadataManufacturer, VmMetadataProductName,
VmSyncPowerStateTransitioning
VmSyncPowerStateTransitioning, SystemVmEnableUserData
};
}

View File

@ -16,12 +16,18 @@
// under the License.
package org.apache.cloudstack.userdata;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.cloud.domain.Domain;
import com.cloud.user.User;
import com.cloud.user.UserDataVO;
import com.cloud.user.dao.UserDataDao;
import com.cloud.utils.compression.CompressionUtil;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.commons.codec.binary.Base64;
@ -31,7 +37,12 @@ import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.exception.CloudRuntimeException;
import javax.inject.Inject;
public class UserDataManagerImpl extends ManagerBase implements UserDataManager {
@Inject
UserDataDao userDataDao;
private static final int MAX_USER_DATA_LENGTH_BYTES = 2048;
private static final int MAX_HTTP_GET_LENGTH = 2 * MAX_USER_DATA_LENGTH_BYTES; // 4KB
private static final int NUM_OF_2K_BLOCKS = 512;
@ -118,6 +129,25 @@ public class UserDataManagerImpl extends ManagerBase implements UserDataManager
return Base64.encodeBase64String(decodedUserData);
}
@Override
public String validateAndGetUserDataForSystemVM(String userDataUuid) throws IOException {
if (StringUtils.isBlank(userDataUuid)) {
return null;
}
UserDataVO userDataVo = userDataDao.findByUuid(userDataUuid);
if (userDataVo == null) {
return null;
}
if (userDataVo.getDomainId() == Domain.ROOT_DOMAIN && userDataVo.getAccountId() == User.UID_ADMIN) {
// Decode base64 user data, compress it, then re-encode to reduce command line length
String plainTextUserData = new String(java.util.Base64.getDecoder().decode(userDataVo.getUserData()));
CompressionUtil compressionUtil = new CompressionUtil();
byte[] compressedUserData = compressionUtil.compressString(plainTextUserData);
return java.util.Base64.getEncoder().encodeToString(compressedUserData);
}
throw new CloudRuntimeException("User data can only be used by system VMs if it belongs to the ROOT domain and ADMIN account.");
}
private byte[] validateAndDecodeByHTTPMethod(String userData, int maxHTTPLength, BaseCmd.HTTPMethod httpMethod) {
byte[] decodedUserData = Base64.decodeBase64(userData.getBytes());
if (decodedUserData == null || decodedUserData.length < 1) {

View File

@ -17,19 +17,37 @@
package org.apache.cloudstack.userdata;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.apache.cloudstack.api.BaseCmd;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.domain.Domain;
import com.cloud.user.User;
import com.cloud.user.UserDataVO;
import com.cloud.user.dao.UserDataDao;
import com.cloud.utils.exception.CloudRuntimeException;
@RunWith(MockitoJUnitRunner.class)
public class UserDataManagerImplTest {
@Mock
private UserDataDao userDataDao;
@Spy
@InjectMocks
private UserDataManagerImpl userDataManager;
@ -56,4 +74,76 @@ public class UserDataManagerImplTest {
assertEquals("validate return the value with padding", encodedUserdata, userDataManager.validateUserData(urlEncodedUserdata, BaseCmd.HTTPMethod.GET));
}
@Test
public void testValidateAndGetUserDataForSystemVMWithBlankUuid() throws IOException {
// Test with blank UUID should return null
assertNull("null UUID should return null", userDataManager.validateAndGetUserDataForSystemVM(null));
assertNull("blank UUID should return null", userDataManager.validateAndGetUserDataForSystemVM(""));
assertNull("blank UUID should return null", userDataManager.validateAndGetUserDataForSystemVM(" "));
}
@Test
public void testValidateAndGetUserDataForSystemVMNotFound() throws IOException {
// Test when userDataVo is not found
String testUuid = "test-uuid-123";
when(userDataDao.findByUuid(testUuid)).thenReturn(null);
assertNull("userdata not found should return null", userDataManager.validateAndGetUserDataForSystemVM(testUuid));
}
@Test(expected = CloudRuntimeException.class)
public void testValidateAndGetUserDataForSystemVMInvalidDomain() throws IOException {
// Test with userDataVo that doesn't belong to ROOT domain
String testUuid = "test-uuid-123";
UserDataVO userDataVo = Mockito.mock(UserDataVO.class);
when(userDataVo.getDomainId()).thenReturn(2L); // Not ROOT domain
when(userDataDao.findByUuid(testUuid)).thenReturn(userDataVo);
userDataManager.validateAndGetUserDataForSystemVM(testUuid);
}
@Test(expected = CloudRuntimeException.class)
public void testValidateAndGetUserDataForSystemVMInvalidAccount() throws IOException {
// Test with userDataVo that doesn't belong to ADMIN account
String testUuid = "test-uuid-123";
UserDataVO userDataVo = Mockito.mock(UserDataVO.class);
when(userDataVo.getDomainId()).thenReturn(Domain.ROOT_DOMAIN);
when(userDataVo.getAccountId()).thenReturn(3L);
userDataVo.setUserData("dGVzdCBkYXRh"); // "test data" in base64
when(userDataDao.findByUuid(testUuid)).thenReturn(userDataVo);
userDataManager.validateAndGetUserDataForSystemVM(testUuid);
}
@Test
public void testValidateAndGetUserDataForSystemVMValidSystemVMUserData() throws IOException {
// Test with valid system VM userdata (ROOT domain + ADMIN account)
String testUuid = "test-uuid-123";
String originalText = "#!/bin/bash\necho 'Hello World'";
String base64EncodedUserData = Base64.getEncoder().encodeToString(originalText.getBytes());
UserDataVO userDataVo = Mockito.mock(UserDataVO.class);
when(userDataVo.getDomainId()).thenReturn(Domain.ROOT_DOMAIN);
when(userDataVo.getAccountId()).thenReturn(User.UID_ADMIN);
when(userDataVo.getUserData()).thenReturn(base64EncodedUserData);
when(userDataDao.findByUuid(testUuid)).thenReturn(userDataVo);
String result = userDataManager.validateAndGetUserDataForSystemVM(testUuid);
// Verify result is not null and is base64 encoded
assertNotNull("result should not be null", result);
assertFalse("result should be base64 encoded", result.isEmpty());
// Verify the result is valid base64
try {
Base64.getDecoder().decode(result);
} catch (IllegalArgumentException e) {
throw new AssertionError("Result should be valid base64", e);
}
// The result should be different from input since it's compressed
assertNotEquals("compressed result should be different from original", result, base64EncodedUserData);
}
}

View File

@ -43,6 +43,64 @@ public class ConfigKey<T> {
public static final String CATEGORY_NETWORK = "Network";
public static final String CATEGORY_SYSTEM = "System";
// Configuration Groups to be used to define group for a config key
// Group name, description, precedence
public static final Ternary<String, String, Long> GROUP_MISCELLANEOUS = new Ternary<>("Miscellaneous", "Miscellaneous configuration", 999L);
public static final Ternary<String, String, Long> GROUP_ACCESS = new Ternary<>("Access", "Identity and Access management configuration", 1L);
public static final Ternary<String, String, Long> GROUP_COMPUTE = new Ternary<>("Compute", "Compute configuration", 2L);
public static final Ternary<String, String, Long> GROUP_STORAGE = new Ternary<>("Storage", "Storage configuration", 3L);
public static final Ternary<String, String, Long> GROUP_NETWORK = new Ternary<>("Network", "Network configuration", 4L);
public static final Ternary<String, String, Long> GROUP_HYPERVISOR = new Ternary<>("Hypervisor", "Hypervisor specific configuration", 5L);
public static final Ternary<String, String, Long> GROUP_MANAGEMENT_SERVER = new Ternary<>("Management Server", "Management Server configuration", 6L);
public static final Ternary<String, String, Long> GROUP_SYSTEM_VMS = new Ternary<>("System VMs", "System VMs related configuration", 7L);
public static final Ternary<String, String, Long> GROUP_INFRASTRUCTURE = new Ternary<>("Infrastructure", "Infrastructure configuration", 8L);
public static final Ternary<String, String, Long> GROUP_USAGE_SERVER = new Ternary<>("Usage Server", "Usage Server related configuration", 9L);
// Configuration Subgroups to be used to define subgroup for a config key
// Subgroup name, description, precedence
public static final Pair<String, Long> SUBGROUP_OTHERS = new Pair<>("Others", 999L);
public static final Pair<String, Long> SUBGROUP_ACCOUNT = new Pair<>("Account", 1L);
public static final Pair<String, Long> SUBGROUP_DOMAIN = new Pair<>("Domain", 2L);
public static final Pair<String, Long> SUBGROUP_PROJECT = new Pair<>("Project", 3L);
public static final Pair<String, Long> SUBGROUP_LDAP = new Pair<>("LDAP", 4L);
public static final Pair<String, Long> SUBGROUP_SAML = new Pair<>("SAML", 5L);
public static final Pair<String, Long> SUBGROUP_VIRTUAL_MACHINE = new Pair<>("Virtual Machine", 1L);
public static final Pair<String, Long> SUBGROUP_KUBERNETES = new Pair<>("Kubernetes", 2L);
public static final Pair<String, Long> SUBGROUP_HIGH_AVAILABILITY = new Pair<>("High Availability", 3L);
public static final Pair<String, Long> SUBGROUP_IMAGES = new Pair<>("Images", 1L);
public static final Pair<String, Long> SUBGROUP_VOLUME = new Pair<>("Volume", 2L);
public static final Pair<String, Long> SUBGROUP_SNAPSHOT = new Pair<>("Snapshot", 3L);
public static final Pair<String, Long> SUBGROUP_VM_SNAPSHOT = new Pair<>("VM Snapshot", 4L);
public static final Pair<String, Long> SUBGROUP_NETWORK = new Pair<>("Network", 1L);
public static final Pair<String, Long> SUBGROUP_DHCP = new Pair<>("DHCP", 2L);
public static final Pair<String, Long> SUBGROUP_VPC = new Pair<>("VPC", 3L);
public static final Pair<String, Long> SUBGROUP_LOADBALANCER = new Pair<>("LoadBalancer", 4L);
public static final Pair<String, Long> SUBGROUP_API = new Pair<>("API", 1L);
public static final Pair<String, Long> SUBGROUP_ALERTS = new Pair<>("Alerts", 2L);
public static final Pair<String, Long> SUBGROUP_EVENTS = new Pair<>("Events", 3L);
public static final Pair<String, Long> SUBGROUP_SECURITY = new Pair<>("Security", 4L);
public static final Pair<String, Long> SUBGROUP_USAGE = new Pair<>("Usage", 1L);
public static final Pair<String, Long> SUBGROUP_LIMITS = new Pair<>("Limits", 6L);
public static final Pair<String, Long> SUBGROUP_JOBS = new Pair<>("Jobs", 7L);
public static final Pair<String, Long> SUBGROUP_AGENT = new Pair<>("Agent", 8L);
public static final Pair<String, Long> SUBGROUP_HYPERVISOR = new Pair<>("Hypervisor", 1L);
public static final Pair<String, Long> SUBGROUP_KVM = new Pair<>("KVM", 2L);
public static final Pair<String, Long> SUBGROUP_VMWARE = new Pair<>("VMware", 3L);
public static final Pair<String, Long> SUBGROUP_XENSERVER = new Pair<>("XenServer", 4L);
public static final Pair<String, Long> SUBGROUP_OVM = new Pair<>("OVM", 5L);
public static final Pair<String, Long> SUBGROUP_BAREMETAL = new Pair<>("Baremetal", 6L);
public static final Pair<String, Long> SUBGROUP_CONSOLE_PROXY_VM = new Pair<>("ConsoleProxyVM", 1L);
public static final Pair<String, Long> SUBGROUP_SEC_STORAGE_VM = new Pair<>("SecStorageVM", 2L);
public static final Pair<String, Long> SUBGROUP_VIRTUAL_ROUTER = new Pair<>("VirtualRouter", 3L);
public static final Pair<String, Long> SUBGROUP_DIAGNOSTICS = new Pair<>("Diagnostics", 4L);
public static final Pair<String, Long> SUBGROUP_PRIMARY_STORAGE = new Pair<>("Primary Storage", 1L);
public static final Pair<String, Long> SUBGROUP_SECONDARY_STORAGE = new Pair<>("Secondary Storage", 2L);
public static final Pair<String, Long> SUBGROUP_BACKUP_AND_RECOVERY = new Pair<>("Backup & Recovery", 1L);
public static final Pair<String, Long> SUBGROUP_CERTIFICATE_AUTHORITY = new Pair<>("Certificate Authority", 2L);
public static final Pair<String, Long> SUBGROUP_QUOTA = new Pair<>("Quota", 3L);
public static final Pair<String, Long> SUBGROUP_CLOUDIAN = new Pair<>("Cloudian", 4L);
public static final Pair<String, Long> SUBGROUP_DRS = new Pair<>("DRS", 4L);
public enum Scope {
Global(null, 1),
Zone(Global, 1 << 1),

View File

@ -35,6 +35,8 @@ import org.apache.cloudstack.config.ApiServiceConfiguration;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.cloud.agent.AgentManager;
@ -101,6 +103,9 @@ import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.DomainRouterDao;
import com.cloud.vm.dao.NicDao;
import static com.cloud.network.router.VirtualNetworkApplianceManager.VirtualRouterUserData;
import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
@Component
public class ElasticLoadBalancerManagerImpl extends ManagerBase implements ElasticLoadBalancerManager, VirtualMachineGuru {
@ -136,6 +141,8 @@ public class ElasticLoadBalancerManagerImpl extends ManagerBase implements Elast
private ElasticLbVmMapDao _elbVmMapDao;
@Inject
private NicDao _nicDao;
@Inject
private UserDataManager userDataManager;
String _instance;
@ -477,6 +484,19 @@ public class ElasticLoadBalancerManagerImpl extends ManagerBase implements Elast
}
String msPublicKey = _configDao.getValue("ssh.publickey");
buf.append(" authorized_key=").append(VirtualMachineGuru.getEncodedMsPublicKey(msPublicKey));
if (SystemVmEnableUserData.valueIn(dc.getId())) {
String userDataUuid = VirtualRouterUserData.valueIn(dc.getId());
try {
String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
if (StringUtils.isNotBlank(userData)) {
buf.append(" userdata=").append(userData);
}
} catch (Exception e) {
logger.warn("Failed to load user data for the elastic lb vm, ignored", e);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Boot Args for " + profile + ": " + buf.toString());
}

View File

@ -21,6 +21,8 @@ import static com.cloud.hypervisor.Hypervisor.HypervisorType.KVM;
import static com.cloud.hypervisor.Hypervisor.HypervisorType.LXC;
import static com.cloud.hypervisor.Hypervisor.HypervisorType.VMware;
import static com.cloud.hypervisor.Hypervisor.HypervisorType.XenServer;
import static com.cloud.network.router.VirtualNetworkApplianceManager.VirtualRouterUserData;
import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
import java.util.ArrayList;
import java.util.Arrays;
@ -39,6 +41,7 @@ import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationSe
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.lb.ApplicationLoadBalancerRuleVO;
import org.apache.cloudstack.lb.dao.ApplicationLoadBalancerRuleDao;
import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.commons.collections.CollectionUtils;
import com.cloud.agent.AgentManager;
@ -126,6 +129,7 @@ import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VirtualMachineProfile.Param;
import com.cloud.vm.dao.DomainRouterDao;
import com.cloud.vm.dao.NicDao;
import org.apache.commons.lang3.StringUtils;
public class InternalLoadBalancerVMManagerImpl extends ManagerBase implements InternalLoadBalancerVMManager, InternalLoadBalancerVMService, VirtualMachineGuru {
static final private String InternalLbVmNamePrefix = "b";
@ -175,6 +179,8 @@ public class InternalLoadBalancerVMManagerImpl extends ManagerBase implements In
ResourceManager _resourceMgr;
@Inject
UserDao _userDao;
@Inject
private UserDataManager userDataManager;
@Override
public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile, final DeployDestination dest, final ReservationContext context) {
@ -243,6 +249,19 @@ public class InternalLoadBalancerVMManagerImpl extends ManagerBase implements In
final String type = "ilbvm";
buf.append(" type=" + type);
long dcId = profile.getVirtualMachine().getDataCenterId();
if (SystemVmEnableUserData.valueIn(dcId)) {
String userDataUuid = VirtualRouterUserData.valueIn(dcId);
try {
String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
if (StringUtils.isNotBlank(userData)) {
buf.append(" userdata=").append(userData);
}
} catch (Exception e) {
logger.warn("Failed to load user data for the internal lb vm, ignored", e);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Boot Args for " + profile + ": " + buf.toString());
}

View File

@ -18,6 +18,7 @@ package org.apache.cloudstack.internallbvmmgr;
import java.io.IOException;
import org.apache.cloudstack.userdata.UserDataManager;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@ -166,6 +167,11 @@ public class LbChildTestConfiguration {
return Mockito.mock(AccountDao.class);
}
@Bean
public UserDataManager userDataManager() {
return Mockito.mock(UserDataManager.class);
}
@Override
public boolean match(MetadataReader mdr, MetadataReaderFactory arg1) throws IOException {
mdr.getClassMetadata().getClassName();

View File

@ -50,6 +50,9 @@ import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.consoleproxy.ConsoleProxyManager;
import com.cloud.network.router.VirtualNetworkApplianceManager;
import com.cloud.storage.secondary.SecondaryStorageVmManager;
import com.cloud.vm.VirtualMachineManager;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.affinity.AffinityGroup;
@ -638,6 +641,11 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
protected void populateConfigKeysAllowedOnlyForDefaultAdmin() {
configKeysAllowedOnlyForDefaultAdmin.add(AccountManagerImpl.listOfRoleTypesAllowedForOperationsOfSameRoleType.key());
configKeysAllowedOnlyForDefaultAdmin.add(AccountManagerImpl.allowOperationsOnUsersInSameAccount.key());
configKeysAllowedOnlyForDefaultAdmin.add(VirtualMachineManager.SystemVmEnableUserData.key());
configKeysAllowedOnlyForDefaultAdmin.add(ConsoleProxyManager.ConsoleProxyVmUserData.key());
configKeysAllowedOnlyForDefaultAdmin.add(SecondaryStorageVmManager.SecondaryStorageVmUserData.key());
configKeysAllowedOnlyForDefaultAdmin.add(VirtualNetworkApplianceManager.VirtualRouterUserData.key());
}
private void initMessageBusListener() {

View File

@ -97,6 +97,12 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService {
ConfigKey<String> ConsoleProxyManagementLastState = new ConfigKey<String>(ConfigKey.CATEGORY_ADVANCED, String.class, "consoleproxy.management.state.last", com.cloud.consoleproxy.ConsoleProxyManagementState.Auto.toString(),
"last console proxy service management state", false, ConfigKey.Kind.Select, consoleProxyManagementStates);
ConfigKey<String> ConsoleProxyVmUserData = new ConfigKey<>(String.class, "console.proxy.vm.userdata",
ConfigKey.CATEGORY_ADVANCED, "",
"UUID for user data for console proxy VMs. This works only when systemvm.userdata.enabled is set to true",
true, ConfigKey.Scope.Zone, null, "User Data for CPVMs",
null, ConfigKey.GROUP_SYSTEM_VMS, ConfigKey.SUBGROUP_CONSOLE_PROXY_VM);
void setManagementState(ConsoleProxyManagementState state);
ConsoleProxyManagementState getManagementState();

View File

@ -48,6 +48,7 @@ import org.apache.cloudstack.framework.security.keystore.KeystoreVO;
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.userdata.UserDataManager;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
@ -152,6 +153,8 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
/**
* Class to manage console proxys. <br><br>
* Possible console proxy state transition cases:<br>
@ -227,6 +230,8 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
private CAManager caManager;
@Inject
private NetworkOrchestrationService networkMgr;
@Inject
private UserDataManager userDataManager;
private ConsoleProxyListener consoleProxyListener;
@ -1265,6 +1270,19 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
buf.append(" vncport=").append(getVncPort(datacenterId));
}
buf.append(" keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16)));
if (SystemVmEnableUserData.valueIn(dc.getId())) {
String userDataUuid = ConsoleProxyVmUserData.valueIn(dc.getId());
try {
String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
if (StringUtils.isNotBlank(userData)) {
buf.append(" userdata=").append(userData);
}
} catch (Exception e) {
logger.warn("Failed to load user data for the cpvm, ignored", e);
}
}
String bootArgs = buf.toString();
if (logger.isDebugEnabled()) {
logger.debug("Boot Args for " + profile + ": " + bootArgs);
@ -1570,9 +1588,10 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] { ConsoleProxySslEnabled, NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled, ConsoleProxyServiceOffering,
ConsoleProxyCapacityStandby, ConsoleProxyCapacityScanInterval, ConsoleProxyCmdPort, ConsoleProxyRestart, ConsoleProxyUrlDomain, ConsoleProxySessionMax, ConsoleProxySessionTimeout, ConsoleProxyDisableRpFilter, ConsoleProxyLaunchMax,
ConsoleProxyManagementLastState, ConsoleProxyServiceManagementState, NoVncConsoleShowDot };
return new ConfigKey<?>[] {ConsoleProxySslEnabled, NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled, ConsoleProxyServiceOffering,
ConsoleProxyCapacityStandby, ConsoleProxyCapacityScanInterval, ConsoleProxyCmdPort, ConsoleProxyRestart, ConsoleProxyUrlDomain, ConsoleProxySessionMax, ConsoleProxySessionTimeout, ConsoleProxyDisableRpFilter, ConsoleProxyLaunchMax,
ConsoleProxyManagementLastState, ConsoleProxyServiceManagementState, NoVncConsoleShowDot,
ConsoleProxyVmUserData};
}
protected ConsoleProxyStatus parseJsonToConsoleProxyStatus(String json) throws JsonParseException {

View File

@ -64,6 +64,12 @@ public interface VirtualNetworkApplianceManager extends Manager, VirtualNetworkA
ConfigKey<String> RouterTemplateOvm3 = new ConfigKey<>(String.class, RouterTemplateOvm3CK, "Advanced", "SystemVM Template (Ovm3)",
"Name of the default router template on Ovm3.", true, ConfigKey.Scope.Zone, null);
ConfigKey<String> VirtualRouterUserData = new ConfigKey<>(String.class, "virtual.router.userdata",
ConfigKey.CATEGORY_ADVANCED, "",
"UUID for user data of VR, VPC VR, internal LB, and elastic LB. This works only when systemvm.userdata.enabled is set to true",
true, ConfigKey.Scope.Zone, null, "User Data for VRs",
null, ConfigKey.GROUP_SYSTEM_VMS, ConfigKey.SUBGROUP_VIRTUAL_ROUTER);
ConfigKey<Boolean> SetServiceMonitor = new ConfigKey<>(Boolean.class, SetServiceMonitorCK, "Advanced", "true",
"service monitoring in router enable/disable option, default true", true, ConfigKey.Scope.Zone, null);

View File

@ -18,6 +18,7 @@
package com.cloud.network.router;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
import java.lang.reflect.Type;
import java.math.BigInteger;
@ -71,6 +72,7 @@ import org.apache.cloudstack.network.BgpPeer;
import org.apache.cloudstack.network.RoutedIpv4Manager;
import org.apache.cloudstack.network.topology.NetworkTopology;
import org.apache.cloudstack.network.topology.NetworkTopologyContext;
import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.cloudstack.utils.CloudStackVersion;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.cloudstack.utils.usage.UsageUtils;
@ -352,6 +354,8 @@ Configurable, StateListener<VirtualMachine.State, VirtualMachine.Event, VirtualM
@Inject
BGPService bgpService;
@Inject
private UserDataManager userDataManager;
private int _routerStatsInterval = 300;
private int _routerCheckInterval = 30;
private int _rvrStatusUpdatePoolSize = 10;
@ -2096,6 +2100,18 @@ Configurable, StateListener<VirtualMachine.State, VirtualMachine.Event, VirtualM
" on the virtual router.", RouterLogrotateFrequency.key(), routerLogrotateFrequency, dc.getUuid()));
buf.append(String.format(" logrotatefrequency=%s", routerLogrotateFrequency));
if (SystemVmEnableUserData.valueIn(router.getDataCenterId())) {
String userDataUuid = VirtualRouterUserData.valueIn(dc.getId());
try {
String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
if (StringUtils.isNotBlank(userData)) {
buf.append(" userdata=").append(userData);
}
} catch (Exception e) {
logger.warn("Failed to load user data for the virtual router, ignored", e);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Boot Args for " + profile + ": " + buf);
}
@ -3355,7 +3371,8 @@ Configurable, StateListener<VirtualMachine.State, VirtualMachine.Event, VirtualM
RouterHealthChecksMaxMemoryUsageThreshold,
ExposeDnsAndBootpServer,
RouterLogrotateFrequency,
RemoveControlIpOnStop
RemoveControlIpOnStop,
VirtualRouterUserData
};
}

View File

@ -44,6 +44,13 @@ public interface SecondaryStorageVmManager extends Manager {
"The time interval(in millisecond) to scan whether or not system needs more SSVM to ensure minimal standby capacity",
false);
ConfigKey<String> SecondaryStorageVmUserData = new ConfigKey<>(String.class, "secstorage.vm.userdata",
ConfigKey.CATEGORY_ADVANCED, "",
"UUID for user data for secondary storage VMs. This works only when systemvm.userdata.enabled is set to true",
true, ConfigKey.Scope.Zone, null, "User Data for SSVMs",
null, ConfigKey.GROUP_SYSTEM_VMS, ConfigKey.SUBGROUP_SEC_STORAGE_VM);
public static final int DEFAULT_SS_VM_RAMSIZE = 512; // 512M
public static final int DEFAULT_SS_VM_CPUMHZ = 500; // 500 MHz
public static final int DEFAULT_SS_VM_MTUSIZE = 1500;

View File

@ -17,6 +17,7 @@
package org.apache.cloudstack.secondarystorage;
import static com.cloud.configuration.Config.SecStorageAllowedInternalDownloadSites;
import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
import java.net.URI;
import java.net.URISyntaxException;
@ -50,6 +51,7 @@ import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao;
import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
@ -255,6 +257,9 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar
private IndirectAgentLB indirectAgentLB;
@Inject
private CAManager caManager;
@Inject
private UserDataManager userDataManager;
private int _secStorageVmMtuSize;
private String _instance;
@ -1227,6 +1232,19 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar
String nfsVersion = imageStoreDetailsUtil != null ? imageStoreDetailsUtil.getNfsVersion(secStores.get(0).getId()) : null;
buf.append(" nfsVersion=").append(nfsVersion);
buf.append(" keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16)));
if (SystemVmEnableUserData.valueIn(dc.getId())) {
String userDataUuid = SecondaryStorageVmUserData.valueIn(dc.getId());
try {
String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
if (StringUtils.isNotBlank(userData)) {
buf.append(" userdata=").append(userData);
}
} catch (Exception e) {
logger.warn("Failed to load user data for the ssvm, ignored", e);
}
}
String bootArgs = buf.toString();
if (logger.isDebugEnabled()) {
logger.debug(String.format("Boot args for machine profile [%s]: [%s].", profile.toString(), bootArgs));
@ -1529,7 +1547,8 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {NTPServerConfig, MaxNumberOfSsvmsForMigration, SecondaryStorageCapacityScanInterval};
return new ConfigKey<?>[] {NTPServerConfig, MaxNumberOfSsvmsForMigration, SecondaryStorageCapacityScanInterval,
SecondaryStorageVmUserData};
}
}

View File

@ -20,6 +20,8 @@ set -x
PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
CMDLINE=/var/cache/cloud/cmdline
. /lib/lsb/init-functions
log_it() {
echo "$(date) $@" >> /var/log/cloud.log
log_action_msg "$@"

View File

@ -18,6 +18,8 @@
#
# This scripts before ssh.service but after cloud-early-config
. /lib/lsb/init-functions
log_it() {
echo "$(date) $@" >> /var/log/cloud.log
log_action_msg "$@"
@ -47,6 +49,97 @@ fi
CMDLINE=/var/cache/cloud/cmdline
TYPE=$(grep -Po 'type=\K[a-zA-Z]*' $CMDLINE)
# Execute cloud-init if user data is present
run_cloud_init() {
if [ ! -f "$CMDLINE" ]; then
log_it "No cmdline file found, skipping cloud-init execution"
return 0
fi
local encoded_userdata=$(grep -Po 'userdata=\K[^[:space:]]*' "$CMDLINE" || true)
if [ -z "$encoded_userdata" ]; then
log_it "No user data found in cmdline, skipping cloud-init execution"
return 0
fi
log_it "User data detected, setting up and running cloud-init"
# Update cloud-init config to use NoCloud datasource
cat <<EOF > /etc/cloud/cloud.cfg.d/cloudstack.cfg
#cloud-config
datasource_list: ['NoCloud']
network:
config: disabled
manage_etc_hosts: false
manage_resolv_conf: false
users: []
disable_root: false
ssh_pwauth: false
cloud_init_modules:
- migrator
- seed_random
- bootcmd
- write-files
- growpart
- resizefs
- disk_setup
- mounts
- rsyslog
cloud_config_modules:
- locale
- timezone
- runcmd
cloud_final_modules:
- scripts-per-once
- scripts-per-boot
- scripts-per-instance
- scripts-user
- final-message
- power-state-change
EOF
# Set up user data files (reuse the function from init.sh)
mkdir -p /var/lib/cloud/seed/nocloud
# Decode and decompress user data
local decoded_userdata
decoded_userdata=$(echo "$encoded_userdata" | base64 -d 2>/dev/null | gunzip 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$decoded_userdata" ]; then
log_it "ERROR: Failed to decode or decompress user data"
return 1
fi
# Write user data
echo "$decoded_userdata" > /var/lib/cloud/seed/nocloud/user-data
chmod 600 /var/lib/cloud/seed/nocloud/user-data
# Create meta-data
local instance_name=$(grep -Po 'name=\K[^[:space:]]*' "$CMDLINE" || hostname)
cat > /var/lib/cloud/seed/nocloud/meta-data << EOF
instance-id: $instance_name
local-hostname: $instance_name
EOF
chmod 644 /var/lib/cloud/seed/nocloud/meta-data
log_it "User data files created, executing cloud-init..."
# Run cloud-init stages manually
cloud-init init --local && \
cloud-init init && \
cloud-init modules --mode=config && \
cloud-init modules --mode=final
local cloud_init_result=$?
if [ $cloud_init_result -eq 0 ]; then
log_it "Cloud-init executed successfully"
else
log_it "ERROR: Cloud-init execution failed with exit code: $cloud_init_result"
fi
return $cloud_init_result
}
if [ "$TYPE" == "router" ] || [ "$TYPE" == "vpcrouter" ] || [ "$TYPE" == "dhcpsrvr" ]
then
if [ -x /opt/cloud/bin/update_config.py ]
@ -71,4 +164,6 @@ do
systemctl disable --now --no-block $svc
done
run_cloud_init
date > /var/cache/cloud/boot_up_done

View File

@ -18,15 +18,14 @@
"""
# Import Local Modules
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.cloudstackAPI import (stopSystemVm,
from marvin.cloudstackAPI import (getDiagnosticsData, stopSystemVm,
rebootSystemVm,
destroySystemVm, updateConfiguration)
from marvin.lib.utils import (cleanup_resources,
get_process_status,
get_host_credentials,
wait_until)
from marvin.lib.base import (PhysicalNetwork,
NetScaler, ImageStore)
from marvin.lib.base import (PhysicalNetwork, NetScaler, ImageStore, UserData)
from marvin.lib.common import (get_zone,
list_hosts,
list_ssvms,
@ -35,6 +34,10 @@ from marvin.lib.common import (get_zone,
from nose.plugins.attrib import attr
import telnetlib
import logging
import base64
import os
import urllib
import zipfile
# Import System modules
import time

View File

@ -0,0 +1,410 @@
# 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.
# Import Local Modules
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.cloudstackAPI import (
getDiagnosticsData,
stopSystemVm,
rebootSystemVm,
destroySystemVm,
updateConfiguration,
)
from marvin.lib.utils import (
cleanup_resources,
get_process_status,
get_host_credentials,
wait_until,
)
from marvin.lib.base import UserData, Network
from marvin.lib.common import (
get_zone,
list_hosts,
list_routers,
list_ssvms,
list_zones,
list_vlan_ipranges,
createEnabledNetworkOffering,
)
from marvin.codes import PASS
from nose.plugins.attrib import attr
import telnetlib
import logging
import base64
import os
import urllib
import zipfile
import uuid
import shutil
# Import System modules
import time
_multiprocess_shared_ = True
class TestSystemVMUserData(cloudstackTestCase):
@classmethod
def setUpClass(cls):
cls.testClient = super(TestSystemVMUserData, cls).getClsTestClient()
cls.api_client = cls.testClient.getApiClient()
# Fill services from the external config file
cls.testData = cls.testClient.getParsedTestDataConfig()
# Enable user data and set the script to be run on SSVM
cmd = updateConfiguration.updateConfigurationCmd()
cmd.name = "systemvm.userdata.enabled"
cmd.value = "true"
cls.api_client.updateConfiguration(cmd)
@classmethod
def tearDownClass(cls):
# Disable user data
cmd = updateConfiguration.updateConfigurationCmd()
cmd.name = "systemvm.userdata.enabled"
cmd.value = "false"
cls.api_client.updateConfiguration(cmd)
def setUp(self):
test_case = super(TestSystemVMUserData, self)
self.apiclient = self.testClient.getApiClient()
self.hypervisor = self.testClient.getHypervisorInfo()
self.cleanup = []
self.config = test_case.getClsConfig()
self.services = self.testClient.getParsedTestDataConfig()
self.zone = get_zone(self.apiclient, self.testClient.getZoneForTests())
self.logger = logging.getLogger("TestSystemVMUserData")
self.stream_handler = logging.StreamHandler()
self.logger.setLevel(logging.DEBUG)
self.logger.addHandler(self.stream_handler)
def tearDown(self):
if self.userdata_id:
UserData.delete(self.apiclient, self.userdata_id)
self.userdata_id = None
try:
cleanup_resources(self.apiclient, self.cleanup)
except Exception as e:
raise Exception("Warning: Exception during cleanup : %s" % e)
def waitForSystemVMAgent(self, vmname):
def checkRunningAgent():
list_host_response = list_hosts(self.apiclient, name=vmname)
if isinstance(list_host_response, list):
return list_host_response[0].state == "Up", None
return False, None
res, _ = wait_until(3, 300, checkRunningAgent)
if not res:
raise Exception("Failed to wait for SSVM agent to be Up")
def checkForRunningSystemVM(self, ssvm, ssvm_type=None):
if not ssvm:
return None
def checkRunningState():
if not ssvm_type:
response = list_ssvms(self.apiclient, id=ssvm.id)
else:
response = list_ssvms(
self.apiclient, zoneid=self.zone.id, systemvmtype=ssvm_type
)
if isinstance(response, list):
ssvm_response = response[0]
return ssvm_response.state == "Running", ssvm_response
return False, None
res, ssvm_response = wait_until(3, 300, checkRunningState)
if not res:
self.fail("Failed to reach systemvm state to Running")
return ssvm_response
def register_userdata(
self, userdata_name, global_setting_name, vm_type_display_name
):
"""Helper method to register userdata and configure the global setting
Args:
userdata_name: Name for the userdata entry
global_setting_name: Global setting name to update (e.g., 'secstorage.vm.userdata', 'console.proxy.vm.userdata', 'virtual.router.userdata')
vm_type_display_name: Display name for the VM type (e.g., 'SSVM', 'CPVM', 'VR')
Returns:
UserData object
"""
userdata_script = f"""#!/bin/bash
echo "User data script ran successfully on {vm_type_display_name}" > /tmp/userdata.txt
"""
b64_encoded_userdata = base64.b64encode(userdata_script.encode("utf-8")).decode(
"utf-8"
)
# Create a userdata entry
try:
userdata = UserData.register(
self.apiclient, name=userdata_name, userdata=b64_encoded_userdata
)
userdata_id = userdata["userdata"]["id"]
except Exception as e:
if "already exists" in str(e):
self.debug("Userdata already exists, getting it")
userdata = UserData.list(
self.apiclient, name=userdata_name, listall=True
)[0]
userdata_id = userdata.id
else:
self.fail("Failed to register userdata: %s" % e)
# Update global configuration to use this userdata
cmd = updateConfiguration.updateConfigurationCmd()
cmd.name = global_setting_name
cmd.value = userdata_id
self.apiclient.updateConfiguration(cmd)
self.debug(
"Updated global setting %s with userdata ID: %s"
% (global_setting_name, userdata.id)
)
return userdata_id
def download_and_verify_diagnostics_data(
self, target_id, vm_type_display_name, expected_content, retries=4
):
"""Helper method to download and verify diagnostics data
Args:
target_id: ID of the target VM/router
vm_type_display_name: Display name for log messages (e.g., 'SSVM', 'CPVM', 'VR')
expected_content: Expected content to verify in the userdata file
retries: Number of retries for getDiagnosticsData (default: 4)
"""
# Create a random temporary directory for this test
random_suffix = uuid.uuid4().hex[:8]
vm_type_prefix = vm_type_display_name.lower()
temp_dir = f"/tmp/{vm_type_prefix}-{random_suffix}"
os.makedirs(temp_dir, exist_ok=True)
# Download the file created by userdata script using the getDiagnosticsData command
cmd = getDiagnosticsData.getDiagnosticsDataCmd()
cmd.targetid = target_id
cmd.files = "/tmp/userdata.txt"
# getDiagnosticsData command takes some time to work after a VM is started
response = None
while retries > -1:
try:
response = self.apiclient.getDiagnosticsData(cmd)
break # Success, exit retry loop
except Exception as e:
if retries >= 0:
retries = retries - 1
self.debug(
"getDiagnosticsData failed (retries left: %d): %s"
% (retries + 1, e)
)
if retries > -1:
time.sleep(30)
continue
# If all retries exhausted, re-raise the exception
self.fail("Failed to get diagnostics data after retries: %s" % e)
# Download response.url file to temporary directory and extract it
zip_file_path = os.path.join(temp_dir, "userdata.zip")
extracted_file_path = os.path.join(temp_dir, "userdata.txt")
self.debug(
"Downloading userdata file from %s to %s"
% (vm_type_display_name, zip_file_path)
)
try:
urllib.request.urlretrieve(response.url, zip_file_path)
except Exception as e:
self.fail(
"Failed to download userdata file from %s: %s"
% (vm_type_display_name, e)
)
self.debug(
"Downloaded userdata file from %s: %s"
% (vm_type_display_name, zip_file_path)
)
try:
with zipfile.ZipFile(zip_file_path, "r") as zip_ref:
zip_ref.extractall(temp_dir)
except zipfile.BadZipFile as e:
self.fail("Downloaded userdata file is not a zip file: %s" % e)
self.debug("Extracted userdata file from zip: %s" % extracted_file_path)
# Verify the file contains the expected content
try:
with open(extracted_file_path, "r") as f:
content = f.read().strip()
self.debug("Userdata file content: %s" % content)
self.assertEqual(
expected_content in content,
True,
f"Check that userdata file contains expected content: '{expected_content}'",
)
except FileNotFoundError:
self.fail(
"Userdata file not found in extracted zip at %s" % extracted_file_path
)
except Exception as e:
self.fail("Failed to read userdata file: %s" % e)
finally:
# Clean up temporary directory
try:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
self.debug("Cleaned up temporary directory: %s" % temp_dir)
except Exception as e:
self.debug(
"Failed to clean up temporary directory %s: %s" % (temp_dir, e)
)
def test_userdata_on_systemvm(
self, systemvm_type, userdata_name, vm_type_display_name, global_setting_name
):
"""Helper method to test user data functionality on system VMs
Args:
systemvm_type: Type of system VM ('secondarystoragevm' or 'consoleproxy')
userdata_name: Name for the userdata entry
vm_type_display_name: Display name for log messages (e.g., 'SSVM' or 'CPVM')
global_setting_name: Global setting name for userdata (e.g., 'secstorage.vm.userdata' or 'console.proxy.vm.userdata')
"""
# 1) Register userdata and configure global setting
self.userdata_id = self.register_userdata(
userdata_name, global_setting_name, vm_type_display_name
)
# 2) Get and destroy the system VM to trigger recreation with userdata
list_ssvm_response = list_ssvms(
self.apiclient,
systemvmtype=systemvm_type,
state="Running",
zoneid=self.zone.id,
)
self.assertEqual(
isinstance(list_ssvm_response, list),
True,
"Check list response returns a valid list",
)
ssvm = list_ssvm_response[0]
self.debug("Destroying %s: %s" % (vm_type_display_name, ssvm.id))
cmd = destroySystemVm.destroySystemVmCmd()
cmd.id = ssvm.id
self.apiclient.destroySystemVm(cmd)
# 3) Wait for the system VM to be running again
ssvm_response = self.checkForRunningSystemVM(ssvm, systemvm_type)
self.debug(
"%s state after restart: %s" % (vm_type_display_name, ssvm_response.state)
)
self.assertEqual(
ssvm_response.state,
"Running",
"Check whether %s is running or not" % vm_type_display_name,
)
# Wait for the agent to be up
self.waitForSystemVMAgent(ssvm_response.name)
# 4) Download and verify the diagnostics data
expected_content = (
f"User data script ran successfully on {vm_type_display_name}"
)
self.download_and_verify_diagnostics_data(
ssvm_response.id, vm_type_display_name, expected_content
)
@attr(
tags=["advanced", "advancedns", "smoke", "basic", "sg"],
required_hardware="true",
)
def test_1_userdata_on_ssvm(self):
"""Test user data functionality on SSVM"""
self.test_userdata_on_systemvm(
systemvm_type="secondarystoragevm",
userdata_name="ssvm_userdata",
vm_type_display_name="SSVM",
global_setting_name="secstorage.vm.userdata",
)
@attr(
tags=["advanced", "advancedns", "smoke", "basic", "sg"],
required_hardware="true",
)
def test_2_userdata_on_cpvm(self):
"""Test user data functionality on CPVM"""
self.test_userdata_on_systemvm(
systemvm_type="consoleproxy",
userdata_name="cpvm_userdata",
vm_type_display_name="CPVM",
global_setting_name="console.proxy.vm.userdata",
)
@attr(
tags=["advanced", "advancedns", "smoke", "basic", "sg"],
required_hardware="true",
)
def test_3_userdata_on_vr(self):
"""Test user data functionality on VR"""
# 1) Register userdata and configure global setting
self.userdata_id = self.register_userdata("vr_userdata", "virtual.router.userdata", "VR")
# 2) Create an isolated network which will trigger VR creation with userdata
result = createEnabledNetworkOffering(
self.apiclient, self.testData["nw_off_isolated_persistent"]
)
assert result[0] == PASS, (
"Network offering creation/enabling failed due to %s" % result[2]
)
isolated_persistent_network_offering = result[1]
# Create an isolated network
self.network = Network.create(
self.apiclient,
self.testData["isolated_network"],
networkofferingid=isolated_persistent_network_offering.id,
zoneid=self.zone.id,
)
self.assertIsNotNone(self.network, "Network creation failed")
self.cleanup.append(self.network)
self.cleanup.append(isolated_persistent_network_offering)
# 3) Get the VR and verify it's running
routers = list_routers(
self.apiclient, networkid=self.network.id, state="Running"
)
self.assertEqual(
isinstance(routers, list),
True,
"Check list router response returns a valid list",
)
self.assertNotEqual(len(routers), 0, "Check list router response")
router = routers[0]
self.debug("Found VR: %s" % router.id)
# 4) Download and verify the diagnostics data
# VR doesn't need retries as it's freshly created
expected_content = "User data script ran successfully on VR"
self.download_and_verify_diagnostics_data(router.id, "VR", expected_content)

View File

@ -133,7 +133,7 @@ function configure_services() {
systemctl disable containerd
# Disable cloud init by default
cat <<EOF > /etc/cloud/cloud.cfg.d/cloudstack.cfg
cat <<EOF > /etc/cloud/cloud.cfg.d/cloudstack.cfg
datasource_list: ['CloudStack']
datasource:
CloudStack: