This is an automated email from the ASF dual-hosted git repository.

harikrishna pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new d2615bb142c Add support for providing userdata to system VMs (#11654)
d2615bb142c is described below

commit d2615bb142c374dffad44c5cb1f833caf6bbb34c
Author: Vishesh <[email protected]>
AuthorDate: Wed Oct 8 10:44:26 2025 +0530

    Add support for providing userdata to system VMs (#11654)
    
    This PR adds support for specifying user data (cloud-init) for system VMs 
via Zone Scoped global settings. This allows the operators to customize the 
System VMs and setup monitoring, logging or execute any custom commands.
    
    We set the user data from the global setting in /var/cache/cloud/cmdline, 
and use the NoCloud datasource to process user data. cloud-init service is 
still disabled in the system VMs and it's executed as part of the 
cloud-postinit service which executes the postinit.sh script.
    
    Added global settings:
    systemvm.userdata.enabled - Disabled by default. Needs to be enabled to 
utilize the feature.
    console.proxy.vm.userdata - UUID of the User data to be used for Console 
Proxy
    secstorage.vm.userdata - UUID of the User data to be used for Secondary 
Storage VM
    virtual.router.userdata - UUID of the User data to be used for Virtual 
Routers
---
 .../cloudstack/userdata/UserDataManager.java       |  12 +
 .../java/com/cloud/vm/VirtualMachineManager.java   |   3 +
 .../com/cloud/vm/VirtualMachineManagerImpl.java    |   2 +-
 .../cloudstack/userdata/UserDataManagerImpl.java   |  30 ++
 .../userdata/UserDataManagerImplTest.java          |  90 +++++
 .../cloudstack/framework/config/ConfigKey.java     |  58 +++
 .../network/lb/ElasticLoadBalancerManagerImpl.java |  20 +
 .../lb/InternalLoadBalancerVMManagerImpl.java      |  19 +
 .../internallbvmmgr/LbChildTestConfiguration.java  |   6 +
 .../configuration/ConfigurationManagerImpl.java    |   8 +
 .../cloud/consoleproxy/ConsoleProxyManager.java    |   6 +
 .../consoleproxy/ConsoleProxyManagerImpl.java      |  25 +-
 .../router/VirtualNetworkApplianceManager.java     |   6 +
 .../router/VirtualNetworkApplianceManagerImpl.java |  19 +-
 .../secondary/SecondaryStorageVmManager.java       |   7 +
 .../SecondaryStorageManagerImpl.java               |  21 +-
 systemvm/debian/opt/cloud/bin/setup/init.sh        |   2 +
 systemvm/debian/opt/cloud/bin/setup/postinit.sh    |  95 +++++
 test/integration/smoke/test_ssvm.py                |   9 +-
 test/integration/smoke/test_systemvm_userdata.py   | 410 +++++++++++++++++++++
 .../scripts/configure_systemvm_services.sh         |   2 +-
 21 files changed, 840 insertions(+), 10 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 b4bede24890..7e718413118 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 org.apache.cloudstack.framework.config.Configurable;
 
 import com.cloud.utils.component.Manager;
 
+import java.io.IOException;
+
 public interface UserDataManager extends Manager, Configurable {
     String VM_USERDATA_MAX_LENGTH_STRING = "vm.userdata.max.length";
     ConfigKey<Integer> VM_USERDATA_MAX_LENGTH = new ConfigKey<>("Advanced", 
Integer.class, VM_USERDATA_MAX_LENGTH_STRING, "32768",
@@ -29,4 +31,14 @@ public interface UserDataManager extends Manager, 
Configurable {
 
     String concatenateUserData(String userdata1, String userdata2, String 
userdataProvider);
     String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod);
+
+    /**
+     * This method validates the user data uuid for system VMs and returns the 
user data
+     * after compression and base64 encoding for the system VM to consume.
+     *
+     * @param userDataUuid
+     * @return a String containing the user data after compression and base64 
encoding
+     * @throws IOException
+     */
+    String validateAndGetUserDataForSystemVM(String userDataUuid) throws 
IOException;
 }
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 cffba3d9a13..70240454689 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<Boolean> VmSyncPowerStateTransitioning = new 
ConfigKey<>("Advanced", Boolean.class, "vm.sync.power.state.transitioning", 
"true",
             "Whether to sync power states of the transitioning and stalled VMs 
while processing VM power reports.", false);
 
+    ConfigKey<Boolean> SystemVmEnableUserData = new ConfigKey<>(Boolean.class, 
"systemvm.userdata.enabled", "Advanced", "false",
+            "Enable user data for system VMs. When enabled, the CPVM, SSVM, 
and Router system VMs will use the values from the global settings 
console.proxy.vm.userdata, secstorage.vm.userdata, and virtual.router.userdata, 
respectively, to provide cloud-init user data to the VM.",
+            true, ConfigKey.Scope.Zone, null);
 
     interface Topics {
         String VM_POWER_STATE = "vm.powerstate";
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 7c5d43fea97..a8a92d6b3db 100755
--- 
a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
+++ 
b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
@@ -5244,7 +5244,7 @@ public class VirtualMachineManagerImpl extends 
ManagerBase implements VirtualMac
                 VmConfigDriveLabel, VmConfigDriveOnPrimaryPool, 
VmConfigDriveForceHostCacheUse, VmConfigDriveUseHostCacheOnUnsupportedPool,
                 HaVmRestartHostUp, ResourceCountRunningVMsonly, 
AllowExposeHypervisorHostname, AllowExposeHypervisorHostnameAccountLevel, 
SystemVmRootDiskSize,
                 AllowExposeDomainInMetadata, MetadataCustomCloudName, 
VmMetadataManufacturer, VmMetadataProductName,
-                VmSyncPowerStateTransitioning
+                VmSyncPowerStateTransitioning, SystemVmEnableUserData
         };
     }
 
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 664d308e28d..7c5692564c9 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,12 +16,18 @@
 // under the License.
 package org.apache.cloudstack.userdata;
 
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import com.cloud.domain.Domain;
+import com.cloud.user.User;
+import com.cloud.user.UserDataVO;
+import com.cloud.user.dao.UserDataDao;
+import com.cloud.utils.compression.CompressionUtil;
 import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.framework.config.ConfigKey;
 import org.apache.commons.codec.binary.Base64;
@@ -31,7 +37,12 @@ import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.utils.component.ManagerBase;
 import com.cloud.utils.exception.CloudRuntimeException;
 
+import javax.inject.Inject;
+
 public class UserDataManagerImpl extends ManagerBase implements 
UserDataManager {
+    @Inject
+    UserDataDao userDataDao;
+
     private static final int MAX_USER_DATA_LENGTH_BYTES = 2048;
     private static final int MAX_HTTP_GET_LENGTH = 2 * 
MAX_USER_DATA_LENGTH_BYTES; // 4KB
     private static final int NUM_OF_2K_BLOCKS = 512;
@@ -118,6 +129,25 @@ public class UserDataManagerImpl extends ManagerBase 
implements UserDataManager
         return Base64.encodeBase64String(decodedUserData);
     }
 
+    @Override
+    public String validateAndGetUserDataForSystemVM(String userDataUuid) 
throws IOException {
+        if (StringUtils.isBlank(userDataUuid)) {
+            return null;
+        }
+        UserDataVO userDataVo = userDataDao.findByUuid(userDataUuid);
+        if (userDataVo == null) {
+            return null;
+        }
+        if (userDataVo.getDomainId() == Domain.ROOT_DOMAIN && 
userDataVo.getAccountId() == User.UID_ADMIN) {
+            // Decode base64 user data, compress it, then re-encode to reduce 
command line length
+            String plainTextUserData = new 
String(java.util.Base64.getDecoder().decode(userDataVo.getUserData()));
+            CompressionUtil compressionUtil = new CompressionUtil();
+            byte[] compressedUserData = 
compressionUtil.compressString(plainTextUserData);
+            return 
java.util.Base64.getEncoder().encodeToString(compressedUserData);
+        }
+        throw new CloudRuntimeException("User data can only be used by system 
VMs if it belongs to the ROOT domain and ADMIN account.");
+    }
+
     private byte[] validateAndDecodeByHTTPMethod(String userData, int 
maxHTTPLength, BaseCmd.HTTPMethod httpMethod) {
         byte[] decodedUserData = Base64.decodeBase64(userData.getBytes());
         if (decodedUserData == null || decodedUserData.length < 1) {
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 67e7b38e37d..70a56bc0c61 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 class UserDataManagerImplTest {
         assertEquals("validate return the value with padding", 
encodedUserdata, userDataManager.validateUserData(urlEncodedUserdata, 
BaseCmd.HTTPMethod.GET));
     }
 
+    @Test
+    public void testValidateAndGetUserDataForSystemVMWithBlankUuid() throws 
IOException {
+        // Test with blank UUID should return null
+        assertNull("null UUID should return null", 
userDataManager.validateAndGetUserDataForSystemVM(null));
+        assertNull("blank UUID should return null", 
userDataManager.validateAndGetUserDataForSystemVM(""));
+        assertNull("blank UUID should return null", 
userDataManager.validateAndGetUserDataForSystemVM("   "));
+    }
+
+    @Test
+    public void testValidateAndGetUserDataForSystemVMNotFound() throws 
IOException {
+        // Test when userDataVo is not found
+        String testUuid = "test-uuid-123";
+        when(userDataDao.findByUuid(testUuid)).thenReturn(null);
+
+        assertNull("userdata not found should return null", 
userDataManager.validateAndGetUserDataForSystemVM(testUuid));
+    }
+
+    @Test(expected = CloudRuntimeException.class)
+    public void testValidateAndGetUserDataForSystemVMInvalidDomain() throws 
IOException {
+        // Test with userDataVo that doesn't belong to ROOT domain
+        String testUuid = "test-uuid-123";
+        UserDataVO userDataVo = Mockito.mock(UserDataVO.class);
+        when(userDataVo.getDomainId()).thenReturn(2L); // Not ROOT domain
+
+        when(userDataDao.findByUuid(testUuid)).thenReturn(userDataVo);
+        userDataManager.validateAndGetUserDataForSystemVM(testUuid);
+    }
+
+    @Test(expected = CloudRuntimeException.class)
+    public void testValidateAndGetUserDataForSystemVMInvalidAccount() throws 
IOException {
+        // Test with userDataVo that doesn't belong to ADMIN account
+        String testUuid = "test-uuid-123";
+        UserDataVO userDataVo = Mockito.mock(UserDataVO.class);
+        when(userDataVo.getDomainId()).thenReturn(Domain.ROOT_DOMAIN);
+        when(userDataVo.getAccountId()).thenReturn(3L);
+        userDataVo.setUserData("dGVzdCBkYXRh"); // "test data" in base64
+
+        when(userDataDao.findByUuid(testUuid)).thenReturn(userDataVo);
+        userDataManager.validateAndGetUserDataForSystemVM(testUuid);
+    }
+
+    @Test
+    public void testValidateAndGetUserDataForSystemVMValidSystemVMUserData() 
throws IOException {
+        // Test with valid system VM userdata (ROOT domain + ADMIN account)
+        String testUuid = "test-uuid-123";
+        String originalText = "#!/bin/bash\necho 'Hello World'";
+        String base64EncodedUserData = 
Base64.getEncoder().encodeToString(originalText.getBytes());
+
+        UserDataVO userDataVo = Mockito.mock(UserDataVO.class);
+        when(userDataVo.getDomainId()).thenReturn(Domain.ROOT_DOMAIN);
+        when(userDataVo.getAccountId()).thenReturn(User.UID_ADMIN);
+        when(userDataVo.getUserData()).thenReturn(base64EncodedUserData);
+
+        when(userDataDao.findByUuid(testUuid)).thenReturn(userDataVo);
+
+        String result = 
userDataManager.validateAndGetUserDataForSystemVM(testUuid);
+
+        // Verify result is not null and is base64 encoded
+        assertNotNull("result should not be null", result);
+        assertFalse("result should be base64 encoded", result.isEmpty());
+
+        // Verify the result is valid base64
+        try {
+            Base64.getDecoder().decode(result);
+        } catch (IllegalArgumentException e) {
+            throw new AssertionError("Result should be valid base64", e);
+        }
+
+        // The result should be different from input since it's compressed
+        assertNotEquals("compressed result should be different from original", 
result, base64EncodedUserData);
+    }
+
 }
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 88eca1d28de..0ea910fcb6d 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
@@ -43,6 +43,64 @@ public class ConfigKey<T> {
     public static final String CATEGORY_NETWORK = "Network";
     public static final String CATEGORY_SYSTEM = "System";
 
+    // Configuration Groups to be used to define group for a config key
+    // Group name, description, precedence
+    public static final Ternary<String, String, Long> GROUP_MISCELLANEOUS = 
new Ternary<>("Miscellaneous", "Miscellaneous configuration", 999L);
+    public static final Ternary<String, String, Long> GROUP_ACCESS = new 
Ternary<>("Access", "Identity and Access management configuration", 1L);
+    public static final Ternary<String, String, Long> GROUP_COMPUTE = new 
Ternary<>("Compute", "Compute configuration", 2L);
+    public static final Ternary<String, String, Long> GROUP_STORAGE = new 
Ternary<>("Storage", "Storage configuration", 3L);
+    public static final Ternary<String, String, Long> GROUP_NETWORK = new 
Ternary<>("Network", "Network configuration", 4L);
+    public static final Ternary<String, String, Long> GROUP_HYPERVISOR = new 
Ternary<>("Hypervisor", "Hypervisor specific configuration", 5L);
+    public static final Ternary<String, String, Long> GROUP_MANAGEMENT_SERVER 
= new Ternary<>("Management Server", "Management Server configuration", 6L);
+    public static final Ternary<String, String, Long> GROUP_SYSTEM_VMS = new 
Ternary<>("System VMs", "System VMs related configuration", 7L);
+    public static final Ternary<String, String, Long> GROUP_INFRASTRUCTURE = 
new Ternary<>("Infrastructure", "Infrastructure configuration", 8L);
+    public static final Ternary<String, String, Long> GROUP_USAGE_SERVER = new 
Ternary<>("Usage Server", "Usage Server related configuration", 9L);
+
+    // Configuration Subgroups to be used to define subgroup for a config key
+    // Subgroup name, description, precedence
+    public static final Pair<String, Long> SUBGROUP_OTHERS = new 
Pair<>("Others", 999L);
+    public static final Pair<String, Long> SUBGROUP_ACCOUNT = new 
Pair<>("Account", 1L);
+    public static final Pair<String, Long> SUBGROUP_DOMAIN = new 
Pair<>("Domain", 2L);
+    public static final Pair<String, Long> SUBGROUP_PROJECT = new 
Pair<>("Project", 3L);
+    public static final Pair<String, Long> SUBGROUP_LDAP = new Pair<>("LDAP", 
4L);
+    public static final Pair<String, Long> SUBGROUP_SAML = new Pair<>("SAML", 
5L);
+    public static final Pair<String, Long> SUBGROUP_VIRTUAL_MACHINE = new 
Pair<>("Virtual Machine", 1L);
+    public static final Pair<String, Long> SUBGROUP_KUBERNETES = new 
Pair<>("Kubernetes", 2L);
+    public static final Pair<String, Long> SUBGROUP_HIGH_AVAILABILITY = new 
Pair<>("High Availability", 3L);
+    public static final Pair<String, Long> SUBGROUP_IMAGES = new 
Pair<>("Images", 1L);
+    public static final Pair<String, Long> SUBGROUP_VOLUME = new 
Pair<>("Volume", 2L);
+    public static final Pair<String, Long> SUBGROUP_SNAPSHOT = new 
Pair<>("Snapshot", 3L);
+    public static final Pair<String, Long> SUBGROUP_VM_SNAPSHOT = new 
Pair<>("VM Snapshot", 4L);
+    public static final Pair<String, Long> SUBGROUP_NETWORK = new 
Pair<>("Network", 1L);
+    public static final Pair<String, Long> SUBGROUP_DHCP = new Pair<>("DHCP", 
2L);
+    public static final Pair<String, Long> SUBGROUP_VPC = new Pair<>("VPC", 
3L);
+    public static final Pair<String, Long> SUBGROUP_LOADBALANCER = new 
Pair<>("LoadBalancer", 4L);
+    public static final Pair<String, Long> SUBGROUP_API = new Pair<>("API", 
1L);
+    public static final Pair<String, Long> SUBGROUP_ALERTS = new 
Pair<>("Alerts", 2L);
+    public static final Pair<String, Long> SUBGROUP_EVENTS = new 
Pair<>("Events", 3L);
+    public static final Pair<String, Long> SUBGROUP_SECURITY = new 
Pair<>("Security", 4L);
+    public static final Pair<String, Long> SUBGROUP_USAGE = new 
Pair<>("Usage", 1L);
+    public static final Pair<String, Long> SUBGROUP_LIMITS = new 
Pair<>("Limits", 6L);
+    public static final Pair<String, Long> SUBGROUP_JOBS = new Pair<>("Jobs", 
7L);
+    public static final Pair<String, Long> SUBGROUP_AGENT = new 
Pair<>("Agent", 8L);
+    public static final Pair<String, Long> SUBGROUP_HYPERVISOR = new 
Pair<>("Hypervisor", 1L);
+    public static final Pair<String, Long> SUBGROUP_KVM = new Pair<>("KVM", 
2L);
+    public static final Pair<String, Long> SUBGROUP_VMWARE = new 
Pair<>("VMware", 3L);
+    public static final Pair<String, Long> SUBGROUP_XENSERVER = new 
Pair<>("XenServer", 4L);
+    public static final Pair<String, Long> SUBGROUP_OVM = new Pair<>("OVM", 
5L);
+    public static final Pair<String, Long> SUBGROUP_BAREMETAL = new 
Pair<>("Baremetal", 6L);
+    public static final Pair<String, Long> SUBGROUP_CONSOLE_PROXY_VM = new 
Pair<>("ConsoleProxyVM", 1L);
+    public static final Pair<String, Long> SUBGROUP_SEC_STORAGE_VM = new 
Pair<>("SecStorageVM", 2L);
+    public static final Pair<String, Long> SUBGROUP_VIRTUAL_ROUTER = new 
Pair<>("VirtualRouter", 3L);
+    public static final Pair<String, Long> SUBGROUP_DIAGNOSTICS = new 
Pair<>("Diagnostics", 4L);
+    public static final Pair<String, Long> SUBGROUP_PRIMARY_STORAGE = new 
Pair<>("Primary Storage", 1L);
+    public static final Pair<String, Long> SUBGROUP_SECONDARY_STORAGE = new 
Pair<>("Secondary Storage", 2L);
+    public static final Pair<String, Long> SUBGROUP_BACKUP_AND_RECOVERY = new 
Pair<>("Backup & Recovery", 1L);
+    public static final Pair<String, Long> SUBGROUP_CERTIFICATE_AUTHORITY = 
new Pair<>("Certificate Authority", 2L);
+    public static final Pair<String, Long> SUBGROUP_QUOTA = new 
Pair<>("Quota", 3L);
+    public static final Pair<String, Long> SUBGROUP_CLOUDIAN = new 
Pair<>("Cloudian", 4L);
+    public static final Pair<String, Long> SUBGROUP_DRS = new Pair<>("DRS", 
4L);
+
     public enum Scope {
         Global(null, 1),
         Zone(Global, 1 << 1),
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 f895ba2944c..4dadec21325 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
@@ -35,6 +35,8 @@ import org.apache.cloudstack.config.ApiServiceConfiguration;
 import 
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.managed.context.ManagedContextRunnable;
+import org.apache.cloudstack.userdata.UserDataManager;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Component;
 
 import com.cloud.agent.AgentManager;
@@ -101,6 +103,9 @@ import com.cloud.vm.VirtualMachineProfile;
 import com.cloud.vm.dao.DomainRouterDao;
 import com.cloud.vm.dao.NicDao;
 
+import static 
com.cloud.network.router.VirtualNetworkApplianceManager.VirtualRouterUserData;
+import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
+
 @Component
 public class ElasticLoadBalancerManagerImpl extends ManagerBase implements 
ElasticLoadBalancerManager, VirtualMachineGuru {
 
@@ -136,6 +141,8 @@ public class ElasticLoadBalancerManagerImpl extends 
ManagerBase implements Elast
     private ElasticLbVmMapDao _elbVmMapDao;
     @Inject
     private NicDao _nicDao;
+    @Inject
+    private UserDataManager userDataManager;
 
     String _instance;
 
@@ -477,6 +484,19 @@ public class ElasticLoadBalancerManagerImpl extends 
ManagerBase implements Elast
         }
         String msPublicKey = _configDao.getValue("ssh.publickey");
         buf.append(" 
authorized_key=").append(VirtualMachineGuru.getEncodedMsPublicKey(msPublicKey));
+
+        if (SystemVmEnableUserData.valueIn(dc.getId())) {
+            String userDataUuid = VirtualRouterUserData.valueIn(dc.getId());
+            try {
+                String userData = 
userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
+                if (StringUtils.isNotBlank(userData)) {
+                    buf.append(" userdata=").append(userData);
+                }
+            } catch (Exception e) {
+                logger.warn("Failed to load user data for the elastic lb vm, 
ignored", e);
+            }
+        }
+
         if (logger.isDebugEnabled()) {
             logger.debug("Boot Args for " + profile + ": " + buf.toString());
         }
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 d979a4b3033..2c78120559e 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,6 +21,8 @@ import static 
com.cloud.hypervisor.Hypervisor.HypervisorType.KVM;
 import static com.cloud.hypervisor.Hypervisor.HypervisorType.LXC;
 import static com.cloud.hypervisor.Hypervisor.HypervisorType.VMware;
 import static com.cloud.hypervisor.Hypervisor.HypervisorType.XenServer;
+import static 
com.cloud.network.router.VirtualNetworkApplianceManager.VirtualRouterUserData;
+import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -39,6 +41,7 @@ import 
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationSe
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.lb.ApplicationLoadBalancerRuleVO;
 import org.apache.cloudstack.lb.dao.ApplicationLoadBalancerRuleDao;
+import org.apache.cloudstack.userdata.UserDataManager;
 import org.apache.commons.collections.CollectionUtils;
 
 import com.cloud.agent.AgentManager;
@@ -126,6 +129,7 @@ import com.cloud.vm.VirtualMachineProfile;
 import com.cloud.vm.VirtualMachineProfile.Param;
 import com.cloud.vm.dao.DomainRouterDao;
 import com.cloud.vm.dao.NicDao;
+import org.apache.commons.lang3.StringUtils;
 
 public class InternalLoadBalancerVMManagerImpl extends ManagerBase implements 
InternalLoadBalancerVMManager, InternalLoadBalancerVMService, 
VirtualMachineGuru {
     static final private String InternalLbVmNamePrefix = "b";
@@ -175,6 +179,8 @@ public class InternalLoadBalancerVMManagerImpl extends 
ManagerBase implements In
     ResourceManager _resourceMgr;
     @Inject
     UserDao _userDao;
+    @Inject
+    private UserDataManager userDataManager;
 
     @Override
     public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile 
profile, final DeployDestination dest, final ReservationContext context) {
@@ -243,6 +249,19 @@ public class InternalLoadBalancerVMManagerImpl extends 
ManagerBase implements In
         final String type = "ilbvm";
         buf.append(" type=" + type);
 
+        long dcId = profile.getVirtualMachine().getDataCenterId();
+        if (SystemVmEnableUserData.valueIn(dcId)) {
+            String userDataUuid = VirtualRouterUserData.valueIn(dcId);
+            try {
+                String userData = 
userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
+                if (StringUtils.isNotBlank(userData)) {
+                    buf.append(" userdata=").append(userData);
+                }
+            } catch (Exception e) {
+                logger.warn("Failed to load user data for the internal lb vm, 
ignored", e);
+            }
+        }
+
         if (logger.isDebugEnabled()) {
             logger.debug("Boot Args for " + profile + ": " + buf.toString());
         }
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 6af10b51936..23c914bc7b0 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 @@ package org.apache.cloudstack.internallbvmmgr;
 
 import java.io.IOException;
 
+import org.apache.cloudstack.userdata.UserDataManager;
 import org.mockito.Mockito;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
@@ -166,6 +167,11 @@ public class LbChildTestConfiguration {
             return Mockito.mock(AccountDao.class);
         }
 
+        @Bean
+        public UserDataManager userDataManager() {
+            return Mockito.mock(UserDataManager.class);
+        }
+
         @Override
         public boolean match(MetadataReader mdr, MetadataReaderFactory arg1) 
throws IOException {
             mdr.getClassMetadata().getClassName();
diff --git 
a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java 
b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
index 2c5a931a831..b96de3c3501 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.inject.Inject;
 import javax.naming.ConfigurationException;
 
 import com.cloud.consoleproxy.ConsoleProxyManager;
+import com.cloud.network.router.VirtualNetworkApplianceManager;
+import com.cloud.storage.secondary.SecondaryStorageVmManager;
+import com.cloud.vm.VirtualMachineManager;
 import org.apache.cloudstack.acl.RoleType;
 import org.apache.cloudstack.acl.SecurityChecker;
 import org.apache.cloudstack.affinity.AffinityGroup;
@@ -638,6 +641,11 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
     protected void populateConfigKeysAllowedOnlyForDefaultAdmin() {
         
configKeysAllowedOnlyForDefaultAdmin.add(AccountManagerImpl.listOfRoleTypesAllowedForOperationsOfSameRoleType.key());
         
configKeysAllowedOnlyForDefaultAdmin.add(AccountManagerImpl.allowOperationsOnUsersInSameAccount.key());
+
+        
configKeysAllowedOnlyForDefaultAdmin.add(VirtualMachineManager.SystemVmEnableUserData.key());
+        
configKeysAllowedOnlyForDefaultAdmin.add(ConsoleProxyManager.ConsoleProxyVmUserData.key());
+        
configKeysAllowedOnlyForDefaultAdmin.add(SecondaryStorageVmManager.SecondaryStorageVmUserData.key());
+        
configKeysAllowedOnlyForDefaultAdmin.add(VirtualNetworkApplianceManager.VirtualRouterUserData.key());
     }
 
     private void initMessageBusListener() {
diff --git 
a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java 
b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java
index 7b5fc123fb0..f344b1b7c46 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<String> ConsoleProxyManagementLastState = new 
ConfigKey<String>(ConfigKey.CATEGORY_ADVANCED, String.class, 
"consoleproxy.management.state.last", 
com.cloud.consoleproxy.ConsoleProxyManagementState.Auto.toString(),
             "last console proxy service management state", false, 
ConfigKey.Kind.Select, consoleProxyManagementStates);
 
+    ConfigKey<String> ConsoleProxyVmUserData = new ConfigKey<>(String.class, 
"console.proxy.vm.userdata",
+            ConfigKey.CATEGORY_ADVANCED, "",
+            "UUID for user data for console proxy VMs. This works only when 
systemvm.userdata.enabled is set to true",
+            true, ConfigKey.Scope.Zone, null, "User Data for CPVMs",
+            null, ConfigKey.GROUP_SYSTEM_VMS, 
ConfigKey.SUBGROUP_CONSOLE_PROXY_VM);
+
     void setManagementState(ConsoleProxyManagementState state);
 
     ConsoleProxyManagementState getManagementState();
diff --git 
a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java 
b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
index c273cf40e2f..7089f1354ec 100644
--- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
+++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
@@ -48,6 +48,7 @@ import 
org.apache.cloudstack.framework.security.keystore.KeystoreVO;
 import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
 import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
+import org.apache.cloudstack.userdata.UserDataManager;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.BooleanUtils;
 
@@ -152,6 +153,8 @@ import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonParseException;
 
+import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
+
 /**
  * Class to manage console proxys. <br><br>
  * Possible console proxy state transition cases:<br>
@@ -227,6 +230,8 @@ public class ConsoleProxyManagerImpl extends ManagerBase 
implements ConsoleProxy
     private CAManager caManager;
     @Inject
     private NetworkOrchestrationService networkMgr;
+    @Inject
+    private UserDataManager userDataManager;
 
     private ConsoleProxyListener consoleProxyListener;
 
@@ -1265,6 +1270,19 @@ public class ConsoleProxyManagerImpl extends ManagerBase 
implements ConsoleProxy
             buf.append(" vncport=").append(getVncPort(datacenterId));
         }
         buf.append(" 
keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16)));
+
+        if (SystemVmEnableUserData.valueIn(dc.getId())) {
+            String userDataUuid = ConsoleProxyVmUserData.valueIn(dc.getId());
+            try {
+                String userData = 
userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
+                if (StringUtils.isNotBlank(userData)) {
+                    buf.append(" userdata=").append(userData);
+                }
+            } catch (Exception e) {
+                logger.warn("Failed to load user data for the cpvm, ignored", 
e);
+            }
+        }
+
         String bootArgs = buf.toString();
         if (logger.isDebugEnabled()) {
             logger.debug("Boot Args for " + profile + ": " + bootArgs);
@@ -1570,9 +1588,10 @@ public class ConsoleProxyManagerImpl extends ManagerBase 
implements ConsoleProxy
 
     @Override
     public ConfigKey<?>[] getConfigKeys() {
-        return new ConfigKey<?>[] { ConsoleProxySslEnabled, 
NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled, 
ConsoleProxyServiceOffering,
-                ConsoleProxyCapacityStandby, ConsoleProxyCapacityScanInterval, 
ConsoleProxyCmdPort, ConsoleProxyRestart, ConsoleProxyUrlDomain, 
ConsoleProxySessionMax, ConsoleProxySessionTimeout, 
ConsoleProxyDisableRpFilter, ConsoleProxyLaunchMax,
-                ConsoleProxyManagementLastState, 
ConsoleProxyServiceManagementState, NoVncConsoleShowDot };
+        return new ConfigKey<?>[] {ConsoleProxySslEnabled, 
NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled, 
ConsoleProxyServiceOffering,
+                                   ConsoleProxyCapacityStandby, 
ConsoleProxyCapacityScanInterval, ConsoleProxyCmdPort, ConsoleProxyRestart, 
ConsoleProxyUrlDomain, ConsoleProxySessionMax, ConsoleProxySessionTimeout, 
ConsoleProxyDisableRpFilter, ConsoleProxyLaunchMax,
+                                   ConsoleProxyManagementLastState, 
ConsoleProxyServiceManagementState, NoVncConsoleShowDot,
+                                   ConsoleProxyVmUserData};
     }
 
     protected ConsoleProxyStatus parseJsonToConsoleProxyStatus(String json) 
throws JsonParseException {
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 f77081aa96c..8ef77d3fb32 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<String> RouterTemplateOvm3 = new ConfigKey<>(String.class, 
RouterTemplateOvm3CK, "Advanced", "SystemVM Template (Ovm3)",
             "Name of the default router template on Ovm3.", true, 
ConfigKey.Scope.Zone, null);
 
+    ConfigKey<String> VirtualRouterUserData = new ConfigKey<>(String.class, 
"virtual.router.userdata",
+            ConfigKey.CATEGORY_ADVANCED, "",
+            "UUID for user data of VR, VPC VR, internal LB, and elastic LB. 
This works only when systemvm.userdata.enabled is set to true",
+            true, ConfigKey.Scope.Zone, null, "User Data for VRs",
+            null, ConfigKey.GROUP_SYSTEM_VMS, 
ConfigKey.SUBGROUP_VIRTUAL_ROUTER);
+
     ConfigKey<Boolean> SetServiceMonitor = new ConfigKey<>(Boolean.class, 
SetServiceMonitorCK, "Advanced", "true",
             "service monitoring in router enable/disable option, default 
true", true, ConfigKey.Scope.Zone, null);
 
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 eb5995b56f8..e881bd7ac6e 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;
@@ -71,6 +72,7 @@ import org.apache.cloudstack.network.BgpPeer;
 import org.apache.cloudstack.network.RoutedIpv4Manager;
 import org.apache.cloudstack.network.topology.NetworkTopology;
 import org.apache.cloudstack.network.topology.NetworkTopologyContext;
+import org.apache.cloudstack.userdata.UserDataManager;
 import org.apache.cloudstack.utils.CloudStackVersion;
 import org.apache.cloudstack.utils.identity.ManagementServerNode;
 import org.apache.cloudstack.utils.usage.UsageUtils;
@@ -352,6 +354,8 @@ Configurable, StateListener<VirtualMachine.State, 
VirtualMachine.Event, VirtualM
     @Inject
     BGPService bgpService;
 
+    @Inject
+    private UserDataManager userDataManager;
     private int _routerStatsInterval = 300;
     private int _routerCheckInterval = 30;
     private int _rvrStatusUpdatePoolSize = 10;
@@ -2096,6 +2100,18 @@ Configurable, StateListener<VirtualMachine.State, 
VirtualMachine.Event, VirtualM
                 " on the virtual router.", RouterLogrotateFrequency.key(), 
routerLogrotateFrequency, dc.getUuid()));
         buf.append(String.format(" logrotatefrequency=%s", 
routerLogrotateFrequency));
 
+        if (SystemVmEnableUserData.valueIn(router.getDataCenterId())) {
+            String userDataUuid = VirtualRouterUserData.valueIn(dc.getId());
+            try {
+                String userData = 
userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
+                if (StringUtils.isNotBlank(userData)) {
+                    buf.append(" userdata=").append(userData);
+                }
+            } catch (Exception e) {
+                logger.warn("Failed to load user data for the virtual router, 
ignored", e);
+            }
+        }
+
         if (logger.isDebugEnabled()) {
             logger.debug("Boot Args for " + profile + ": " + buf);
         }
@@ -3355,7 +3371,8 @@ Configurable, StateListener<VirtualMachine.State, 
VirtualMachine.Event, VirtualM
                 RouterHealthChecksMaxMemoryUsageThreshold,
                 ExposeDnsAndBootpServer,
                 RouterLogrotateFrequency,
-                RemoveControlIpOnStop
+                RemoveControlIpOnStop,
+                VirtualRouterUserData
         };
     }
 
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 a34658a7f6d..599e8c171c5 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<String> SecondaryStorageVmUserData = new 
ConfigKey<>(String.class, "secstorage.vm.userdata",
+            ConfigKey.CATEGORY_ADVANCED, "",
+            "UUID for user data for secondary storage VMs. This works only 
when systemvm.userdata.enabled is set to true",
+            true, ConfigKey.Scope.Zone, null, "User Data for SSVMs",
+            null, ConfigKey.GROUP_SYSTEM_VMS, 
ConfigKey.SUBGROUP_SEC_STORAGE_VM);
+
+
     public static final int DEFAULT_SS_VM_RAMSIZE = 512;            // 512M
     public static final int DEFAULT_SS_VM_CPUMHZ = 500;             // 500 MHz
     public static final int DEFAULT_SS_VM_MTUSIZE = 1500;
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 fae8e69a386..2dbd34ad8ad 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,6 +17,7 @@
 package org.apache.cloudstack.secondarystorage;
 
 import static 
com.cloud.configuration.Config.SecStorageAllowedInternalDownloadSites;
+import static com.cloud.vm.VirtualMachineManager.SystemVmEnableUserData;
 
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -50,6 +51,7 @@ import 
org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
 import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
 import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao;
+import org.apache.cloudstack.userdata.UserDataManager;
 import org.apache.cloudstack.utils.identity.ManagementServerNode;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.ArrayUtils;
@@ -255,6 +257,9 @@ public class SecondaryStorageManagerImpl extends 
ManagerBase implements Secondar
     private IndirectAgentLB indirectAgentLB;
     @Inject
     private CAManager caManager;
+    @Inject
+    private UserDataManager userDataManager;
+
     private int _secStorageVmMtuSize;
 
     private String _instance;
@@ -1227,6 +1232,19 @@ public class SecondaryStorageManagerImpl extends 
ManagerBase implements Secondar
         String nfsVersion = imageStoreDetailsUtil != null ? 
imageStoreDetailsUtil.getNfsVersion(secStores.get(0).getId()) : null;
         buf.append(" nfsVersion=").append(nfsVersion);
         buf.append(" 
keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16)));
+
+        if (SystemVmEnableUserData.valueIn(dc.getId())) {
+            String userDataUuid = 
SecondaryStorageVmUserData.valueIn(dc.getId());
+            try {
+                String userData = 
userDataManager.validateAndGetUserDataForSystemVM(userDataUuid);
+                if (StringUtils.isNotBlank(userData)) {
+                    buf.append(" userdata=").append(userData);
+                }
+            } catch (Exception e) {
+                logger.warn("Failed to load user data for the ssvm, ignored", 
e);
+            }
+        }
+
         String bootArgs = buf.toString();
         if (logger.isDebugEnabled()) {
             logger.debug(String.format("Boot args for machine profile [%s]: 
[%s].", profile.toString(), bootArgs));
@@ -1529,7 +1547,8 @@ public class SecondaryStorageManagerImpl extends 
ManagerBase implements Secondar
 
     @Override
     public ConfigKey<?>[] getConfigKeys() {
-        return new ConfigKey<?>[] {NTPServerConfig, 
MaxNumberOfSsvmsForMigration, SecondaryStorageCapacityScanInterval};
+        return new ConfigKey<?>[] {NTPServerConfig, 
MaxNumberOfSsvmsForMigration, SecondaryStorageCapacityScanInterval,
+                                   SecondaryStorageVmUserData};
     }
 
 }
diff --git a/systemvm/debian/opt/cloud/bin/setup/init.sh 
b/systemvm/debian/opt/cloud/bin/setup/init.sh
index b6da7059366..66b08b99205 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 801770fcc83..1ad4de4c1a5 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,97 @@ fi
 
 CMDLINE=/var/cache/cloud/cmdline
 TYPE=$(grep -Po 'type=\K[a-zA-Z]*' $CMDLINE)
+
+# Execute cloud-init if user data is present
+run_cloud_init() {
+  if [ ! -f "$CMDLINE" ]; then
+    log_it "No cmdline file found, skipping cloud-init execution"
+    return 0
+  fi
+
+  local encoded_userdata=$(grep -Po 'userdata=\K[^[:space:]]*' "$CMDLINE" || 
true)
+  if [ -z "$encoded_userdata" ]; then
+    log_it "No user data found in cmdline, skipping cloud-init execution"
+    return 0
+  fi
+
+  log_it "User data detected, setting up and running cloud-init"
+
+  # Update cloud-init config to use NoCloud datasource
+  cat <<EOF > /etc/cloud/cloud.cfg.d/cloudstack.cfg
+#cloud-config
+datasource_list: ['NoCloud']
+network:
+  config: disabled
+manage_etc_hosts: false
+manage_resolv_conf: false
+users: []
+disable_root: false
+ssh_pwauth: false
+cloud_init_modules:
+  - migrator
+  - seed_random
+  - bootcmd
+  - write-files
+  - growpart
+  - resizefs
+  - disk_setup
+  - mounts
+  - rsyslog
+cloud_config_modules:
+  - locale
+  - timezone
+  - runcmd
+cloud_final_modules:
+  - scripts-per-once
+  - scripts-per-boot
+  - scripts-per-instance
+  - scripts-user
+  - final-message
+  - power-state-change
+EOF
+
+  # Set up user data files (reuse the function from init.sh)
+  mkdir -p /var/lib/cloud/seed/nocloud
+
+  # Decode and decompress user data
+  local decoded_userdata
+  decoded_userdata=$(echo "$encoded_userdata" | base64 -d 2>/dev/null | gunzip 
2>/dev/null)
+  if [ $? -ne 0 ] || [ -z "$decoded_userdata" ]; then
+    log_it "ERROR: Failed to decode or decompress user data"
+    return 1
+  fi
+
+  # Write user data
+  echo "$decoded_userdata" > /var/lib/cloud/seed/nocloud/user-data
+  chmod 600 /var/lib/cloud/seed/nocloud/user-data
+
+  # Create meta-data
+  local instance_name=$(grep -Po 'name=\K[^[:space:]]*' "$CMDLINE" || hostname)
+  cat > /var/lib/cloud/seed/nocloud/meta-data << EOF
+instance-id: $instance_name
+local-hostname: $instance_name
+EOF
+  chmod 644 /var/lib/cloud/seed/nocloud/meta-data
+
+  log_it "User data files created, executing cloud-init..."
+
+  # Run cloud-init stages manually
+  cloud-init init --local && \
+  cloud-init init && \
+  cloud-init modules --mode=config && \
+  cloud-init modules --mode=final
+
+  local cloud_init_result=$?
+  if [ $cloud_init_result -eq 0 ]; then
+    log_it "Cloud-init executed successfully"
+  else
+    log_it "ERROR: Cloud-init execution failed with exit code: 
$cloud_init_result"
+  fi
+
+  return $cloud_init_result
+}
+
 if [ "$TYPE" == "router" ] || [ "$TYPE" == "vpcrouter" ] || [ "$TYPE" == 
"dhcpsrvr" ]
 then
   if [ -x /opt/cloud/bin/update_config.py ]
@@ -71,4 +164,6 @@ do
   systemctl disable --now --no-block $svc
 done
 
+run_cloud_init
+
 date > /var/cache/cloud/boot_up_done
diff --git a/test/integration/smoke/test_ssvm.py 
b/test/integration/smoke/test_ssvm.py
index 0784bc3820c..798401f68e1 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 marvin.lib.common import (get_zone,
 from nose.plugins.attrib import attr
 import telnetlib
 import logging
+import base64
+import os
+import urllib
+import zipfile
 
 # Import System modules
 import time
diff --git a/test/integration/smoke/test_systemvm_userdata.py 
b/test/integration/smoke/test_systemvm_userdata.py
new file mode 100644
index 00000000000..e34ae53fcc7
--- /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.vm.userdata', 'console.proxy.vm.userdata', 
'virtual.router.userdata')
+            vm_type_display_name: Display name for the VM type (e.g., 'SSVM', 
'CPVM', 'VR')
+
+        Returns:
+            UserData object
+        """
+        userdata_script = f"""#!/bin/bash
+echo "User data script ran successfully on {vm_type_display_name}" > 
/tmp/userdata.txt
+"""
+        b64_encoded_userdata = 
base64.b64encode(userdata_script.encode("utf-8")).decode(
+            "utf-8"
+        )
+
+        # Create a userdata entry
+        try:
+            userdata = UserData.register(
+                self.apiclient, name=userdata_name, 
userdata=b64_encoded_userdata
+            )
+            userdata_id = userdata["userdata"]["id"]
+        except Exception as e:
+            if "already exists" in str(e):
+                self.debug("Userdata already exists, getting it")
+                userdata = UserData.list(
+                    self.apiclient, name=userdata_name, listall=True
+                )[0]
+                userdata_id = userdata.id
+            else:
+                self.fail("Failed to register userdata: %s" % e)
+
+        # Update global configuration to use this userdata
+        cmd = updateConfiguration.updateConfigurationCmd()
+        cmd.name = global_setting_name
+        cmd.value = userdata_id
+        self.apiclient.updateConfiguration(cmd)
+        self.debug(
+            "Updated global setting %s with userdata ID: %s"
+            % (global_setting_name, userdata.id)
+        )
+
+        return userdata_id
+
+    def download_and_verify_diagnostics_data(
+        self, target_id, vm_type_display_name, expected_content, retries=4
+    ):
+        """Helper method to download and verify diagnostics data
+
+        Args:
+            target_id: ID of the target VM/router
+            vm_type_display_name: Display name for log messages (e.g., 'SSVM', 
'CPVM', 'VR')
+            expected_content: Expected content to verify in the userdata file
+            retries: Number of retries for getDiagnosticsData (default: 4)
+        """
+        # Create a random temporary directory for this test
+        random_suffix = uuid.uuid4().hex[:8]
+        vm_type_prefix = vm_type_display_name.lower()
+        temp_dir = f"/tmp/{vm_type_prefix}-{random_suffix}"
+        os.makedirs(temp_dir, exist_ok=True)
+
+        # Download the file created by userdata script using the 
getDiagnosticsData command
+        cmd = getDiagnosticsData.getDiagnosticsDataCmd()
+        cmd.targetid = target_id
+        cmd.files = "/tmp/userdata.txt"
+
+        # getDiagnosticsData command takes some time to work after a VM is 
started
+        response = None
+        while retries > -1:
+            try:
+                response = self.apiclient.getDiagnosticsData(cmd)
+                break  # Success, exit retry loop
+            except Exception as e:
+                if retries >= 0:
+                    retries = retries - 1
+                    self.debug(
+                        "getDiagnosticsData failed (retries left: %d): %s"
+                        % (retries + 1, e)
+                    )
+                    if retries > -1:
+                        time.sleep(30)
+                        continue
+                # If all retries exhausted, re-raise the exception
+                self.fail("Failed to get diagnostics data after retries: %s" % 
e)
+
+        # Download response.url file to temporary directory and extract it
+        zip_file_path = os.path.join(temp_dir, "userdata.zip")
+        extracted_file_path = os.path.join(temp_dir, "userdata.txt")
+
+        self.debug(
+            "Downloading userdata file from %s to %s"
+            % (vm_type_display_name, zip_file_path)
+        )
+        try:
+            urllib.request.urlretrieve(response.url, zip_file_path)
+        except Exception as e:
+            self.fail(
+                "Failed to download userdata file from %s: %s"
+                % (vm_type_display_name, e)
+            )
+        self.debug(
+            "Downloaded userdata file from %s: %s"
+            % (vm_type_display_name, zip_file_path)
+        )
+
+        try:
+            with zipfile.ZipFile(zip_file_path, "r") as zip_ref:
+                zip_ref.extractall(temp_dir)
+        except zipfile.BadZipFile as e:
+            self.fail("Downloaded userdata file is not a zip file: %s" % e)
+        self.debug("Extracted userdata file from zip: %s" % 
extracted_file_path)
+
+        # Verify the file contains the expected content
+        try:
+            with open(extracted_file_path, "r") as f:
+                content = f.read().strip()
+            self.debug("Userdata file content: %s" % content)
+            self.assertEqual(
+                expected_content in content,
+                True,
+                f"Check that userdata file contains expected content: 
'{expected_content}'",
+            )
+        except FileNotFoundError:
+            self.fail(
+                "Userdata file not found in extracted zip at %s" % 
extracted_file_path
+            )
+        except Exception as e:
+            self.fail("Failed to read userdata file: %s" % e)
+        finally:
+            # Clean up temporary directory
+            try:
+                if os.path.exists(temp_dir):
+                    shutil.rmtree(temp_dir)
+                    self.debug("Cleaned up temporary directory: %s" % temp_dir)
+            except Exception as e:
+                self.debug(
+                    "Failed to clean up temporary directory %s: %s" % 
(temp_dir, e)
+                )
+
+    def test_userdata_on_systemvm(
+        self, systemvm_type, userdata_name, vm_type_display_name, 
global_setting_name
+    ):
+        """Helper method to test user data functionality on system VMs
+
+        Args:
+            systemvm_type: Type of system VM ('secondarystoragevm' or 
'consoleproxy')
+            userdata_name: Name for the userdata entry
+            vm_type_display_name: Display name for log messages (e.g., 'SSVM' 
or 'CPVM')
+            global_setting_name: Global setting name for userdata (e.g., 
'secstorage.vm.userdata' or 'console.proxy.vm.userdata')
+        """
+        # 1) Register userdata and configure global setting
+        self.userdata_id = self.register_userdata(
+            userdata_name, global_setting_name, vm_type_display_name
+        )
+
+        # 2) Get and destroy the system VM to trigger recreation with userdata
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype=systemvm_type,
+            state="Running",
+            zoneid=self.zone.id,
+        )
+        self.assertEqual(
+            isinstance(list_ssvm_response, list),
+            True,
+            "Check list response returns a valid list",
+        )
+
+        ssvm = list_ssvm_response[0]
+        self.debug("Destroying %s: %s" % (vm_type_display_name, ssvm.id))
+        cmd = destroySystemVm.destroySystemVmCmd()
+        cmd.id = ssvm.id
+        self.apiclient.destroySystemVm(cmd)
+
+        # 3) Wait for the system VM to be running again
+        ssvm_response = self.checkForRunningSystemVM(ssvm, systemvm_type)
+        self.debug(
+            "%s state after restart: %s" % (vm_type_display_name, 
ssvm_response.state)
+        )
+        self.assertEqual(
+            ssvm_response.state,
+            "Running",
+            "Check whether %s is running or not" % vm_type_display_name,
+        )
+        # Wait for the agent to be up
+        self.waitForSystemVMAgent(ssvm_response.name)
+
+        # 4) Download and verify the diagnostics data
+        expected_content = (
+            f"User data script ran successfully on {vm_type_display_name}"
+        )
+        self.download_and_verify_diagnostics_data(
+            ssvm_response.id, vm_type_display_name, expected_content
+        )
+
+    @attr(
+        tags=["advanced", "advancedns", "smoke", "basic", "sg"],
+        required_hardware="true",
+    )
+    def test_1_userdata_on_ssvm(self):
+        """Test user data functionality on SSVM"""
+        self.test_userdata_on_systemvm(
+            systemvm_type="secondarystoragevm",
+            userdata_name="ssvm_userdata",
+            vm_type_display_name="SSVM",
+            global_setting_name="secstorage.vm.userdata",
+        )
+
+    @attr(
+        tags=["advanced", "advancedns", "smoke", "basic", "sg"],
+        required_hardware="true",
+    )
+    def test_2_userdata_on_cpvm(self):
+        """Test user data functionality on CPVM"""
+        self.test_userdata_on_systemvm(
+            systemvm_type="consoleproxy",
+            userdata_name="cpvm_userdata",
+            vm_type_display_name="CPVM",
+            global_setting_name="console.proxy.vm.userdata",
+        )
+
+    @attr(
+        tags=["advanced", "advancedns", "smoke", "basic", "sg"],
+        required_hardware="true",
+    )
+    def test_3_userdata_on_vr(self):
+        """Test user data functionality on VR"""
+        # 1) Register userdata and configure global setting
+        self.userdata_id = self.register_userdata("vr_userdata", 
"virtual.router.userdata", "VR")
+
+        # 2) Create an isolated network which will trigger VR creation with 
userdata
+        result = createEnabledNetworkOffering(
+            self.apiclient, self.testData["nw_off_isolated_persistent"]
+        )
+        assert result[0] == PASS, (
+            "Network offering creation/enabling failed due to %s" % result[2]
+        )
+        isolated_persistent_network_offering = result[1]
+
+        # Create an isolated network
+        self.network = Network.create(
+            self.apiclient,
+            self.testData["isolated_network"],
+            networkofferingid=isolated_persistent_network_offering.id,
+            zoneid=self.zone.id,
+        )
+        self.assertIsNotNone(self.network, "Network creation failed")
+        self.cleanup.append(self.network)
+        self.cleanup.append(isolated_persistent_network_offering)
+
+        # 3) Get the VR and verify it's running
+        routers = list_routers(
+            self.apiclient, networkid=self.network.id, state="Running"
+        )
+        self.assertEqual(
+            isinstance(routers, list),
+            True,
+            "Check list router response returns a valid list",
+        )
+        self.assertNotEqual(len(routers), 0, "Check list router response")
+        router = routers[0]
+        self.debug("Found VR: %s" % router.id)
+
+        # 4) Download and verify the diagnostics data
+        # VR doesn't need retries as it's freshly created
+        expected_content = "User data script ran successfully on VR"
+        self.download_and_verify_diagnostics_data(router.id, "VR", 
expected_content)
diff --git 
a/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh 
b/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh
index 02a5c39dc71..219586a9729 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 <<EOF > /etc/cloud/cloud.cfg.d/cloudstack.cfg
+  cat <<EOF > /etc/cloud/cloud.cfg.d/cloudstack.cfg
 datasource_list: ['CloudStack']
 datasource:
   CloudStack:

Reply via email to