From f4b2a1866f06cf4b67477d0e52b46ed628ef7a65 Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Wed, 10 Sep 2025 14:48:07 +0530 Subject: [PATCH 1/9] Add support for providing userdata to system VMs --- .../com/cloud/vm/VirtualMachineManager.java | 3 + .../cloud/vm/VirtualMachineManagerImpl.java | 2 +- .../framework/config/ConfigKey.java | 59 +++++++++++ .../lb/ElasticLoadBalancerManagerImpl.java | 14 +++ .../lb/InternalLoadBalancerVMManagerImpl.java | 13 +++ .../ConfigurationManagerImpl.java | 8 ++ .../consoleproxy/ConsoleProxyManager.java | 6 ++ .../consoleproxy/ConsoleProxyManagerImpl.java | 14 ++- .../VirtualNetworkApplianceManager.java | 6 ++ .../VirtualNetworkApplianceManagerImpl.java | 13 ++- .../secondary/SecondaryStorageVmManager.java | 7 ++ .../SecondaryStorageManagerImpl.java | 13 ++- systemvm/debian/opt/cloud/bin/setup/init.sh | 2 + .../debian/opt/cloud/bin/setup/postinit.sh | 98 +++++++++++++++++++ .../scripts/configure_systemvm_services.sh | 2 +- 15 files changed, 255 insertions(+), 5 deletions(-) diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index c05c29add556..6df3ef1d21bd 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -106,6 +106,9 @@ public interface VirtualMachineManager extends Manager { ConfigKey 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 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 consoleproxy.userdata, secstorage.userdata, and 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"; diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index b55972803646..4f3b38f9815b 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -5136,7 +5136,7 @@ public ConfigKey[] getConfigKeys() { VmConfigDriveLabel, VmConfigDriveOnPrimaryPool, VmConfigDriveForceHostCacheUse, VmConfigDriveUseHostCacheOnUnsupportedPool, HaVmRestartHostUp, ResourceCountRunningVMsonly, AllowExposeHypervisorHostname, AllowExposeHypervisorHostnameAccountLevel, SystemVmRootDiskSize, AllowExposeDomainInMetadata, MetadataCustomCloudName, VmMetadataManufacturer, VmMetadataProductName, - VmSyncPowerStateTransitioning + VmSyncPowerStateTransitioning, SystemVmEnableUserData }; } diff --git a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java index 88eca1d28dee..8de852c8fb68 100644 --- a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java @@ -41,8 +41,67 @@ public class ConfigKey { public static final String CATEGORY_ADVANCED = "Advanced"; public static final String CATEGORY_ALERT = "Alert"; public static final String CATEGORY_NETWORK = "Network"; + public static final String CATEGORY_SECURE = "Secure"; 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 GROUP_MISCELLANEOUS = new Ternary<>("Miscellaneous", "Miscellaneous configuration", 999L); + public static final Ternary GROUP_ACCESS = new Ternary<>("Access", "Identity and Access management configuration", 1L); + public static final Ternary GROUP_COMPUTE = new Ternary<>("Compute", "Compute configuration", 2L); + public static final Ternary GROUP_STORAGE = new Ternary<>("Storage", "Storage configuration", 3L); + public static final Ternary GROUP_NETWORK = new Ternary<>("Network", "Network configuration", 4L); + public static final Ternary GROUP_HYPERVISOR = new Ternary<>("Hypervisor", "Hypervisor specific configuration", 5L); + public static final Ternary GROUP_MANAGEMENT_SERVER = new Ternary<>("Management Server", "Management Server configuration", 6L); + public static final Ternary GROUP_SYSTEM_VMS = new Ternary<>("System VMs", "System VMs related configuration", 7L); + public static final Ternary GROUP_INFRASTRUCTURE = new Ternary<>("Infrastructure", "Infrastructure configuration", 8L); + public static final Ternary 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 SUBGROUP_OTHERS = new Pair<>("Others", 999L); + public static final Pair SUBGROUP_ACCOUNT = new Pair<>("Account", 1L); + public static final Pair SUBGROUP_DOMAIN = new Pair<>("Domain", 2L); + public static final Pair SUBGROUP_PROJECT = new Pair<>("Project", 3L); + public static final Pair SUBGROUP_LDAP = new Pair<>("LDAP", 4L); + public static final Pair SUBGROUP_SAML = new Pair<>("SAML", 5L); + public static final Pair SUBGROUP_VIRTUAL_MACHINE = new Pair<>("Virtual Machine", 1L); + public static final Pair SUBGROUP_KUBERNETES = new Pair<>("Kubernetes", 2L); + public static final Pair SUBGROUP_HIGH_AVAILABILITY = new Pair<>("High Availability", 3L); + public static final Pair SUBGROUP_IMAGES = new Pair<>("Images", 1L); + public static final Pair SUBGROUP_VOLUME = new Pair<>("Volume", 2L); + public static final Pair SUBGROUP_SNAPSHOT = new Pair<>("Snapshot", 3L); + public static final Pair SUBGROUP_VM_SNAPSHOT = new Pair<>("VM Snapshot", 4L); + public static final Pair SUBGROUP_NETWORK = new Pair<>("Network", 1L); + public static final Pair SUBGROUP_DHCP = new Pair<>("DHCP", 2L); + public static final Pair SUBGROUP_VPC = new Pair<>("VPC", 3L); + public static final Pair SUBGROUP_LOADBALANCER = new Pair<>("LoadBalancer", 4L); + public static final Pair SUBGROUP_API = new Pair<>("API", 1L); + public static final Pair SUBGROUP_ALERTS = new Pair<>("Alerts", 2L); + public static final Pair SUBGROUP_EVENTS = new Pair<>("Events", 3L); + public static final Pair SUBGROUP_SECURITY = new Pair<>("Security", 4L); + public static final Pair SUBGROUP_USAGE = new Pair<>("Usage", 1L); + public static final Pair SUBGROUP_LIMITS = new Pair<>("Limits", 6L); + public static final Pair SUBGROUP_JOBS = new Pair<>("Jobs", 7L); + public static final Pair SUBGROUP_AGENT = new Pair<>("Agent", 8L); + public static final Pair SUBGROUP_HYPERVISOR = new Pair<>("Hypervisor", 1L); + public static final Pair SUBGROUP_KVM = new Pair<>("KVM", 2L); + public static final Pair SUBGROUP_VMWARE = new Pair<>("VMware", 3L); + public static final Pair SUBGROUP_XENSERVER = new Pair<>("XenServer", 4L); + public static final Pair SUBGROUP_OVM = new Pair<>("OVM", 5L); + public static final Pair SUBGROUP_BAREMETAL = new Pair<>("Baremetal", 6L); + public static final Pair SUBGROUP_CONSOLE_PROXY_VM = new Pair<>("ConsoleProxyVM", 1L); + public static final Pair SUBGROUP_SEC_STORAGE_VM = new Pair<>("SecStorageVM", 2L); + public static final Pair SUBGROUP_VIRTUAL_ROUTER = new Pair<>("VirtualRouter", 3L); + public static final Pair SUBGROUP_DIAGNOSTICS = new Pair<>("Diagnostics", 4L); + public static final Pair SUBGROUP_PRIMARY_STORAGE = new Pair<>("Primary Storage", 1L); + public static final Pair SUBGROUP_SECONDARY_STORAGE = new Pair<>("Secondary Storage", 2L); + public static final Pair SUBGROUP_BACKUP_AND_RECOVERY = new Pair<>("Backup & Recovery", 1L); + public static final Pair SUBGROUP_CERTIFICATE_AUTHORITY = new Pair<>("Certificate Authority", 2L); + public static final Pair SUBGROUP_QUOTA = new Pair<>("Quota", 3L); + public static final Pair SUBGROUP_CLOUDIAN = new Pair<>("Cloudian", 4L); + public static final Pair SUBGROUP_DRS = new Pair<>("DRS", 4L); + public enum Scope { Global(null, 1), Zone(Global, 1 << 1), diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java index f895ba2944cc..02331a810242 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java @@ -17,6 +17,7 @@ package com.cloud.network.lb; import java.util.ArrayList; +import java.util.Base64; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -35,6 +36,7 @@ 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.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import com.cloud.agent.AgentManager; @@ -101,6 +103,9 @@ import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; +import static com.cloud.network.router.VirtualNetworkApplianceManager.RouterUserData; +import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData; + @Component public class ElasticLoadBalancerManagerImpl extends ManagerBase implements ElasticLoadBalancerManager, VirtualMachineGuru { @@ -477,6 +482,15 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl } String msPublicKey = _configDao.getValue("ssh.publickey"); buf.append(" authorized_key=").append(VirtualMachineGuru.getEncodedMsPublicKey(msPublicKey)); + + if (SystemVmEnableUserData.valueIn(dc.getId())) { + String userData = RouterUserData.valueIn(dc.getId()); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } + if (logger.isDebugEnabled()) { logger.debug("Boot Args for " + profile + ": " + buf.toString()); } diff --git a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java index d979a4b30335..7d496f665243 100644 --- a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java +++ b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java @@ -21,9 +21,12 @@ 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.RouterUserData; +import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -126,6 +129,7 @@ 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"; @@ -243,6 +247,15 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile final String type = "ilbvm"; buf.append(" type=" + type); + long dcId = profile.getVirtualMachine().getDataCenterId(); + if (SystemVmEnableUserData.valueIn(dcId)) { + String userData = RouterUserData.valueIn(dcId); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } + if (logger.isDebugEnabled()) { logger.debug("Boot Args for " + profile + ": " + buf.toString()); } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 2c5a931a8319..c4da58d783bd 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -50,6 +50,9 @@ 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 @@ protected void overProvisioningFactorsForValidation() { protected void populateConfigKeysAllowedOnlyForDefaultAdmin() { configKeysAllowedOnlyForDefaultAdmin.add(AccountManagerImpl.listOfRoleTypesAllowedForOperationsOfSameRoleType.key()); configKeysAllowedOnlyForDefaultAdmin.add(AccountManagerImpl.allowOperationsOnUsersInSameAccount.key()); + + configKeysAllowedOnlyForDefaultAdmin.add(VirtualMachineManager.SystemVmEnableUserData.key()); + configKeysAllowedOnlyForDefaultAdmin.add(ConsoleProxyManager.ConsoleProxyUserData.key()); + configKeysAllowedOnlyForDefaultAdmin.add(SecondaryStorageVmManager.SecondaryStorageUserData.key()); + configKeysAllowedOnlyForDefaultAdmin.add(VirtualNetworkApplianceManager.RouterUserData.key()); } private void initMessageBusListener() { diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java index 7b5fc123fb0e..fcb92a24aca8 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java @@ -97,6 +97,12 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService { ConfigKey ConsoleProxyManagementLastState = new ConfigKey(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 ConsoleProxyUserData = new ConfigKey<>(String.class, "consoleproxy.userdata", + ConfigKey.CATEGORY_SECURE, "", + "Default 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(); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index c273cf40e2fd..df160ebb8712 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -19,6 +19,7 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -152,6 +153,8 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; +import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData; + /** * Class to manage console proxys.

* Possible console proxy state transition cases:
@@ -1265,6 +1268,15 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl buf.append(" vncport=").append(getVncPort(datacenterId)); } buf.append(" keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16))); + + if (SystemVmEnableUserData.valueIn(dc.getId())) { + String userData = ConsoleProxyUserData.valueIn(dc.getId()); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } + String bootArgs = buf.toString(); if (logger.isDebugEnabled()) { logger.debug("Boot Args for " + profile + ": " + bootArgs); @@ -1572,7 +1584,7 @@ public String getConfigComponentName() { public ConfigKey[] getConfigKeys() { return new ConfigKey[] { ConsoleProxySslEnabled, NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled, ConsoleProxyServiceOffering, ConsoleProxyCapacityStandby, ConsoleProxyCapacityScanInterval, ConsoleProxyCmdPort, ConsoleProxyRestart, ConsoleProxyUrlDomain, ConsoleProxySessionMax, ConsoleProxySessionTimeout, ConsoleProxyDisableRpFilter, ConsoleProxyLaunchMax, - ConsoleProxyManagementLastState, ConsoleProxyServiceManagementState, NoVncConsoleShowDot }; + ConsoleProxyManagementLastState, ConsoleProxyServiceManagementState, NoVncConsoleShowDot, ConsoleProxyUserData }; } protected ConsoleProxyStatus parseJsonToConsoleProxyStatus(String json) throws JsonParseException { diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java index f77081aa96cc..7c02d7ebed32 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java @@ -64,6 +64,12 @@ public interface VirtualNetworkApplianceManager extends Manager, VirtualNetworkA ConfigKey RouterTemplateOvm3 = new ConfigKey<>(String.class, RouterTemplateOvm3CK, "Advanced", "SystemVM Template (Ovm3)", "Name of the default router template on Ovm3.", true, ConfigKey.Scope.Zone, null); + ConfigKey RouterUserData = new ConfigKey<>(String.class, "router.userdata", + ConfigKey.CATEGORY_SECURE, "", + "Default user data for 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 SetServiceMonitor = new ConfigKey<>(Boolean.class, SetServiceMonitorCK, "Advanced", "true", "service monitoring in router enable/disable option, default true", true, ConfigKey.Scope.Zone, null); diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index eb5995b56f88..ea6a114a9869 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -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; @@ -27,6 +28,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Base64; import java.util.Calendar; import java.util.Collections; import java.util.Date; @@ -2096,6 +2098,14 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile " on the virtual router.", RouterLogrotateFrequency.key(), routerLogrotateFrequency, dc.getUuid())); buf.append(String.format(" logrotatefrequency=%s", routerLogrotateFrequency)); + if (SystemVmEnableUserData.valueIn(router.getDataCenterId())) { + String userData = RouterUserData.valueIn(router.getDataCenterId()); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } + if (logger.isDebugEnabled()) { logger.debug("Boot Args for " + profile + ": " + buf); } @@ -3355,7 +3365,8 @@ public ConfigKey[] getConfigKeys() { RouterHealthChecksMaxMemoryUsageThreshold, ExposeDnsAndBootpServer, RouterLogrotateFrequency, - RemoveControlIpOnStop + RemoveControlIpOnStop, + RouterUserData }; } diff --git a/server/src/main/java/com/cloud/storage/secondary/SecondaryStorageVmManager.java b/server/src/main/java/com/cloud/storage/secondary/SecondaryStorageVmManager.java index a34658a7f6d4..7dbb62f7c275 100644 --- a/server/src/main/java/com/cloud/storage/secondary/SecondaryStorageVmManager.java +++ b/server/src/main/java/com/cloud/storage/secondary/SecondaryStorageVmManager.java @@ -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 SecondaryStorageUserData = new ConfigKey<>(String.class, "secstorage.userdata", + ConfigKey.CATEGORY_SECURE, "", + "Default 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; diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index fae8e69a386c..48743e1df9f5 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -17,11 +17,13 @@ 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; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -1227,6 +1229,15 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl 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 userData = SecondaryStorageUserData.valueIn(dc.getId()); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } + String bootArgs = buf.toString(); if (logger.isDebugEnabled()) { logger.debug(String.format("Boot args for machine profile [%s]: [%s].", profile.toString(), bootArgs)); @@ -1529,7 +1540,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] {NTPServerConfig, MaxNumberOfSsvmsForMigration, SecondaryStorageCapacityScanInterval}; + return new ConfigKey[] {NTPServerConfig, MaxNumberOfSsvmsForMigration, SecondaryStorageCapacityScanInterval, SecondaryStorageUserData}; } } diff --git a/systemvm/debian/opt/cloud/bin/setup/init.sh b/systemvm/debian/opt/cloud/bin/setup/init.sh index b6da70593669..66b08b992059 100755 --- a/systemvm/debian/opt/cloud/bin/setup/init.sh +++ b/systemvm/debian/opt/cloud/bin/setup/init.sh @@ -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 "$@" diff --git a/systemvm/debian/opt/cloud/bin/setup/postinit.sh b/systemvm/debian/opt/cloud/bin/setup/postinit.sh index 801770fcc834..97f825f4682c 100755 --- a/systemvm/debian/opt/cloud/bin/setup/postinit.sh +++ b/systemvm/debian/opt/cloud/bin/setup/postinit.sh @@ -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,100 @@ 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 < /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 potentially decompress user data + local decoded_userdata + decoded_userdata=$(echo "$encoded_userdata" | base64 -d 2>/dev/null) + if [ $? -ne 0 ] || [ -z "$decoded_userdata" ]; then + log_it "ERROR: Failed to decode base64 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..." + + # Clean any previous cloud-init state + cloud-init clean --logs + + # 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 +167,6 @@ do systemctl disable --now --no-block $svc done +run_cloud_init + date > /var/cache/cloud/boot_up_done diff --git a/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh b/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh index 02a5c39dc712..219586a97291 100755 --- a/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh +++ b/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh @@ -133,7 +133,7 @@ function configure_services() { systemctl disable containerd # Disable cloud init by default -cat < /etc/cloud/cloud.cfg.d/cloudstack.cfg + cat < /etc/cloud/cloud.cfg.d/cloudstack.cfg datasource_list: ['CloudStack'] datasource: CloudStack: From 67e04a7591195279db78f3b145c6cd46a0d9abf8 Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Mon, 22 Sep 2025 18:35:12 +0530 Subject: [PATCH 2/9] Use userdata uuid instead of user data in global settings --- .../cloudstack/userdata/UserDataManager.java | 2 ++ .../userdata/UserDataManagerImpl.java | 25 +++++++++++++++++++ .../framework/config/ConfigKey.java | 1 - .../lb/ElasticLoadBalancerManagerImpl.java | 20 ++++++++++++--- .../lb/InternalLoadBalancerVMManagerImpl.java | 20 ++++++++++++--- .../consoleproxy/ConsoleProxyManager.java | 4 +-- .../consoleproxy/ConsoleProxyManagerImpl.java | 20 ++++++++++++--- .../VirtualNetworkApplianceManager.java | 4 +-- .../VirtualNetworkApplianceManagerImpl.java | 22 +++++++++++++--- .../secondary/SecondaryStorageVmManager.java | 4 +-- .../SecondaryStorageManagerImpl.java | 22 +++++++++++++--- .../debian/opt/cloud/bin/setup/postinit.sh | 3 --- 12 files changed, 117 insertions(+), 30 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 index b4bede248907..2dcb52fac1e3 100644 --- a/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java +++ b/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.userdata; +import com.cloud.template.VirtualMachineTemplate; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -29,4 +30,5 @@ public interface UserDataManager extends Manager, Configurable { String concatenateUserData(String userdata1, String userdata2, String userdataProvider); String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod); + Long validateAndGetUserDataIdForSystemVms(String userDataUuid, VirtualMachineTemplate vmTemplate); } 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 index 664d308e28d1..1f81ec9e80e8 100644 --- a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java +++ b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java @@ -22,6 +22,11 @@ import java.util.List; import java.util.Map; +import com.cloud.domain.Domain; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.User; +import com.cloud.user.UserDataVO; +import com.cloud.user.dao.UserDataDao; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.commons.codec.binary.Base64; @@ -31,7 +36,12 @@ 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 +128,21 @@ public String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) { return Base64.encodeBase64String(decodedUserData); } + @Override + public Long validateAndGetUserDataIdForSystemVms(String userDataUuid, VirtualMachineTemplate vmTemplate) { + UserDataVO templateUserDataVo = vmTemplate.getUserDataId() != null ? userDataDao.findById(vmTemplate.getUserDataId()): null; + UserDataVO userDataVo = StringUtils.isNotBlank(userDataUuid) ? userDataDao.findByUuid(userDataUuid) : null; + if (isUserDataAllowedForSystemVm(templateUserDataVo) && + isUserDataAllowedForSystemVm(userDataVo)) { + return userDataVo != null ? userDataVo.getId() : null; + } + throw new CloudRuntimeException("User data can only be used by system VMs if it belongs to the ROOT domain and ADMIN account."); + } + + private boolean isUserDataAllowedForSystemVm(UserDataVO userData) { + return userData == null || (userData.getDomainId() == Domain.ROOT_DOMAIN && userData.getAccountId() == User.UID_ADMIN); + } + private byte[] validateAndDecodeByHTTPMethod(String userData, int maxHTTPLength, BaseCmd.HTTPMethod httpMethod) { byte[] decodedUserData = Base64.decodeBase64(userData.getBytes()); if (decodedUserData == null || decodedUserData.length < 1) { diff --git a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java index 8de852c8fb68..0ea910fcb6d2 100644 --- a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java @@ -41,7 +41,6 @@ public class ConfigKey { public static final String CATEGORY_ADVANCED = "Advanced"; public static final String CATEGORY_ALERT = "Alert"; public static final String CATEGORY_NETWORK = "Network"; - public static final String CATEGORY_SECURE = "Secure"; public static final String CATEGORY_SYSTEM = "System"; // Configuration Groups to be used to define group for a config key diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java index 02331a810242..a62f4418684b 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java @@ -31,11 +31,13 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.vm.UserVmManager; import org.apache.cloudstack.api.command.user.loadbalancer.CreateLoadBalancerRuleCmd; 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; @@ -141,6 +143,10 @@ public class ElasticLoadBalancerManagerImpl extends ManagerBase implements Elast private ElasticLbVmMapDao _elbVmMapDao; @Inject private NicDao _nicDao; + @Inject + private UserDataManager userDataManager; + @Inject + private UserVmManager userVmManager; String _instance; @@ -484,10 +490,16 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl buf.append(" authorized_key=").append(VirtualMachineGuru.getEncodedMsPublicKey(msPublicKey)); if (SystemVmEnableUserData.valueIn(dc.getId())) { - String userData = RouterUserData.valueIn(dc.getId()); - if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); - buf.append(" userdata=").append(encodedUserData); + String userDataUuid = RouterUserData.valueIn(dc.getId()); + try { + Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); + String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } catch (Exception e) { + logger.warn("Failed to load user data for the elastic lb vm, ignored", e); } } diff --git a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java index 7d496f665243..6e7532325365 100644 --- a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java +++ b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java @@ -37,11 +37,13 @@ import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; +import com.cloud.vm.UserVmManager; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; 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; @@ -179,6 +181,10 @@ public class InternalLoadBalancerVMManagerImpl extends ManagerBase implements In ResourceManager _resourceMgr; @Inject UserDao _userDao; + @Inject + private UserDataManager userDataManager; + @Inject + private UserVmManager userVmManager; @Override public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile, final DeployDestination dest, final ReservationContext context) { @@ -249,10 +255,16 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile long dcId = profile.getVirtualMachine().getDataCenterId(); if (SystemVmEnableUserData.valueIn(dcId)) { - String userData = RouterUserData.valueIn(dcId); - if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); - buf.append(" userdata=").append(encodedUserData); + String userDataUuid = RouterUserData.valueIn(dcId); + try { + Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); + String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } catch (Exception e) { + logger.warn("Failed to load user data for the internal lb vm, ignored", e); } } diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java index fcb92a24aca8..66eea577607e 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java @@ -98,8 +98,8 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService { "last console proxy service management state", false, ConfigKey.Kind.Select, consoleProxyManagementStates); ConfigKey ConsoleProxyUserData = new ConfigKey<>(String.class, "consoleproxy.userdata", - ConfigKey.CATEGORY_SECURE, "", - "Default user data for console proxy VMs. This works only when systemvm.userdata.enabled is set to true", + 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); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index df160ebb8712..470a4f41e8c0 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -33,6 +33,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.vm.UserVmManager; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.consoleproxy.ConsoleAccessManager; @@ -49,6 +50,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.userdata.UserDataManager; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; @@ -230,6 +232,10 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy private CAManager caManager; @Inject private NetworkOrchestrationService networkMgr; + @Inject + private UserDataManager userDataManager; + @Inject + private UserVmManager userVmManager; private ConsoleProxyListener consoleProxyListener; @@ -1270,10 +1276,16 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl buf.append(" keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16))); if (SystemVmEnableUserData.valueIn(dc.getId())) { - String userData = ConsoleProxyUserData.valueIn(dc.getId()); - if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); - buf.append(" userdata=").append(encodedUserData); + String userDataUuid = ConsoleProxyUserData.valueIn(dc.getId()); + try { + Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); + String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + if (org.apache.commons.lang3.StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } catch (Exception e) { + logger.warn("Failed to load user data for the cpvm, ignored", e); } } diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java index 7c02d7ebed32..22191e54a0f6 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java @@ -65,8 +65,8 @@ public interface VirtualNetworkApplianceManager extends Manager, VirtualNetworkA "Name of the default router template on Ovm3.", true, ConfigKey.Scope.Zone, null); ConfigKey RouterUserData = new ConfigKey<>(String.class, "router.userdata", - ConfigKey.CATEGORY_SECURE, "", - "Default user data for VR, VPC VR, internal LB, and elastic LB. This works only when systemvm.userdata.enabled is set to true", + 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); diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index ea6a114a9869..22caf575531a 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -49,6 +49,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.vm.UserVmManager; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; @@ -73,6 +74,7 @@ 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; @@ -354,6 +356,11 @@ public class VirtualNetworkApplianceManagerImpl extends ManagerBase implements V @Inject BGPService bgpService; + @Inject + private UserDataManager userDataManager; + @Inject + private UserVmManager userVmManager; + private int _routerStatsInterval = 300; private int _routerCheckInterval = 30; private int _rvrStatusUpdatePoolSize = 10; @@ -2099,10 +2106,17 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile buf.append(String.format(" logrotatefrequency=%s", routerLogrotateFrequency)); if (SystemVmEnableUserData.valueIn(router.getDataCenterId())) { - String userData = RouterUserData.valueIn(router.getDataCenterId()); - if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); - buf.append(" userdata=").append(encodedUserData); + String userDataUuid = RouterUserData.valueIn(dc.getId()); + try { + Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, + profile.getTemplate()); + String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } catch (Exception e) { + logger.warn("Failed to load user data for the virtual router, ignored", e); } } diff --git a/server/src/main/java/com/cloud/storage/secondary/SecondaryStorageVmManager.java b/server/src/main/java/com/cloud/storage/secondary/SecondaryStorageVmManager.java index 7dbb62f7c275..6254823eab06 100644 --- a/server/src/main/java/com/cloud/storage/secondary/SecondaryStorageVmManager.java +++ b/server/src/main/java/com/cloud/storage/secondary/SecondaryStorageVmManager.java @@ -45,8 +45,8 @@ public interface SecondaryStorageVmManager extends Manager { false); ConfigKey SecondaryStorageUserData = new ConfigKey<>(String.class, "secstorage.userdata", - ConfigKey.CATEGORY_SECURE, "", - "Default user data for secondary storage VMs. This works only when systemvm.userdata.enabled is set to true", + 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); diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 48743e1df9f5..501ddcc69851 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -36,6 +36,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.vm.UserVmManager; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.context.CallContext; @@ -52,6 +53,7 @@ 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; @@ -257,6 +259,11 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar private IndirectAgentLB indirectAgentLB; @Inject private CAManager caManager; + @Inject + private UserDataManager userDataManager; + @Inject + private UserVmManager userVmManager; + private int _secStorageVmMtuSize; private String _instance; @@ -1231,10 +1238,17 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl buf.append(" keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16))); if (SystemVmEnableUserData.valueIn(dc.getId())) { - String userData = SecondaryStorageUserData.valueIn(dc.getId()); - if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); - buf.append(" userdata=").append(encodedUserData); + String userDataUuid = SecondaryStorageUserData.valueIn(dc.getId()); + try { + Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, + profile.getTemplate()); + String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + if (StringUtils.isNotBlank(userData)) { + String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + buf.append(" userdata=").append(encodedUserData); + } + } catch (Exception e) { + logger.warn("Failed to load user data for the ssvm, ignored", e); } } diff --git a/systemvm/debian/opt/cloud/bin/setup/postinit.sh b/systemvm/debian/opt/cloud/bin/setup/postinit.sh index 97f825f4682c..ebeea6d0d2a7 100755 --- a/systemvm/debian/opt/cloud/bin/setup/postinit.sh +++ b/systemvm/debian/opt/cloud/bin/setup/postinit.sh @@ -124,9 +124,6 @@ EOF log_it "User data files created, executing cloud-init..." - # Clean any previous cloud-init state - cloud-init clean --logs - # Run cloud-init stages manually cloud-init init --local && \ cloud-init init && \ From a58bf91eed46755316e2fe77ba31c4c0250900fc Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Mon, 22 Sep 2025 19:41:32 +0530 Subject: [PATCH 3/9] gzip user data before sending to commandline --- .../cloud/network/lb/ElasticLoadBalancerManagerImpl.java | 7 ++++++- .../network/lb/InternalLoadBalancerVMManagerImpl.java | 7 ++++++- .../com/cloud/consoleproxy/ConsoleProxyManagerImpl.java | 9 +++++++-- .../router/VirtualNetworkApplianceManagerImpl.java | 7 ++++++- .../secondarystorage/SecondaryStorageManagerImpl.java | 7 ++++++- systemvm/debian/opt/cloud/bin/setup/postinit.sh | 6 +++--- 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java index a62f4418684b..f79513094816 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java @@ -31,6 +31,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.compression.CompressionUtil; import com.cloud.vm.UserVmManager; import org.apache.cloudstack.api.command.user.loadbalancer.CreateLoadBalancerRuleCmd; import org.apache.cloudstack.config.ApiServiceConfiguration; @@ -495,7 +496,11 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + // Decode base64 user data, compress it, then re-encode to reduce command line length + String plainTextUserData = new String(Base64.getDecoder().decode(userData)); + CompressionUtil compressionUtil = new CompressionUtil(); + byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); + String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); buf.append(" userdata=").append(encodedUserData); } } catch (Exception e) { diff --git a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java index 6e7532325365..f64eaaea0426 100644 --- a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java +++ b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java @@ -37,6 +37,7 @@ import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; +import com.cloud.utils.compression.CompressionUtil; import com.cloud.vm.UserVmManager; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -260,7 +261,11 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + // Decode base64 user data, compress it, then re-encode to reduce command line length + String plainTextUserData = new String(Base64.getDecoder().decode(userData)); + CompressionUtil compressionUtil = new CompressionUtil(); + byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); + String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); buf.append(" userdata=").append(encodedUserData); } } catch (Exception e) { diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index 470a4f41e8c0..2645e15ed795 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -34,6 +34,7 @@ import javax.naming.ConfigurationException; import com.cloud.vm.UserVmManager; +import com.cloud.utils.compression.CompressionUtil; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.consoleproxy.ConsoleAccessManager; @@ -1280,8 +1281,12 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl try { Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); - if (org.apache.commons.lang3.StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + if (StringUtils.isNotBlank(userData)) { + // Decode base64 user data, compress it, then re-encode to reduce command line length + String plainTextUserData = new String(Base64.getDecoder().decode(userData)); + CompressionUtil compressionUtil = new CompressionUtil(); + byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); + String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); buf.append(" userdata=").append(encodedUserData); } } catch (Exception e) { diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index 22caf575531a..300cfdca7b73 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -49,6 +49,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.compression.CompressionUtil; import com.cloud.vm.UserVmManager; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; @@ -2112,7 +2113,11 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile profile.getTemplate()); String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + // Decode base64 user data, compress it, then re-encode to reduce command line length + String plainTextUserData = new String(Base64.getDecoder().decode(userData)); + CompressionUtil compressionUtil = new CompressionUtil(); + byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); + String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); buf.append(" userdata=").append(encodedUserData); } } catch (Exception e) { diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 501ddcc69851..0015e41b1e75 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -36,6 +36,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.compression.CompressionUtil; import com.cloud.vm.UserVmManager; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; @@ -1244,7 +1245,11 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl profile.getTemplate()); String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); if (StringUtils.isNotBlank(userData)) { - String encodedUserData = Base64.getEncoder().encodeToString(userData.getBytes()); + // Decode base64 user data, compress it, then re-encode to reduce command line length + String plainTextUserData = new String(Base64.getDecoder().decode(userData)); + CompressionUtil compressionUtil = new CompressionUtil(); + byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); + String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); buf.append(" userdata=").append(encodedUserData); } } catch (Exception e) { diff --git a/systemvm/debian/opt/cloud/bin/setup/postinit.sh b/systemvm/debian/opt/cloud/bin/setup/postinit.sh index ebeea6d0d2a7..1ad4de4c1a56 100755 --- a/systemvm/debian/opt/cloud/bin/setup/postinit.sh +++ b/systemvm/debian/opt/cloud/bin/setup/postinit.sh @@ -102,11 +102,11 @@ EOF # Set up user data files (reuse the function from init.sh) mkdir -p /var/lib/cloud/seed/nocloud - # Decode and potentially decompress user data + # Decode and decompress user data local decoded_userdata - decoded_userdata=$(echo "$encoded_userdata" | base64 -d 2>/dev/null) + 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 base64 user data" + log_it "ERROR: Failed to decode or decompress user data" return 1 fi From 44d976c8890adda79b60c451c43546b1b910d775 Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Mon, 22 Sep 2025 20:27:43 +0530 Subject: [PATCH 4/9] Don't use userdata in system VM's template --- .../cloudstack/userdata/UserDataManager.java | 3 +-- .../userdata/UserDataManagerImpl.java | 21 +++++++++---------- .../lb/ElasticLoadBalancerManagerImpl.java | 3 +-- .../lb/InternalLoadBalancerVMManagerImpl.java | 3 +-- .../consoleproxy/ConsoleProxyManagerImpl.java | 3 +-- .../VirtualNetworkApplianceManagerImpl.java | 4 +--- .../SecondaryStorageManagerImpl.java | 4 +--- 7 files changed, 16 insertions(+), 25 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 index 2dcb52fac1e3..00bf2296e74d 100644 --- a/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java +++ b/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java @@ -16,7 +16,6 @@ // under the License. package org.apache.cloudstack.userdata; -import com.cloud.template.VirtualMachineTemplate; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -30,5 +29,5 @@ public interface UserDataManager extends Manager, Configurable { String concatenateUserData(String userdata1, String userdata2, String userdataProvider); String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod); - Long validateAndGetUserDataIdForSystemVms(String userDataUuid, VirtualMachineTemplate vmTemplate); + String validateAndGetUserDataForSystemVM(String userDataUuid); } 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 index 1f81ec9e80e8..f632d96d1f91 100644 --- a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java +++ b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java @@ -23,7 +23,6 @@ import java.util.Map; import com.cloud.domain.Domain; -import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.User; import com.cloud.user.UserDataVO; import com.cloud.user.dao.UserDataDao; @@ -129,20 +128,20 @@ public String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) { } @Override - public Long validateAndGetUserDataIdForSystemVms(String userDataUuid, VirtualMachineTemplate vmTemplate) { - UserDataVO templateUserDataVo = vmTemplate.getUserDataId() != null ? userDataDao.findById(vmTemplate.getUserDataId()): null; - UserDataVO userDataVo = StringUtils.isNotBlank(userDataUuid) ? userDataDao.findByUuid(userDataUuid) : null; - if (isUserDataAllowedForSystemVm(templateUserDataVo) && - isUserDataAllowedForSystemVm(userDataVo)) { - return userDataVo != null ? userDataVo.getId() : null; + public String validateAndGetUserDataForSystemVM(String userDataUuid) { + 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) { + return userDataVo.getUserData(); } throw new CloudRuntimeException("User data can only be used by system VMs if it belongs to the ROOT domain and ADMIN account."); } - private boolean isUserDataAllowedForSystemVm(UserDataVO userData) { - return userData == null || (userData.getDomainId() == Domain.ROOT_DOMAIN && userData.getAccountId() == User.UID_ADMIN); - } - private byte[] validateAndDecodeByHTTPMethod(String userData, int maxHTTPLength, BaseCmd.HTTPMethod httpMethod) { byte[] decodedUserData = Base64.decodeBase64(userData.getBytes()); if (decodedUserData == null || decodedUserData.length < 1) { diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java index f79513094816..0c0bebfd152d 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java @@ -493,8 +493,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl if (SystemVmEnableUserData.valueIn(dc.getId())) { String userDataUuid = RouterUserData.valueIn(dc.getId()); try { - Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); - String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { // Decode base64 user data, compress it, then re-encode to reduce command line length String plainTextUserData = new String(Base64.getDecoder().decode(userData)); diff --git a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java index f64eaaea0426..7eb02b976935 100644 --- a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java +++ b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java @@ -258,8 +258,7 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile if (SystemVmEnableUserData.valueIn(dcId)) { String userDataUuid = RouterUserData.valueIn(dcId); try { - Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); - String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { // Decode base64 user data, compress it, then re-encode to reduce command line length String plainTextUserData = new String(Base64.getDecoder().decode(userData)); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index 2645e15ed795..000c159ca6eb 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -1279,8 +1279,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl if (SystemVmEnableUserData.valueIn(dc.getId())) { String userDataUuid = ConsoleProxyUserData.valueIn(dc.getId()); try { - Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, profile.getTemplate()); - String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { // Decode base64 user data, compress it, then re-encode to reduce command line length String plainTextUserData = new String(Base64.getDecoder().decode(userData)); diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index 300cfdca7b73..c9d3c3d2c97c 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -2109,9 +2109,7 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile if (SystemVmEnableUserData.valueIn(router.getDataCenterId())) { String userDataUuid = RouterUserData.valueIn(dc.getId()); try { - Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, - profile.getTemplate()); - String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { // Decode base64 user data, compress it, then re-encode to reduce command line length String plainTextUserData = new String(Base64.getDecoder().decode(userData)); diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 0015e41b1e75..ccd7e707280d 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -1241,9 +1241,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl if (SystemVmEnableUserData.valueIn(dc.getId())) { String userDataUuid = SecondaryStorageUserData.valueIn(dc.getId()); try { - Long userDataId = userDataManager.validateAndGetUserDataIdForSystemVms(userDataUuid, - profile.getTemplate()); - String userData = userVmManager.finalizeUserData(null, userDataId, profile.getTemplate()); + String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { // Decode base64 user data, compress it, then re-encode to reduce command line length String plainTextUserData = new String(Base64.getDecoder().decode(userData)); From 71561387f985e6b2e2949aa4488853a5c72a7314 Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Mon, 22 Sep 2025 20:49:07 +0530 Subject: [PATCH 5/9] Fix test failures --- .../cloud/network/lb/ElasticLoadBalancerManagerImpl.java | 3 --- .../network/lb/InternalLoadBalancerVMManagerImpl.java | 3 --- .../internallbvmmgr/LbChildTestConfiguration.java | 6 ++++++ .../com/cloud/consoleproxy/ConsoleProxyManagerImpl.java | 3 --- .../network/router/VirtualNetworkApplianceManagerImpl.java | 4 ---- .../secondarystorage/SecondaryStorageManagerImpl.java | 3 --- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java index 0c0bebfd152d..08e79ba89dda 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java @@ -32,7 +32,6 @@ import javax.naming.ConfigurationException; import com.cloud.utils.compression.CompressionUtil; -import com.cloud.vm.UserVmManager; import org.apache.cloudstack.api.command.user.loadbalancer.CreateLoadBalancerRuleCmd; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -146,8 +145,6 @@ public class ElasticLoadBalancerManagerImpl extends ManagerBase implements Elast private NicDao _nicDao; @Inject private UserDataManager userDataManager; - @Inject - private UserVmManager userVmManager; String _instance; diff --git a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java index 7eb02b976935..99ce68008d60 100644 --- a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java +++ b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java @@ -38,7 +38,6 @@ import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; import com.cloud.utils.compression.CompressionUtil; -import com.cloud.vm.UserVmManager; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -184,8 +183,6 @@ public class InternalLoadBalancerVMManagerImpl extends ManagerBase implements In UserDao _userDao; @Inject private UserDataManager userDataManager; - @Inject - private UserVmManager userVmManager; @Override public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile, final DeployDestination dest, final ReservationContext context) { diff --git a/plugins/network-elements/internal-loadbalancer/src/test/java/org/apache/cloudstack/internallbvmmgr/LbChildTestConfiguration.java b/plugins/network-elements/internal-loadbalancer/src/test/java/org/apache/cloudstack/internallbvmmgr/LbChildTestConfiguration.java index 6af10b51936c..23c914bc7b00 100644 --- a/plugins/network-elements/internal-loadbalancer/src/test/java/org/apache/cloudstack/internallbvmmgr/LbChildTestConfiguration.java +++ b/plugins/network-elements/internal-loadbalancer/src/test/java/org/apache/cloudstack/internallbvmmgr/LbChildTestConfiguration.java @@ -18,6 +18,7 @@ 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 AccountDao accountDao() { 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(); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index 000c159ca6eb..422fb5aa9c7b 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -33,7 +33,6 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.vm.UserVmManager; import com.cloud.utils.compression.CompressionUtil; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; @@ -235,8 +234,6 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy private NetworkOrchestrationService networkMgr; @Inject private UserDataManager userDataManager; - @Inject - private UserVmManager userVmManager; private ConsoleProxyListener consoleProxyListener; diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index c9d3c3d2c97c..b8c955cd6e3d 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -50,7 +50,6 @@ import javax.naming.ConfigurationException; import com.cloud.utils.compression.CompressionUtil; -import com.cloud.vm.UserVmManager; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; @@ -359,9 +358,6 @@ public class VirtualNetworkApplianceManagerImpl extends ManagerBase implements V @Inject private UserDataManager userDataManager; - @Inject - private UserVmManager userVmManager; - private int _routerStatsInterval = 300; private int _routerCheckInterval = 30; private int _rvrStatusUpdatePoolSize = 10; diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index ccd7e707280d..94ed080151b9 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -37,7 +37,6 @@ import javax.naming.ConfigurationException; import com.cloud.utils.compression.CompressionUtil; -import com.cloud.vm.UserVmManager; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.context.CallContext; @@ -262,8 +261,6 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar private CAManager caManager; @Inject private UserDataManager userDataManager; - @Inject - private UserVmManager userVmManager; private int _secStorageVmMtuSize; From 2a4a2819d094053260da1712c9ff29200a1f1386 Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Tue, 23 Sep 2025 14:09:19 +0530 Subject: [PATCH 6/9] minor code refactoring --- .../apache/cloudstack/userdata/UserDataManager.java | 13 ++++++++++++- .../cloudstack/userdata/UserDataManagerImpl.java | 10 ++++++++-- .../network/lb/ElasticLoadBalancerManagerImpl.java | 9 +-------- .../lb/InternalLoadBalancerVMManagerImpl.java | 9 +-------- .../cloud/consoleproxy/ConsoleProxyManagerImpl.java | 9 +-------- .../router/VirtualNetworkApplianceManagerImpl.java | 9 +-------- .../SecondaryStorageManagerImpl.java | 9 +-------- 7 files changed, 25 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 index 00bf2296e74d..7e718413118e 100644 --- a/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java +++ b/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java @@ -22,6 +22,8 @@ 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 VM_USERDATA_MAX_LENGTH = new ConfigKey<>("Advanced", Integer.class, VM_USERDATA_MAX_LENGTH_STRING, "32768", @@ -29,5 +31,14 @@ public interface UserDataManager extends Manager, Configurable { String concatenateUserData(String userdata1, String userdata2, String userdataProvider); String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod); - String validateAndGetUserDataForSystemVM(String userDataUuid); + + /** + * 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; } 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 index f632d96d1f91..7c5692564c99 100644 --- a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java +++ b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.userdata; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.HashMap; @@ -26,6 +27,7 @@ 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; @@ -128,7 +130,7 @@ public String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) { } @Override - public String validateAndGetUserDataForSystemVM(String userDataUuid) { + public String validateAndGetUserDataForSystemVM(String userDataUuid) throws IOException { if (StringUtils.isBlank(userDataUuid)) { return null; } @@ -137,7 +139,11 @@ public String validateAndGetUserDataForSystemVM(String userDataUuid) { return null; } if (userDataVo.getDomainId() == Domain.ROOT_DOMAIN && userDataVo.getAccountId() == User.UID_ADMIN) { - return userDataVo.getUserData(); + // 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."); } diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java index 08e79ba89dda..cb4f1ff7481f 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java @@ -17,7 +17,6 @@ package com.cloud.network.lb; import java.util.ArrayList; -import java.util.Base64; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -31,7 +30,6 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.utils.compression.CompressionUtil; import org.apache.cloudstack.api.command.user.loadbalancer.CreateLoadBalancerRuleCmd; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -492,12 +490,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl try { String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { - // Decode base64 user data, compress it, then re-encode to reduce command line length - String plainTextUserData = new String(Base64.getDecoder().decode(userData)); - CompressionUtil compressionUtil = new CompressionUtil(); - byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); - String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); - buf.append(" userdata=").append(encodedUserData); + buf.append(" userdata=").append(userData); } } catch (Exception e) { logger.warn("Failed to load user data for the elastic lb vm, ignored", e); diff --git a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java index 99ce68008d60..41a9aad083e9 100644 --- a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java +++ b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java @@ -26,7 +26,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -37,7 +36,6 @@ import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; -import com.cloud.utils.compression.CompressionUtil; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -257,12 +255,7 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile try { String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { - // Decode base64 user data, compress it, then re-encode to reduce command line length - String plainTextUserData = new String(Base64.getDecoder().decode(userData)); - CompressionUtil compressionUtil = new CompressionUtil(); - byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); - String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); - buf.append(" userdata=").append(encodedUserData); + buf.append(" userdata=").append(userData); } } catch (Exception e) { logger.warn("Failed to load user data for the internal lb vm, ignored", e); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index 422fb5aa9c7b..bd48e16fcfa5 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -19,7 +19,6 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -33,7 +32,6 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.utils.compression.CompressionUtil; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.consoleproxy.ConsoleAccessManager; @@ -1278,12 +1276,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl try { String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { - // Decode base64 user data, compress it, then re-encode to reduce command line length - String plainTextUserData = new String(Base64.getDecoder().decode(userData)); - CompressionUtil compressionUtil = new CompressionUtil(); - byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); - String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); - buf.append(" userdata=").append(encodedUserData); + buf.append(" userdata=").append(userData); } } catch (Exception e) { logger.warn("Failed to load user data for the cpvm, ignored", e); diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index b8c955cd6e3d..d7deca40e915 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -28,7 +28,6 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Base64; import java.util.Calendar; import java.util.Collections; import java.util.Date; @@ -49,7 +48,6 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.utils.compression.CompressionUtil; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; @@ -2107,12 +2105,7 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile try { String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { - // Decode base64 user data, compress it, then re-encode to reduce command line length - String plainTextUserData = new String(Base64.getDecoder().decode(userData)); - CompressionUtil compressionUtil = new CompressionUtil(); - byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); - String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); - buf.append(" userdata=").append(encodedUserData); + buf.append(" userdata=").append(userData); } } catch (Exception e) { logger.warn("Failed to load user data for the virtual router, ignored", e); diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 94ed080151b9..c577e66cf874 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -23,7 +23,6 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -36,7 +35,6 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.utils.compression.CompressionUtil; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.context.CallContext; @@ -1240,12 +1238,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl try { String userData = userDataManager.validateAndGetUserDataForSystemVM(userDataUuid); if (StringUtils.isNotBlank(userData)) { - // Decode base64 user data, compress it, then re-encode to reduce command line length - String plainTextUserData = new String(Base64.getDecoder().decode(userData)); - CompressionUtil compressionUtil = new CompressionUtil(); - byte[] compressedUserData = compressionUtil.compressString(plainTextUserData); - String encodedUserData = Base64.getEncoder().encodeToString(compressedUserData); - buf.append(" userdata=").append(encodedUserData); + buf.append(" userdata=").append(userData); } } catch (Exception e) { logger.warn("Failed to load user data for the ssvm, ignored", e); From 2cc17725b31b640a1731028a8b7c7bf8063d60e1 Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Fri, 26 Sep 2025 11:55:51 +0530 Subject: [PATCH 7/9] Add unit tests --- .../userdata/UserDataManagerImplTest.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/engine/userdata/src/test/java/org/apache/cloudstack/userdata/UserDataManagerImplTest.java b/engine/userdata/src/test/java/org/apache/cloudstack/userdata/UserDataManagerImplTest.java index 67e7b38e37d0..70a56bc0c61f 100644 --- a/engine/userdata/src/test/java/org/apache/cloudstack/userdata/UserDataManagerImplTest.java +++ b/engine/userdata/src/test/java/org/apache/cloudstack/userdata/UserDataManagerImplTest.java @@ -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 void testValidateUrlEncodedBase64() { 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); + } + } From 219d4bf54b62806de8045340fb81fdad46a731ad Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Sun, 28 Sep 2025 20:13:18 +0530 Subject: [PATCH 8/9] Add smoke test for ssvm --- test/integration/smoke/test_ssvm.py | 145 +++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 3 deletions(-) diff --git a/test/integration/smoke/test_ssvm.py b/test/integration/smoke/test_ssvm.py index 0784bc3820c6..06b076fee307 100644 --- a/test/integration/smoke/test_ssvm.py +++ b/test/integration/smoke/test_ssvm.py @@ -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 nose.plugins.attrib import attr import telnetlib import logging +import base64 +import os +import urllib +import zipfile # Import System modules import time @@ -1399,3 +1402,139 @@ def test_13_ss_nfs_version_on_ssvm(self): int(nfs_version), "Check mounted NFS version to be the same as provided" ) + + @attr( + tags=[ + "advanced", + "advancedns", + "smoke", + "basic", "vj", + "sg"], + required_hardware="true") + def test_14_userdata_on_ssvm(self): + """Test user data functionality on SSVM + """ + # 1) Register userdata for the default admin user + # 2) Update global settings to enable userdata and add userdata for SSVM + # 3) Delete the SSVM if already present and wait for it to come up again + # 4) Wait for the SSVM to be ready + # 5) Download the file created by userdata script using the getDiagnosticsData command + # 6) Verify the file contains the expected content + + userdata_script = """#!/bin/bash +echo "User data script ran successfully" > /tmp/userdata.txt +""" + userdata_file_path = "/tmp/userdata.txt" + expected_userdata_content = "User data script ran successfully" + b64_encoded_userdata = base64.b64encode(userdata_script.encode('utf-8')).decode('utf-8') + # Create a userdata entry + try: + self.userdata = UserData.register( + self.apiclient, + name="ssvm_userdata", + userdata=b64_encoded_userdata + ) + except Exception as e: + if "already exists" in str(e): + self.debug("Userdata already exists, getting it") + self.userdata = UserData.list(self.apiclient, name="ssvm_userdata", listall=True)[0] + else: + self.fail("Failed to register userdata: %s" % e) + + #Enable user data and set the script to be run on SSVM + cmd = updateConfiguration.updateConfigurationCmd() + cmd.name = "systemvm.userdata.enabled" + cmd.value = "true" + self.apiclient.updateConfiguration(cmd) + + cmd = updateConfiguration.updateConfigurationCmd() + cmd.name = "secstorage.userdata" + cmd.value = self.userdata.id + self.apiclient.updateConfiguration(cmd) + + list_ssvm_response = list_ssvms(self.apiclient, systemvmtype='secondarystoragevm', 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 SSVM: %s" % ssvm.id) + cmd = destroySystemVm.destroySystemVmCmd() + cmd.id = ssvm.id + self.apiclient.destroySystemVm(cmd) + + ssvm_response = self.checkForRunningSystemVM(ssvm, 'secondarystoragevm') + self.debug("SSVM state after debug: %s" % ssvm_response.state) + self.assertEqual( + ssvm_response.state, + 'Running', + "Check whether SSVM is running or not" + ) + # Wait for the agent to be up + self.waitForSystemVMAgent(ssvm_response.name) + + # 5) Download the file created by userdata script using the getDiagnosticsData command + cmd = getDiagnosticsData.getDiagnosticsDataCmd() + cmd.targetid = ssvm_response.id + cmd.files = userdata_file_path + + # getDiagnosticsData command takes some time to work after a SSVM is started + retries = 4 + 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 /tmp/ directory and extract it + self.debug("Downloading userdata file from SSVM") + try: + urllib.request.urlretrieve( + response.url, + "/tmp/userdata.zip" + ) + except Exception as e: + self.fail("Failed to download userdata file from SSVM: %s" % e) + self.debug("Downloaded userdata file from SSVM: %s" % + "/tmp/userdata.zip") + try: + with zipfile.ZipFile("/tmp/userdata.zip", 'r') as zip_ref: + zip_ref.extractall("/tmp/") + 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" % "/tmp/userdata.txt") + + # 7) Verify the file contains the expected content + try: + with open("/tmp/userdata.txt", 'r') as f: + content = f.read().strip() + self.debug("Userdata file content: %s" % content) + self.assertEqual( + expected_userdata_content in content, + True, + f"Check that userdata file contains expected content: '{expected_userdata_content}'" + ) + except FileNotFoundError: + self.fail("Userdata file not found in extracted zip") + except Exception as e: + self.fail("Failed to read userdata file: %s" % e) + finally: + # Clean up temporary files + try: + if os.path.exists("/tmp/userdata.zip"): + os.remove("/tmp/userdata.zip") + if os.path.exists("/tmp/userdata.txt"): + os.remove("/tmp/userdata.txt") + except Exception as e: + self.debug("Failed to clean up temporary files: %s" % e) From d769a9b26ccff7485632951c436d785b907af157 Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Tue, 30 Sep 2025 16:01:13 +0530 Subject: [PATCH 9/9] Add tests userdata in cpvm & vr --- test/integration/smoke/test_ssvm.py | 136 ------ .../smoke/test_systemvm_userdata.py | 410 ++++++++++++++++++ 2 files changed, 410 insertions(+), 136 deletions(-) create mode 100644 test/integration/smoke/test_systemvm_userdata.py diff --git a/test/integration/smoke/test_ssvm.py b/test/integration/smoke/test_ssvm.py index 06b076fee307..798401f68e1f 100644 --- a/test/integration/smoke/test_ssvm.py +++ b/test/integration/smoke/test_ssvm.py @@ -1402,139 +1402,3 @@ def test_13_ss_nfs_version_on_ssvm(self): int(nfs_version), "Check mounted NFS version to be the same as provided" ) - - @attr( - tags=[ - "advanced", - "advancedns", - "smoke", - "basic", "vj", - "sg"], - required_hardware="true") - def test_14_userdata_on_ssvm(self): - """Test user data functionality on SSVM - """ - # 1) Register userdata for the default admin user - # 2) Update global settings to enable userdata and add userdata for SSVM - # 3) Delete the SSVM if already present and wait for it to come up again - # 4) Wait for the SSVM to be ready - # 5) Download the file created by userdata script using the getDiagnosticsData command - # 6) Verify the file contains the expected content - - userdata_script = """#!/bin/bash -echo "User data script ran successfully" > /tmp/userdata.txt -""" - userdata_file_path = "/tmp/userdata.txt" - expected_userdata_content = "User data script ran successfully" - b64_encoded_userdata = base64.b64encode(userdata_script.encode('utf-8')).decode('utf-8') - # Create a userdata entry - try: - self.userdata = UserData.register( - self.apiclient, - name="ssvm_userdata", - userdata=b64_encoded_userdata - ) - except Exception as e: - if "already exists" in str(e): - self.debug("Userdata already exists, getting it") - self.userdata = UserData.list(self.apiclient, name="ssvm_userdata", listall=True)[0] - else: - self.fail("Failed to register userdata: %s" % e) - - #Enable user data and set the script to be run on SSVM - cmd = updateConfiguration.updateConfigurationCmd() - cmd.name = "systemvm.userdata.enabled" - cmd.value = "true" - self.apiclient.updateConfiguration(cmd) - - cmd = updateConfiguration.updateConfigurationCmd() - cmd.name = "secstorage.userdata" - cmd.value = self.userdata.id - self.apiclient.updateConfiguration(cmd) - - list_ssvm_response = list_ssvms(self.apiclient, systemvmtype='secondarystoragevm', 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 SSVM: %s" % ssvm.id) - cmd = destroySystemVm.destroySystemVmCmd() - cmd.id = ssvm.id - self.apiclient.destroySystemVm(cmd) - - ssvm_response = self.checkForRunningSystemVM(ssvm, 'secondarystoragevm') - self.debug("SSVM state after debug: %s" % ssvm_response.state) - self.assertEqual( - ssvm_response.state, - 'Running', - "Check whether SSVM is running or not" - ) - # Wait for the agent to be up - self.waitForSystemVMAgent(ssvm_response.name) - - # 5) Download the file created by userdata script using the getDiagnosticsData command - cmd = getDiagnosticsData.getDiagnosticsDataCmd() - cmd.targetid = ssvm_response.id - cmd.files = userdata_file_path - - # getDiagnosticsData command takes some time to work after a SSVM is started - retries = 4 - 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 /tmp/ directory and extract it - self.debug("Downloading userdata file from SSVM") - try: - urllib.request.urlretrieve( - response.url, - "/tmp/userdata.zip" - ) - except Exception as e: - self.fail("Failed to download userdata file from SSVM: %s" % e) - self.debug("Downloaded userdata file from SSVM: %s" % - "/tmp/userdata.zip") - try: - with zipfile.ZipFile("/tmp/userdata.zip", 'r') as zip_ref: - zip_ref.extractall("/tmp/") - 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" % "/tmp/userdata.txt") - - # 7) Verify the file contains the expected content - try: - with open("/tmp/userdata.txt", 'r') as f: - content = f.read().strip() - self.debug("Userdata file content: %s" % content) - self.assertEqual( - expected_userdata_content in content, - True, - f"Check that userdata file contains expected content: '{expected_userdata_content}'" - ) - except FileNotFoundError: - self.fail("Userdata file not found in extracted zip") - except Exception as e: - self.fail("Failed to read userdata file: %s" % e) - finally: - # Clean up temporary files - try: - if os.path.exists("/tmp/userdata.zip"): - os.remove("/tmp/userdata.zip") - if os.path.exists("/tmp/userdata.txt"): - os.remove("/tmp/userdata.txt") - except Exception as e: - self.debug("Failed to clean up temporary files: %s" % e) diff --git a/test/integration/smoke/test_systemvm_userdata.py b/test/integration/smoke/test_systemvm_userdata.py new file mode 100644 index 000000000000..4b04915d2f9d --- /dev/null +++ b/test/integration/smoke/test_systemvm_userdata.py @@ -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.userdata', 'cpvm.userdata', '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.userdata' or 'cpvm.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.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="consoleproxy.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", "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)