Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,23 @@

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",
"Max length of vm userdata after base64 encoding. Default is 32768 and maximum is 1048576", true);

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5136,7 +5136,7 @@ public ConfigKey<?>[] getConfigKeys() {
VmConfigDriveLabel, VmConfigDriveOnPrimaryPool, VmConfigDriveForceHostCacheUse, VmConfigDriveUseHostCacheOnUnsupportedPool,
HaVmRestartHostUp, ResourceCountRunningVMsonly, AllowExposeHypervisorHostname, AllowExposeHypervisorHostnameAccountLevel, SystemVmRootDiskSize,
AllowExposeDomainInMetadata, MetadataCustomCloudName, VmMetadataManufacturer, VmMetadataProductName,
VmSyncPowerStateTransitioning
VmSyncPowerStateTransitioning, SystemVmEnableUserData
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,7 +37,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;
Expand Down Expand Up @@ -118,6 +129,25 @@ public String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
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;
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -136,6 +141,8 @@ public class ElasticLoadBalancerManagerImpl extends ManagerBase implements Elast
private ElasticLbVmMapDao _elbVmMapDao;
@Inject
private NicDao _nicDao;
@Inject
private UserDataManager userDataManager;

String _instance;

Expand Down Expand Up @@ -477,6 +484,19 @@ 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 userDataUuid = RouterUserData.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());
}
Expand Down
Loading
Loading