diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 5880804ccfb..4baa3a39cd9 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -100,6 +100,8 @@ public class EventTypes { public static final String EVENT_VM_DYNAMIC_SCALE = "VM.DYNAMIC.SCALE"; public static final String EVENT_VM_RESETPASSWORD = "VM.RESETPASSWORD"; public static final String EVENT_VM_RESETSSHKEY = "VM.RESETSSHKEY"; + + public static final String EVENT_VM_RESETUSERDATA = "VM.RESETUSERDATA"; public static final String EVENT_VM_MIGRATE = "VM.MIGRATE"; public static final String EVENT_VM_MOVE = "VM.MOVE"; public static final String EVENT_VM_RESTORE = "VM.RESTORE"; @@ -229,6 +231,9 @@ public class EventTypes { //registering SSH keypair events public static final String EVENT_REGISTER_SSH_KEYPAIR = "REGISTER.SSH.KEYPAIR"; + //registering userdata events + public static final String EVENT_REGISTER_USER_DATA = "REGISTER.USER.DATA"; + //register for user API and secret keys public static final String EVENT_REGISTER_FOR_SECRET_API_KEY = "REGISTER.USER.KEY"; diff --git a/api/src/main/java/com/cloud/network/NetworkModel.java b/api/src/main/java/com/cloud/network/NetworkModel.java index f2d1a13663c..be59ac4ab07 100644 --- a/api/src/main/java/com/cloud/network/NetworkModel.java +++ b/api/src/main/java/com/cloud/network/NetworkModel.java @@ -20,6 +20,7 @@ package com.cloud.network; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -82,6 +83,9 @@ public interface NetworkModel { .put(HYPERVISOR_HOST_NAME_FILE, HYPERVISOR_HOST_NAME_FILE) .build(); + List metadataFileNames = new ArrayList<>(Arrays.asList(SERVICE_OFFERING_FILE, AVAILABILITY_ZONE_FILE, LOCAL_HOSTNAME_FILE, LOCAL_IPV4_FILE, PUBLIC_HOSTNAME_FILE, PUBLIC_IPV4_FILE, + INSTANCE_ID_FILE, VM_ID_FILE, PUBLIC_KEYS_FILE, CLOUD_IDENTIFIER_FILE, HYPERVISOR_HOST_NAME_FILE)); + static final ConfigKey MACIdentifier = new ConfigKey("Advanced",Integer.class, "mac.identifier", "0", "This value will be used while generating the mac addresses for isolated and shared networks. The hexadecimal equivalent value will be present at the 2nd octet of the mac address. Default value is null which means this feature is disabled.Its scope is global.", true, ConfigKey.Scope.Global); @@ -318,7 +322,7 @@ public interface NetworkModel { boolean getNetworkEgressDefaultPolicy(Long networkId); - List generateVmData(String userData, String serviceOffering, long datacenterId, + List generateVmData(String userData, String userDataDetails, String serviceOffering, long datacenterId, String vmName, String vmHostName, long vmId, String vmUuid, String guestIpAddress, String publicKey, String password, Boolean isWindows, String hostname); String getValidNetworkCidr(Network guestNetwork); diff --git a/api/src/main/java/com/cloud/server/ManagementService.java b/api/src/main/java/com/cloud/server/ManagementService.java index 868e606a333..050b93b63dc 100644 --- a/api/src/main/java/com/cloud/server/ManagementService.java +++ b/api/src/main/java/com/cloud/server/ManagementService.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import com.cloud.user.UserData; import org.apache.cloudstack.api.command.admin.cluster.ListClustersCmd; import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd; import org.apache.cloudstack.api.command.admin.config.UpdateHypervisorCapabilitiesCmd; @@ -55,6 +56,9 @@ import org.apache.cloudstack.api.command.user.ssh.CreateSSHKeyPairCmd; import org.apache.cloudstack.api.command.user.ssh.DeleteSSHKeyPairCmd; import org.apache.cloudstack.api.command.user.ssh.ListSSHKeyPairsCmd; import org.apache.cloudstack.api.command.user.ssh.RegisterSSHKeyPairCmd; +import org.apache.cloudstack.api.command.user.userdata.DeleteUserDataCmd; +import org.apache.cloudstack.api.command.user.userdata.ListUserDataCmd; +import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd; import org.apache.cloudstack.api.command.user.vm.GetVMPasswordCmd; import org.apache.cloudstack.api.command.user.vmgroup.UpdateVMGroupCmd; import org.apache.cloudstack.config.Configuration; @@ -332,6 +336,33 @@ public interface ManagementService { */ String generateRandomPassword(); + /** + * Search registered userdatas for the logged in user. + * + * @param cmd + * The api command class. + * @return The list of userdatas found. + */ + Pair, Integer> listUserDatas(ListUserDataCmd cmd); + + /** + * Registers a userdata. + * + * @param cmd + * The api command class. + * @return A VO with the registered userdata. + */ + UserData registerUserData(RegisterUserDataCmd cmd); + + /** + * Deletes a userdata. + * + * @param cmd + * The api command class. + * @return True on success. False otherwise. + */ + boolean deleteUserData(DeleteUserDataCmd cmd); + /** * Search registered key pairs for the logged in user. * diff --git a/api/src/main/java/com/cloud/template/TemplateApiService.java b/api/src/main/java/com/cloud/template/TemplateApiService.java index ea818a55a0c..5b494c308c3 100644 --- a/api/src/main/java/com/cloud/template/TemplateApiService.java +++ b/api/src/main/java/com/cloud/template/TemplateApiService.java @@ -40,6 +40,7 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.StorageUnavailableException; import com.cloud.user.Account; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; import org.apache.cloudstack.api.response.GetUploadParamsResponse; public interface TemplateApiService { @@ -56,6 +57,8 @@ public interface TemplateApiService { VirtualMachineTemplate prepareTemplate(long templateId, long zoneId, Long storageId); + + boolean detachIso(long vmId, boolean forced); boolean attachIso(long isoId, long vmId, boolean forced); @@ -106,4 +109,6 @@ public interface TemplateApiService { VirtualMachineTemplate updateTemplate(UpdateIsoCmd cmd); VirtualMachineTemplate updateTemplate(UpdateTemplateCmd cmd); + + VirtualMachineTemplate linkUserDataToTemplate(LinkUserDataToTemplateCmd cmd); } diff --git a/api/src/main/java/com/cloud/template/VirtualMachineTemplate.java b/api/src/main/java/com/cloud/template/VirtualMachineTemplate.java index 95d1ebf0b87..9a89d4da644 100644 --- a/api/src/main/java/com/cloud/template/VirtualMachineTemplate.java +++ b/api/src/main/java/com/cloud/template/VirtualMachineTemplate.java @@ -19,6 +19,7 @@ package com.cloud.template; import java.util.Date; import java.util.Map; +import com.cloud.user.UserData; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; @@ -140,4 +141,9 @@ public interface VirtualMachineTemplate extends ControlledEntity, Identity, Inte Date getUpdated(); boolean isDeployAsIs(); + + Long getUserDataId(); + + UserData.UserDataOverridePolicy getUserDataOverridePolicy(); + } diff --git a/api/src/main/java/com/cloud/user/UserData.java b/api/src/main/java/com/cloud/user/UserData.java new file mode 100644 index 00000000000..fa0c50473c0 --- /dev/null +++ b/api/src/main/java/com/cloud/user/UserData.java @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.user; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface UserData extends ControlledEntity, InternalIdentity, Identity { + + public enum UserDataOverridePolicy { + ALLOWOVERRIDE, APPEND, DENYOVERRIDE + } + + String getUserData(); + + String getParams(); +} diff --git a/api/src/main/java/com/cloud/uservm/UserVm.java b/api/src/main/java/com/cloud/uservm/UserVm.java index 16a203746a9..e30f5e03054 100644 --- a/api/src/main/java/com/cloud/uservm/UserVm.java +++ b/api/src/main/java/com/cloud/uservm/UserVm.java @@ -35,6 +35,14 @@ public interface UserVm extends VirtualMachine, ControlledEntity { void setUserData(String userData); + void setUserDataId(Long userDataId); + + Long getUserDataId(); + + void setUserDataDetails(String userDataDetails); + + String getUserDataDetails(); + String getDetail(String name); void setAccountId(long accountId); diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index d6252c08475..36ba95baa35 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -29,6 +29,7 @@ import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; import org.apache.cloudstack.api.command.user.vm.StartVMCmd; @@ -103,6 +104,8 @@ public interface UserVmService { */ UserVm resetVMSSHKey(ResetVMSSHKeyCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException; + UserVm resetVMUserData(ResetVMUserDataCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException; + UserVm startVirtualMachine(StartVMCmd cmd) throws StorageUnavailableException, ExecutionException, ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException; @@ -181,6 +184,8 @@ public interface UserVmService { * base64 encoded before adding it to the request. Currently only * HTTP GET is supported. Using HTTP GET (via querystring), you * can send up to 2KB of data after base64 encoding + * @param userDataId + * @param userDataDetails * @param sshKeyPair * - name of the ssh key pair used to login to the virtual * machine @@ -215,7 +220,7 @@ public interface UserVmService { */ UserVm createBasicSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, - String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard, + String userData, Long userDataId, String userDataDetails, String sshKeyPair, Map requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameter, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled) throws InsufficientCapacityException, @@ -264,6 +269,8 @@ public interface UserVmService { * base64 encoded before adding it to the request. Currently only * HTTP GET is supported. Using HTTP GET (via querystring), you * can send up to 2KB of data after base64 encoding + * @param userDataId + * @param userDataDetails * @param sshKeyPair * - name of the ssh key pair used to login to the virtual * machine @@ -297,7 +304,7 @@ public interface UserVmService { */ UserVm createAdvancedSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List networkIdList, List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, - HTTPMethod httpmethod, String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, + HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled) throws InsufficientCapacityException, @@ -343,6 +350,8 @@ public interface UserVmService { * base64 encoded before adding it to the request. Currently only * HTTP GET is supported. Using HTTP GET (via querystring), you * can send up to 2KB of data after base64 encoding + * @param userDataId + * @param userDataDetails * @param sshKeyPair * - name of the ssh key pair used to login to the virtual * machine @@ -377,7 +386,7 @@ public interface UserVmService { */ UserVm createAdvancedVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List networkIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, - String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, + Long userDataId, String userDataDetails, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, Map templateOvfPropertiesMap, boolean dynamicScalingEnabled, String type) diff --git a/api/src/main/java/org/apache/cloudstack/annotation/AnnotationService.java b/api/src/main/java/org/apache/cloudstack/annotation/AnnotationService.java index 0aca007a44f..6e47347ad71 100644 --- a/api/src/main/java/org/apache/cloudstack/annotation/AnnotationService.java +++ b/api/src/main/java/org/apache/cloudstack/annotation/AnnotationService.java @@ -39,7 +39,7 @@ public interface AnnotationService { enum EntityType { VM(true), VOLUME(true), SNAPSHOT(true), - VM_SNAPSHOT(true), INSTANCE_GROUP(true), SSH_KEYPAIR(true), + VM_SNAPSHOT(true), INSTANCE_GROUP(true), SSH_KEYPAIR(true), USER_DATA(true), NETWORK(true), VPC(true), PUBLIC_IP_ADDRESS(true), VPN_CUSTOMER_GATEWAY(true), TEMPLATE(true), ISO(true), KUBERNETES_CLUSTER(true), SERVICE_OFFERING(false), DISK_OFFERING(false), NETWORK_OFFERING(false), diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 4f80e47b9e9..83fcad54e83 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -394,6 +394,12 @@ public class ApiConstants { public static final String URL = "url"; public static final String USAGE_INTERFACE = "usageinterface"; public static final String USER_DATA = "userdata"; + + public static final String USER_DATA_NAME = "userdataname"; + public static final String USER_DATA_ID = "userdataid"; + public static final String USER_DATA_POLICY = "userdatapolicy"; + public static final String USER_DATA_DETAILS = "userdatadetails"; + public static final String USER_DATA_PARAMS = "userdataparams"; public static final String USER_FILTER = "userfilter"; public static final String USER_ID = "userid"; public static final String USER_SOURCE = "usersource"; diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index 6e149b60edf..d718ecacb23 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.Set; import com.cloud.server.ResourceIcon; -import com.cloud.utils.Pair; import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse; import org.apache.cloudstack.api.response.ResourceIconResponse; import org.apache.cloudstack.api.response.DirectDownloadCertificateHostStatusResponse; @@ -33,6 +32,7 @@ import org.apache.cloudstack.api.response.RollingMaintenanceResponse; import org.apache.cloudstack.direct.download.DirectDownloadCertificate; import org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMap; import org.apache.cloudstack.direct.download.DirectDownloadManager; +import org.apache.cloudstack.api.response.UserDataResponse; import org.apache.cloudstack.management.ManagementServerHost; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; @@ -211,8 +211,10 @@ import com.cloud.user.Account; import com.cloud.user.SSHKeyPair; import com.cloud.user.User; import com.cloud.user.UserAccount; +import com.cloud.user.UserData; import com.cloud.uservm.UserVm; import com.cloud.utils.net.Ip; +import com.cloud.utils.Pair; import com.cloud.vm.InstanceGroup; import com.cloud.vm.Nic; import com.cloud.vm.NicSecondaryIp; @@ -483,6 +485,8 @@ public interface ResponseGenerator { SSHKeyPairResponse createSSHKeyPairResponse(SSHKeyPair sshkeyPair, boolean privatekey); + UserDataResponse createUserDataResponse(UserData userData); + BackupResponse createBackupResponse(Backup backup); BackupScheduleResponse createBackupScheduleResponse(BackupSchedule backup); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ResetVMUserDataCmdAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ResetVMUserDataCmdAdmin.java new file mode 100644 index 00000000000..83e3481840b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ResetVMUserDataCmdAdmin.java @@ -0,0 +1,31 @@ +package org.apache.cloudstack.api.command.admin.vm; +// 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 com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; +import org.apache.cloudstack.api.response.UserVmResponse; + +@APICommand(name = "resetUserDataForVirtualMachine", responseObject = UserVmResponse.class, description = "Resets the UserData for virtual machine. " + + "The virtual machine must be in a \"Stopped\" state. [async]", responseView = ResponseObject.ResponseView.Full, entityType = {VirtualMachine.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) + +public class ResetVMUserDataCmdAdmin extends ResetVMUserDataCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/DeleteUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/DeleteUserDataCmd.java new file mode 100644 index 00000000000..24b2f5b22a8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/DeleteUserDataCmd.java @@ -0,0 +1,120 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.userdata; + +import com.cloud.user.Account; +import com.cloud.user.UserData; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.UserDataResponse; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; + + +@APICommand(name = "deleteUserData", description = "Deletes a userdata", responseObject = SuccessResponse.class, entityType = {UserData.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.18") +public class DeleteUserDataCmd extends BaseCmd { + + public static final Logger s_logger = Logger.getLogger(DeleteUserDataCmd.class.getName()); + private static final String s_name = "deleteuserdataresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, required = true, entityType = UserDataResponse.class, description = "the ID of the Userdata") + private Long id; + + //Owner information + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "an optional account for the userdata. Must be used with domainId.") + private String accountName; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.UUID, + entityType = DomainResponse.class, + description = "an optional domainId for the userdata. If the account parameter is used, domainId must also be used.") + private Long domainId; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "an optional project for the userdata") + private Long projectId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getAccountName() { + return accountName; + } + + public Long getDomainId() { + return domainId; + } + + public Long getProjectId() { + return projectId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + boolean result = _mgr.deleteUserData(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete userdata"); + } + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + Account account = CallContext.current().getCallingAccount(); + if ((account == null || _accountService.isAdmin(account.getId())) && (domainId != null && accountName != null)) { + Account userAccount = _responseGenerator.findAccountByNameDomain(accountName, domainId); + if (userAccount != null) { + return userAccount.getId(); + } + } + + if (account != null) { + return account.getId(); + } + + return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/LinkUserDataToTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/LinkUserDataToTemplateCmd.java new file mode 100644 index 00000000000..8ae5a10a0fb --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/LinkUserDataToTemplateCmd.java @@ -0,0 +1,130 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.userdata; + +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.user.UserData; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.api.response.UserDataResponse; +import org.apache.log4j.Logger; + +@APICommand(name = "linkUserDataToTemplate", description = "Link or unlink a userdata to a template.", responseObject = TemplateResponse.class, responseView = ResponseObject.ResponseView.Restricted, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.18.0") +public class LinkUserDataToTemplateCmd extends BaseCmd implements AdminCmd { + public static final Logger s_logger = Logger.getLogger(LinkUserDataToTemplateCmd.class.getName()); + + private static final String s_name = "linkuserdatatotemplateresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.TEMPLATE_ID, + type = CommandType.UUID, + entityType = TemplateResponse.class, + description = "the ID of the template for the virtual machine") + private Long templateId; + + @Parameter(name = ApiConstants.ISO_ID, + type = CommandType.UUID, + entityType = TemplateResponse.class, + description = "the ID of the ISO for the virtual machine") + private Long isoId; + + @Parameter(name = ApiConstants.USER_DATA_ID, + type = CommandType.UUID, + entityType = UserDataResponse.class, + description = "the ID of the userdata that has to be linked to template/ISO. If not provided existing userdata will be unlinked from the template/ISO") + private Long userdataId; + + @Parameter(name = ApiConstants.USER_DATA_POLICY, + type = CommandType.STRING, + description = "an optional override policy of the userdata. Possible values are - ALLOWOVERRIDE, APPEND, DENYOVERRIDE. Default policy is allowoverride") + private String userdataPolicy; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getTemplateId() { + return templateId; + } + + public Long getIsoId() { + return isoId; + } + + public Long getUserdataId() { + return userdataId; + } + + public UserData.UserDataOverridePolicy getUserdataPolicy() { + if (userdataPolicy == null) { + return UserData.UserDataOverridePolicy.ALLOWOVERRIDE; + } + return UserData.UserDataOverridePolicy.valueOf(userdataPolicy.toUpperCase()); + } + + @Override + public void execute() { + VirtualMachineTemplate result = null; + try { + result = _templateService.linkUserDataToTemplate(this); + } catch (Exception e) { + throw new CloudRuntimeException(String.format("Failed to link userdata to template, due to: %s", e.getLocalizedMessage()), e); + } + if (result != null) { + TemplateResponse response = _responseGenerator.createTemplateUpdateResponse(getResponseView(), result); + if (getTemplateId() != null) { + response.setObjectName("template"); + } else { + response.setObjectName("iso"); + } + response.setTemplateType(result.getTemplateType().toString());//Template can be either USER or ROUTING type + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to link userdata to template"); + } + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + VirtualMachineTemplate template = _entityMgr.findById(VirtualMachineTemplate.class, getTemplateId()); + if (template != null) { + return template.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmd.java new file mode 100644 index 00000000000..21d4e3b1224 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmd.java @@ -0,0 +1,85 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.userdata; + +import java.util.ArrayList; +import java.util.List; + +import com.cloud.user.UserData; +import org.apache.cloudstack.api.response.UserDataResponse; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ListResponse; + +import com.cloud.utils.Pair; + +@APICommand(name = "listUserData", description = "List registered userdatas", responseObject = UserDataResponse.class, entityType = {UserData.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.18") +public class ListUserDataCmd extends BaseListProjectAndAccountResourcesCmd { + public static final Logger s_logger = Logger.getLogger(ListUserDataCmd.class.getName()); + private static final String s_name = "listuserdataresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserDataResponse.class, description = "the ID of the Userdata") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Userdata name to look for") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + Pair, Integer> resultList = _mgr.listUserDatas(this); + List responses = new ArrayList<>(); + for (UserData result : resultList.first()) { + UserDataResponse r = _responseGenerator.createUserDataResponse(result); + r.setObjectName(ApiConstants.USER_DATA); + responses.add(r); + } + + ListResponse response = new ListResponse<>(); + response.setResponses(responses, resultList.second()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return s_name; + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java new file mode 100644 index 00000000000..bb2e0a3d2a2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java @@ -0,0 +1,148 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.userdata; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.NetworkModel; +import com.cloud.user.UserData; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserDataResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@APICommand(name = "registerUserData", + description = "Register a new userdata.", + since = "4.18", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class RegisterUserDataCmd extends BaseCmd { + + public static final Logger s_logger = Logger.getLogger(RegisterUserDataCmd.class.getName()); + private static final String s_name = "registeruserdataresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Name of the userdata") + private String name; + + //Owner information + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "an optional account for the userdata. Must be used with domainId.") + private String accountName; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.UUID, + entityType = DomainResponse.class, + description = "an optional domainId for the userdata. If the account parameter is used, domainId must also be used.") + private Long domainId; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "an optional project for the userdata") + private Long projectId; + + @Parameter(name = ApiConstants.USER_DATA, type = CommandType.STRING, required = true, description = "Userdata content", length = 1048576) + private String userData; + + @Parameter(name = ApiConstants.PARAMS, type = CommandType.STRING, description = "comma separated list of variables declared in userdata content") + private String params; + + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public String getAccountName() { + return accountName; + } + + public Long getDomainId() { + return domainId; + } + + public Long getProjectId() { + return projectId; + } + + public String getUserData() { + return userData; + } + + public String getParams() { + checkForVRMetadataFileNames(params); + return params; + } + + public void checkForVRMetadataFileNames(String params) { + if (StringUtils.isNotEmpty(params)) { + List keyValuePairs = new ArrayList<>(Arrays.asList(params.split(","))); + keyValuePairs.retainAll(NetworkModel.metadataFileNames); + if (!keyValuePairs.isEmpty()) { + throw new InvalidParameterValueException(String.format("Params passed here have a few virtual router metadata file names %s", keyValuePairs)); + } + } + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + Long accountId = _accountService.finalyzeAccountId(accountName, domainId, projectId, true); + if (accountId == null) { + return CallContext.current().getCallingAccount().getId(); + } + + return accountId; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + UserData result = _mgr.registerUserData(this); + UserDataResponse response = _responseGenerator.createUserDataResponse(result); + response.setResponseName(getCommandName()); + response.setObjectName(ApiConstants.USER_DATA); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return s_name; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index fce7c91710f..c5cb9961f2f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -47,6 +47,7 @@ import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.api.response.UserDataResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; @@ -154,6 +155,13 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG length = 1048576) private String userData; + @Parameter(name = ApiConstants.USER_DATA_ID, type = CommandType.UUID, entityType = UserDataResponse.class, description = "the ID of the Userdata", since = "4.18") + private Long userdataId; + + @Parameter(name = ApiConstants.USER_DATA_DETAILS, type = CommandType.MAP, description = "used to specify the parameters values for the variables in userdata.", since = "4.18") + private Map userdataDetails; + + @Deprecated @Parameter(name = ApiConstants.SSH_KEYPAIR, type = CommandType.STRING, description = "name of the ssh key pair used to login to the virtual machine") private String sshKeyPairName; @@ -413,6 +421,25 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG return userData; } + public Long getUserdataId() { + return userdataId; + } + + public Map getUserdataDetails() { + Map userdataDetailsMap = new HashMap(); + if (userdataDetails != null && userdataDetails.size() != 0) { + Collection parameterCollection = userdataDetails.values(); + Iterator iter = parameterCollection.iterator(); + while (iter.hasNext()) { + HashMap value = (HashMap)iter.next(); + for (Map.Entry entry: value.entrySet()) { + userdataDetailsMap.put(entry.getKey(),entry.getValue()); + } + } + } + return userdataDetailsMap; + } + public Long getZoneId() { return zoneId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java new file mode 100644 index 00000000000..ad0592ca4ac --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java @@ -0,0 +1,173 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.vm; + +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.UserDataResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.log4j.Logger; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +@APICommand(name = "resetUserDataForVirtualMachine", responseObject = UserVmResponse.class, description = "Resets the UserData for virtual machine. " + + "The virtual machine must be in a \"Stopped\" state.", responseView = ResponseObject.ResponseView.Restricted, entityType = {VirtualMachine.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, since = "4.18.0") +public class ResetVMUserDataCmd extends BaseCmd implements UserCmd { + + public static final Logger s_logger = Logger.getLogger(ResetVMUserDataCmd.class.getName()); + + private static final String s_name = "resetuserdataforvirtualmachineresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserVmResponse.class, required = true, description = "The ID of the virtual machine") + private Long id; + + @Parameter(name = ApiConstants.USER_DATA, + type = CommandType.STRING, + description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. " + + "This binary data must be base64 encoded before adding it to the request. " + + "Using HTTP GET (via querystring), you can send up to 4KB of data after base64 encoding. " + + "Using HTTP POST(via POST body), you can send up to 1MB of data after base64 encoding." + + "You also need to change vm.userdata.max.length value", + length = 1048576) + private String userData; + + @Parameter(name = ApiConstants.USER_DATA_ID, type = CommandType.UUID, entityType = UserDataResponse.class, description = "the ID of the userdata") + private Long userdataId; + + @Parameter(name = ApiConstants.USER_DATA_DETAILS, type = CommandType.MAP, description = "used to specify the parameters values for the variables in userdata.") + private Map userdataDetails; + + //Owner information + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "an optional account for the virtual machine. Must be used with domainId.") + private String accountName; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.UUID, + entityType = DomainResponse.class, + description = "an optional domainId for the virtual machine. If the account parameter is used, domainId must also be used.") + private Long domainId; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "an optional project for the virtual machine") + private Long projectId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getAccountName() { + return accountName; + } + + public Long getDomainId() { + return domainId; + } + + public Long getProjectId() { + return projectId; + } + public String getUserData() { + return userData; + } + + public Long getUserdataId() { + return userdataId; + } + + public Map getUserdataDetails() { + Map userdataDetailsMap = new HashMap(); + if (userdataDetails != null && userdataDetails.size() != 0) { + Collection parameterCollection = userdataDetails.values(); + Iterator iter = parameterCollection.iterator(); + while (iter.hasNext()) { + HashMap value = (HashMap)iter.next(); + for (Map.Entry entry: value.entrySet()) { + userdataDetailsMap.put(entry.getKey(),entry.getValue()); + } + } + } + return userdataDetailsMap; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.VirtualMachine; + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + UserVm vm = _responseGenerator.findUserVmById(getId()); + if (vm != null) { + return vm.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked + } + + @Override + public Long getApiResourceId() { + return getId(); + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException { + + CallContext.current().setEventDetails("Vm Id: " + getId()); + UserVm result = _userVmService.resetVMUserData(this); + + if (result != null) { + UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), "virtualmachine", result).get(0); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to reset vm SSHKey"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java index cc0d7afacdd..3e0ef75ecdd 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java @@ -18,9 +18,14 @@ package org.apache.cloudstack.api.command.user.vm; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.response.UserDataResponse; +import org.apache.log4j.Logger; + import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; @@ -37,13 +42,11 @@ import org.apache.cloudstack.api.response.GuestOSResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.context.CallContext; -import org.apache.log4j.Logger; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.user.Account; import com.cloud.uservm.UserVm; -import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.Dhcp; import com.cloud.vm.VirtualMachine; @@ -90,6 +93,12 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction, since = "4.16.0") private String userData; + @Parameter(name = ApiConstants.USER_DATA_ID, type = CommandType.UUID, entityType = UserDataResponse.class, description = "the ID of the userdata", since = "4.18") + private Long userdataId; + + @Parameter(name = ApiConstants.USER_DATA_DETAILS, type = CommandType.MAP, description = "used to specify the parameters values for the variables in userdata.", since = "4.18") + private Map userdataDetails; + @Parameter(name = ApiConstants.DISPLAY_VM, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the vm to the end user or not.", authorized = {RoleType.Admin}) private Boolean displayVm; @@ -162,6 +171,25 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction, return userData; } + public Long getUserdataId() { + return userdataId; + } + + public Map getUserdataDetails() { + Map userdataDetailsMap = new HashMap(); + if (userdataDetails != null && userdataDetails.size() != 0) { + Collection parameterCollection = userdataDetails.values(); + Iterator iter = parameterCollection.iterator(); + while (iter.hasNext()) { + HashMap value = (HashMap)iter.next(); + for (Map.Entry entry: value.entrySet()) { + userdataDetailsMap.put(entry.getKey(),entry.getValue()); + } + } + } + return userdataDetailsMap; + } + public Boolean getDisplayVm() { return displayVm; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java index 892b5b85262..bd09d098708 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java @@ -227,6 +227,18 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements @Param(description = "Base64 string representation of the resource icon", since = "4.16.0.0") ResourceIconResponse icon; + @SerializedName(ApiConstants.USER_DATA_ID) @Param(description="the id of userdata linked to this template", since = "4.18.0") + private String userDataId; + + @SerializedName(ApiConstants.USER_DATA_NAME) @Param(description="the name of userdata linked to this template", since = "4.18.0") + private String userDataName; + + @SerializedName(ApiConstants.USER_DATA_POLICY) @Param(description="the userdata override policy with the userdata provided while deploying VM", since = "4.18.0") + private String userDataPolicy; + + @SerializedName(ApiConstants.USER_DATA_PARAMS) @Param(description="list of parameters which contains the list of keys or string parameters that are needed to be passed for any variables declared in userdata", since = "4.18.0") + private String userDataParams; + public TemplateResponse() { tags = new LinkedHashSet<>(); } @@ -467,4 +479,36 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements public void setResourceIconResponse(ResourceIconResponse icon) { this.icon = icon; } + + public String getUserDataId() { + return userDataId; + } + + public void setUserDataId(String userDataId) { + this.userDataId = userDataId; + } + + public String getUserDataName() { + return userDataName; + } + + public void setUserDataName(String userDataName) { + this.userDataName = userDataName; + } + + public String getUserDataPolicy() { + return userDataPolicy; + } + + public void setUserDataPolicy(String userDataPolicy) { + this.userDataPolicy = userDataPolicy; + } + + public String getUserDataParams() { + return userDataParams; + } + + public void setUserDataParams(String userDataParams) { + this.userDataParams = userDataParams; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserDataResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserDataResponse.java new file mode 100644 index 00000000000..bbe27f84520 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserDataResponse.java @@ -0,0 +1,128 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.cloud.user.UserData; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponseWithAnnotations; +import org.apache.cloudstack.api.EntityReference; + +@EntityReference(value = UserData.class) +public class UserDataResponse extends BaseResponseWithAnnotations { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the ssh keypair") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the userdata") + private String name; + + @SerializedName(ApiConstants.ACCOUNT_ID) @Param(description="the owner id of the userdata") + private String accountId; + + @SerializedName(ApiConstants.ACCOUNT) @Param(description="the owner of the userdata") + private String accountName; + + @SerializedName(ApiConstants.DOMAIN_ID) @Param(description="the domain id of the userdata owner") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) @Param(description="the domain name of the userdata owner") + private String domain; + + @SerializedName(ApiConstants.USER_DATA) @Param(description="base64 encoded userdata content") + private String userData; + + @SerializedName(ApiConstants.PARAMS) @Param(description="list of parameters which contains the list of keys or string parameters that are needed to be passed for any variables declared in userdata") + private String params; + + public UserDataResponse() { + } + + public UserDataResponse(String id, String name, String userData, String params) { + this.id = id; + this.name = name; + this.userData = userData; + this.params = params; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getDomainId() { + return domainId; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public String getUserData() { + return userData; + } + + public void setUserData(String userData) { + this.userData = userData; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public String getDomainName() { + return domain; + } + + public void setDomainName(String domain) { + this.domain = domain; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index 3483c17f474..17b577bf4cb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -332,6 +332,18 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "Base64 string representation of the resource icon", since = "4.16.0.0") ResourceIconResponse resourceIconResponse; + @SerializedName(ApiConstants.USER_DATA_ID) @Param(description="the id of userdata used for the VM", since = "4.18.0") + private String userDataId; + + @SerializedName(ApiConstants.USER_DATA_NAME) @Param(description="the name of userdata used for the VM", since = "4.18.0") + private String userDataName; + + @SerializedName(ApiConstants.USER_DATA_POLICY) @Param(description="the userdata override policy with the userdata provided while deploying VM", since = "4.18.0") + private String userDataPolicy; + + @SerializedName(ApiConstants.USER_DATA_DETAILS) @Param(description="list of variables and values for the variables declared in userdata", since = "4.18.0") + private String userDataDetails; + public UserVmResponse() { securityGroupList = new LinkedHashSet(); nics = new TreeSet<>(Comparator.comparingInt(x -> Integer.parseInt(x.getDeviceId()))); @@ -952,4 +964,37 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co public void setBytesSent(Long bytesSent) { this.bytesSent = bytesSent; } + + public String getUserDataId() { + return userDataId; + } + + public void setUserDataId(String userDataId) { + this.userDataId = userDataId; + } + + public String getUserDataName() { + return userDataName; + } + + public void setUserDataName(String userDataName) { + this.userDataName = userDataName; + } + + public String getUserDataPolicy() { + return userDataPolicy; + } + + public void setUserDataPolicy(String userDataPolicy) { + this.userDataPolicy = userDataPolicy; + } + + public String getUserDataDetails() { + return userDataDetails; + } + + public void setUserDataDetails(String userDataDetails) { + this.userDataDetails = userDataDetails; + } + } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/ResetVMUserDataCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/ResetVMUserDataCmdTest.java new file mode 100644 index 00000000000..e975138420a --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/ResetVMUserDataCmdTest.java @@ -0,0 +1,138 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.test; + +import com.cloud.user.AccountService; +import com.cloud.uservm.UserVm; +import com.cloud.vm.UserVmService; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(CallContext.class) +@PowerMockIgnore({"javax.xml.*", "org.w3c.dom.*", "org.apache.xerces.*", "org.xml.*"}) +public class ResetVMUserDataCmdTest { + + @InjectMocks + ResetVMUserDataCmd cmd = new ResetVMUserDataCmd(); + + @Mock + AccountService _accountService; + + @Mock + ResponseGenerator _responseGenerator; + + @Mock + UserVmService _userVmService; + + private static final long DOMAIN_ID = 5L; + private static final long PROJECT_ID = 10L; + private static final String ACCOUNT_NAME = "user"; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + ReflectionTestUtils.setField(cmd, "accountName", ACCOUNT_NAME); + ReflectionTestUtils.setField(cmd, "domainId", DOMAIN_ID); + ReflectionTestUtils.setField(cmd, "projectId", PROJECT_ID); + } + + @Test + public void testValidResetVMUserDataExecute() { + UserVm result = Mockito.mock(UserVm.class); + + UserVmResponse response = new UserVmResponse(); + List responseList = new ArrayList<>(); + responseList.add(response); + Mockito.doReturn(responseList).when(_responseGenerator).createUserVmResponse(ResponseObject.ResponseView.Restricted, "virtualmachine", result); + + try { + Mockito.doReturn(result).when(_userVmService).resetVMUserData(cmd); + cmd.execute(); + } catch (Exception e) { + e.printStackTrace(); + } + Assert.assertEquals(response, cmd.getResponseObject()); + Assert.assertEquals("resetuserdataforvirtualmachineresponse", response.getResponseName()); + } + + @Test + public void validateArgsCmd() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + PowerMockito.when(CallContext.current()).thenReturn(callContextMock); + + ReflectionTestUtils.setField(cmd, "id", 1L); + ReflectionTestUtils.setField(cmd, "userdataId", 2L); + ReflectionTestUtils.setField(cmd, "userData", "testUserdata"); + + UserVm vm = Mockito.mock(UserVm.class); + when(_responseGenerator.findUserVmById(1L)).thenReturn(vm); + when(vm.getAccountId()).thenReturn(200L); + + Assert.assertEquals(1L, (long)cmd.getId()); + Assert.assertEquals(2L, (long)cmd.getUserdataId()); + Assert.assertEquals("testUserdata", cmd.getUserData()); + Assert.assertEquals(200L, cmd.getEntityOwnerId()); + } + + @Test + public void testUserdataDetails() { + Map values1 = new HashMap<>(); + values1.put("key1", "value1"); + values1.put("key2", "value2"); + + Map values2 = new HashMap<>(); + values1.put("key3", "value3"); + values1.put("key4", "value4"); + + Map> userdataDetails = new HashMap<>(); + userdataDetails.put(0, values1); + userdataDetails.put(1, values2); + + ReflectionTestUtils.setField(cmd, "userdataDetails", userdataDetails); + + Map result = cmd.getUserdataDetails(); + + values1.putAll(values2); + Assert.assertEquals(values1.toString(), result.toString()); + } + +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/DeleteUserDataCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/DeleteUserDataCmdTest.java new file mode 100644 index 00000000000..fbaf46f1a28 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/DeleteUserDataCmdTest.java @@ -0,0 +1,98 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.userdata; + +import com.cloud.server.ManagementService; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(CallContext.class) +@PowerMockIgnore({"javax.xml.*", "org.w3c.dom.*", "org.apache.xerces.*", "org.xml.*"}) +public class DeleteUserDataCmdTest { + + @InjectMocks + DeleteUserDataCmd cmd = new DeleteUserDataCmd(); + + @Mock + AccountService _accountService; + @Mock + ManagementService _mgr; + + private static final long DOMAIN_ID = 5L; + private static final long PROJECT_ID = 10L; + private static final String ACCOUNT_NAME = "user"; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + ReflectionTestUtils.setField(cmd, "accountName", ACCOUNT_NAME); + ReflectionTestUtils.setField(cmd, "domainId", DOMAIN_ID); + ReflectionTestUtils.setField(cmd, "projectId", PROJECT_ID); + } + + @Test + public void testValidUserDataExecute() { + Mockito.doReturn(true).when(_mgr).deleteUserData(cmd); + + try { + cmd.execute(); + } catch (Exception e) { + e.printStackTrace(); + } + Assert.assertEquals(cmd.getResponseObject().getClass(), SuccessResponse.class); + } + + @Test(expected = ServerApiException.class) + public void testDeleteFailure() { + Mockito.doReturn(false).when(_mgr).deleteUserData(cmd); + cmd.execute(); + } + + @Test + public void validateArgsCmd() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + PowerMockito.when(CallContext.current()).thenReturn(callContextMock); + Account accountMock = PowerMockito.mock(Account.class); + PowerMockito.when(callContextMock.getCallingAccount()).thenReturn(accountMock); + Mockito.when(accountMock.getId()).thenReturn(2L); + Mockito.doReturn(false).when(_accountService).isAdmin(2L); + + ReflectionTestUtils.setField(cmd, "id", 1L); + + Assert.assertEquals(1L, (long)cmd.getId()); + Assert.assertEquals(2L, cmd.getEntityOwnerId()); + } + +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/LinkUserDataToTemplateCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/LinkUserDataToTemplateCmdTest.java new file mode 100644 index 00000000000..c5581ed5a11 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/LinkUserDataToTemplateCmdTest.java @@ -0,0 +1,109 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.userdata; + +import com.cloud.storage.Storage; +import com.cloud.template.TemplateApiService; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.user.UserData; +import com.cloud.utils.db.EntityManager; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.context.CallContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(CallContext.class) +@PowerMockIgnore({"javax.xml.*", "org.w3c.dom.*", "org.apache.xerces.*", "org.xml.*"}) +public class LinkUserDataToTemplateCmdTest { + + @Mock + private ResponseGenerator _responseGenerator; + + @Mock + private EntityManager _entityMgr; + + @InjectMocks + LinkUserDataToTemplateCmd cmd = new LinkUserDataToTemplateCmd(); + + @Mock + TemplateApiService _templateService; + + @Mock + VirtualMachineTemplate virtualMachineTemplate; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testValidIds() { + ReflectionTestUtils.setField(cmd, "userdataId", 1L); + ReflectionTestUtils.setField(cmd, "templateId", 1L); + TemplateResponse response = Mockito.mock(TemplateResponse.class); + Mockito.doReturn(virtualMachineTemplate).when(_templateService).linkUserDataToTemplate(cmd); + Mockito.doReturn(Storage.TemplateType.USER).when(virtualMachineTemplate).getTemplateType(); + Mockito.doReturn(response).when(_responseGenerator).createTemplateUpdateResponse(cmd.getResponseView(), virtualMachineTemplate); + + try { + cmd.execute(); + } catch (Exception e) { + e.printStackTrace(); + } + Assert.assertEquals(response, cmd.getResponseObject()); + } + + @Test + public void validateArgsCmd() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + PowerMockito.when(CallContext.current()).thenReturn(callContextMock); + Account accountMock = PowerMockito.mock(Account.class); + PowerMockito.when(callContextMock.getCallingAccount()).thenReturn(accountMock); + Mockito.when(accountMock.getId()).thenReturn(2L); + ReflectionTestUtils.setField(cmd, "templateId", 1L); + ReflectionTestUtils.setField(cmd, "userdataId", 3L); + + Mockito.doReturn(virtualMachineTemplate).when(_entityMgr).findById(VirtualMachineTemplate.class, cmd.getTemplateId()); + PowerMockito.when(virtualMachineTemplate.getAccountId()).thenReturn(1L); + + Assert.assertEquals(1L, (long)cmd.getTemplateId()); + Assert.assertEquals(3L, (long)cmd.getUserdataId()); + Assert.assertEquals(1L, cmd.getEntityOwnerId()); + } + + @Test + public void testDefaultOverridePolicy() { + Assert.assertEquals(UserData.UserDataOverridePolicy.ALLOWOVERRIDE, cmd.getUserdataPolicy()); + } + +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmdTest.java new file mode 100644 index 00000000000..fd3b1a7a320 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmdTest.java @@ -0,0 +1,89 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.userdata; + +import com.cloud.server.ManagementService; +import com.cloud.user.UserData; +import com.cloud.utils.Pair; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Assert; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.response.UserDataResponse; +import org.apache.cloudstack.context.CallContext; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(CallContext.class) +@PowerMockIgnore({"javax.xml.*", "org.w3c.dom.*", "org.apache.xerces.*", "org.xml.*"}) +public class ListUserDataCmdTest { + + @InjectMocks + ListUserDataCmd cmd = new ListUserDataCmd(); + + @Mock + ManagementService _mgr; + + @Mock + ResponseGenerator _responseGenerator; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testListSuccess() { + UserData userData = Mockito.mock(UserData.class); + List userDataList = new ArrayList(); + userDataList.add(userData); + Pair, Integer> result = new Pair, Integer>(userDataList, 1); + UserDataResponse userDataResponse = Mockito.mock(UserDataResponse.class); + + Mockito.when(_mgr.listUserDatas(cmd)).thenReturn(result); + Mockito.when(_responseGenerator.createUserDataResponse(userData)).thenReturn(userDataResponse); + + cmd.execute(); + + ListResponse actualResponse = (ListResponse)cmd.getResponseObject(); + Assert.assertEquals(userDataResponse, actualResponse.getResponses().get(0)); + } + + @Test + public void testEmptyList() { + List userDataList = new ArrayList(); + Pair, Integer> result = new Pair, Integer>(userDataList, 0); + + Mockito.when(_mgr.listUserDatas(cmd)).thenReturn(result); + + cmd.execute(); + + ListResponse actualResponse = (ListResponse)cmd.getResponseObject(); + Assert.assertEquals(new ArrayList<>(), actualResponse.getResponses()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmdTest.java new file mode 100644 index 00000000000..901ec8477fb --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmdTest.java @@ -0,0 +1,113 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.userdata; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.server.ManagementService; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.user.UserData; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.response.UserDataResponse; +import org.apache.cloudstack.context.CallContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(CallContext.class) +@PowerMockIgnore({"javax.xml.*", "org.w3c.dom.*", "org.apache.xerces.*", "org.xml.*"}) +public class RegisterUserDataCmdTest { + + @InjectMocks + RegisterUserDataCmd cmd = new RegisterUserDataCmd(); + + @Mock + AccountService _accountService; + + @Mock + ResponseGenerator _responseGenerator; + + @Mock + ManagementService _mgr; + + private static final long DOMAIN_ID = 5L; + private static final long PROJECT_ID = 10L; + private static final String ACCOUNT_NAME = "user"; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + ReflectionTestUtils.setField(cmd, "accountName", ACCOUNT_NAME); + ReflectionTestUtils.setField(cmd, "domainId", DOMAIN_ID); + ReflectionTestUtils.setField(cmd, "projectId", PROJECT_ID); + } + + @Test + public void testValidUserDataExecute() { + UserData result = Mockito.mock(UserData.class); + Mockito.doReturn(result).when(_mgr).registerUserData(cmd); + + UserDataResponse response = Mockito.mock(UserDataResponse.class); + Mockito.doReturn(response).when(_responseGenerator).createUserDataResponse(result); + + try { + cmd.execute(); + } catch (Exception e) { + e.printStackTrace(); + } + Assert.assertEquals(response, cmd.getResponseObject()); + } + + @Test + public void validateArgsCmd() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + PowerMockito.when(CallContext.current()).thenReturn(callContextMock); + Account accountMock = PowerMockito.mock(Account.class); + PowerMockito.when(callContextMock.getCallingAccount()).thenReturn(accountMock); + Mockito.when(accountMock.getId()).thenReturn(2L); + ReflectionTestUtils.setField(cmd, "name", "testUserdataName"); + ReflectionTestUtils.setField(cmd, "userData", "testUserdata"); + + when(_accountService.finalyzeAccountId(ACCOUNT_NAME, DOMAIN_ID, PROJECT_ID, true)).thenReturn(200L); + + Assert.assertEquals("testUserdataName", cmd.getName()); + Assert.assertEquals("testUserdata", cmd.getUserData()); + Assert.assertEquals(200L, cmd.getEntityOwnerId()); + } + + @Test(expected = InvalidParameterValueException.class) + public void validateIfUserdataParamsHaveMetadataFileNames() { + // If the userdata params have any key matched to the VR metadata file names, then it will throw exception + ReflectionTestUtils.setField(cmd, "params", "key1,key2,key3,vm-id"); + cmd.getParams(); + } + +} diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateInfo.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateInfo.java index cc8e111d549..1d49e19d2da 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateInfo.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateInfo.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.engine.subsystem.api.storage; import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.UserData; public interface TemplateInfo extends DataObject, VirtualMachineTemplate { @Override @@ -33,4 +34,8 @@ public interface TemplateInfo extends DataObject, VirtualMachineTemplate { boolean isDeployAsIs(); String getDeployAsIsConfiguration(); + + Long getUserDataId(); + + UserData.UserDataOverridePolicy getUserDataOverridePolicy(); } 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 11364495e70..890197f302c 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -3221,7 +3221,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac if (_networkModel.isSharedNetworkWithoutServices(network.getId())) { final String serviceOffering = _serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId()).getDisplayText(); boolean isWindows = _guestOSCategoryDao.findById(_guestOSDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows"); - vmData = _networkModel.generateVmData(userVm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), + vmData = _networkModel.generateVmData(userVm.getUserData(), userVm.getUserDataDetails(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), vm.getUuid(), defaultNic.getMacAddress(), userVm.getDetail("SSH.PublicKey"), (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows, VirtualMachineManager.getHypervisorHostname(destination.getHost() != null ? destination.getHost().getName() : "")); String vmName = vm.getInstanceName(); diff --git a/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java b/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java index 61df40e50d8..2aeb8314eb2 100644 --- a/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java +++ b/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java @@ -31,6 +31,8 @@ import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; +import com.cloud.user.UserData; + import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.Storage.TemplateType; @@ -155,6 +157,13 @@ public class VMTemplateVO implements VirtualMachineTemplate { @Column(name = "deploy_as_is") private boolean deployAsIs; + @Column(name = "user_data_id") + private Long userDataId; + + @Column(name = "user_data_link_policy") + @Enumerated(value = EnumType.STRING) + UserData.UserDataOverridePolicy userDataLinkPolicy; + @Override public String getUniqueName() { return uniqueName; @@ -648,4 +657,23 @@ public class VMTemplateVO implements VirtualMachineTemplate { public void setDeployAsIs(boolean deployAsIs) { this.deployAsIs = deployAsIs; } + + @Override + public Long getUserDataId() { + return userDataId; + } + + public void setUserDataId(Long userDataId) { + this.userDataId = userDataId; + } + + @Override + public UserData.UserDataOverridePolicy getUserDataOverridePolicy() { + return userDataLinkPolicy; + } + + public void setUserDataLinkPolicy(UserData.UserDataOverridePolicy userDataLinkPolicy) { + this.userDataLinkPolicy = userDataLinkPolicy; + } + } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java index 63221e745d8..b1d7f21b4f0 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java @@ -86,4 +86,6 @@ public interface VMTemplateDao extends GenericDao, StateDao< List listByParentTemplatetId(long parentTemplatetId); VMTemplateVO findLatestTemplateByName(String name); + + List findTemplatesLinkedToUserdata(long userdataId); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java index 74d210be0de..08b98f9869e 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java @@ -98,7 +98,7 @@ public class VMTemplateDaoImpl extends GenericDaoBase implem protected SearchBuilder ParentTemplateIdSearch; private SearchBuilder InactiveUnremovedTmpltSearch; private SearchBuilder LatestTemplateByHypervisorTypeSearch; - + private SearchBuilder userDataSearch; @Inject ResourceTagDao _tagsDao; @@ -422,6 +422,11 @@ public class VMTemplateDaoImpl extends GenericDaoBase implem InactiveUnremovedTmpltSearch.and("removed", InactiveUnremovedTmpltSearch.entity().getRemoved(), SearchCriteria.Op.NULL); InactiveUnremovedTmpltSearch.done(); + userDataSearch = createSearchBuilder(); + userDataSearch.and("userDataId", userDataSearch.entity().getUserDataId(), SearchCriteria.Op.EQ); + userDataSearch.and("state", userDataSearch.entity().getState(), SearchCriteria.Op.EQ); + userDataSearch.done(); + return result; } @@ -629,12 +634,20 @@ public class VMTemplateDaoImpl extends GenericDaoBase implem } @Override - public List listUnRemovedTemplatesByStates(VirtualMachineTemplate.State ...states) { + public List listUnRemovedTemplatesByStates(VirtualMachineTemplate.State ...states) { SearchCriteria sc = InactiveUnremovedTmpltSearch.create(); sc.setParameters("state", (Object[]) states); return listBy(sc); } + @Override + public List findTemplatesLinkedToUserdata(long userdataId) { + SearchCriteria sc = userDataSearch.create(); + sc.setParameters("userDataId", userdataId); + sc.setParameters("state", VirtualMachineTemplate.State.Active.toString()); + return listBy(sc); + } + @Override @DB public boolean remove(Long id) { diff --git a/engine/schema/src/main/java/com/cloud/user/UserDataVO.java b/engine/schema/src/main/java/com/cloud/user/UserDataVO.java new file mode 100644 index 00000000000..f54b1a8872e --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/user/UserDataVO.java @@ -0,0 +1,120 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.user; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.UUID; + +@Entity +@Table(name = "user_data") +public class UserDataVO implements UserData { + + public UserDataVO() { + uuid = UUID.randomUUID().toString(); + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id = null; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "name") + private String name; + + @Column(name = "user_data", updatable = true, length = 1048576) + @Basic(fetch = FetchType.LAZY) + private String userData; + + @Column(name = "params", length = 4096) + private String params; + + @Override + public long getDomainId() { + return domainId; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public Class getEntityType() { + return UserDataVO.class; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUserData() { + return userData; + } + + @Override + public String getParams() { + return params; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + public void setName(String name) { + this.name = name; + } + + public void setUserData(String userData) { + this.userData = userData; + } + + public void setParams(String params) { + this.params = params; + } +} diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserDataDao.java b/engine/schema/src/main/java/com/cloud/user/dao/UserDataDao.java new file mode 100644 index 00000000000..f012d41db5e --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserDataDao.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.user.dao; + +import com.cloud.user.UserDataVO; +import com.cloud.utils.db.GenericDao; + +public interface UserDataDao extends GenericDao { + + public UserDataVO findByUserData(long accountId, long domainId, String userData); + + public UserDataVO findByName(long accountId, long domainId, String name); + +} diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserDataDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/UserDataDaoImpl.java new file mode 100644 index 00000000000..416c4418a57 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserDataDaoImpl.java @@ -0,0 +1,66 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.user.dao; + +import com.cloud.user.UserDataVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.springframework.stereotype.Component; + +@Component +public class UserDataDaoImpl extends GenericDaoBase implements UserDataDao { + + private final SearchBuilder userdataSearch; + private final SearchBuilder userdataByNameSearch; + + public UserDataDaoImpl() { + super(); + + userdataSearch = createSearchBuilder(); + userdataSearch.and("accountId", userdataSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + userdataSearch.and("domainId", userdataSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + userdataSearch.and("userData", userdataSearch.entity().getUserData(), SearchCriteria.Op.EQ); + userdataSearch.done(); + + userdataByNameSearch = createSearchBuilder(); + userdataByNameSearch.and("accountId", userdataByNameSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + userdataByNameSearch.and("domainId", userdataByNameSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + userdataByNameSearch.and("name", userdataByNameSearch.entity().getName(), SearchCriteria.Op.EQ); + userdataByNameSearch.done(); + + } + @Override + public UserDataVO findByUserData(long accountId, long domainId, String userData) { + SearchCriteria sc = userdataSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("domainId", domainId); + sc.setParameters("userData", userData); + + return findOneBy(sc); + } + + @Override + public UserDataVO findByName(long accountId, long domainId, String name) { + SearchCriteria sc = userdataByNameSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("domainId", domainId); + sc.setParameters("name", name); + + return findOneBy(sc); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/UserVmVO.java b/engine/schema/src/main/java/com/cloud/vm/UserVmVO.java index 6733a878595..6be995be499 100644 --- a/engine/schema/src/main/java/com/cloud/vm/UserVmVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/UserVmVO.java @@ -44,6 +44,12 @@ public class UserVmVO extends VMInstanceVO implements UserVm { @Basic(fetch = FetchType.LAZY) private String userData; + @Column(name = "user_data_id", nullable = true) + private Long userDataId = null; + + @Column(name = "user_data_details", updatable = true, length = 4096) + private String userDataDetails; + @Column(name = "display_name", updatable = true, nullable = true) private String displayName; @@ -75,9 +81,11 @@ public class UserVmVO extends VMInstanceVO implements UserVm { } public UserVmVO(long id, String instanceName, String displayName, long templateId, HypervisorType hypervisorType, long guestOsId, boolean haEnabled, - boolean limitCpuUse, long domainId, long accountId, long userId, long serviceOfferingId, String userData, String name, Long diskOfferingId) { + boolean limitCpuUse, long domainId, long accountId, long userId, long serviceOfferingId, String userData, Long userDataId, String userDataDetails, String name, Long diskOfferingId) { super(id, serviceOfferingId, name, instanceName, Type.User, templateId, hypervisorType, guestOsId, domainId, accountId, userId, haEnabled, limitCpuUse, diskOfferingId); this.userData = userData; + this.userDataId = userDataId; + this.userDataDetails = userDataDetails; this.displayName = displayName; this.details = new HashMap(); } @@ -100,6 +108,16 @@ public class UserVmVO extends VMInstanceVO implements UserVm { return userData; } + @Override + public void setUserDataId(Long userDataId) { + this.userDataId = userDataId; + } + + @Override + public Long getUserDataId() { + return userDataId; + } + @Override public String getDisplayName() { return displayName; @@ -147,4 +165,14 @@ public class UserVmVO extends VMInstanceVO implements UserVm { public String getDisplayNameOrHostName() { return StringUtils.isNotBlank(displayName) ? displayName : getHostName(); } + + @Override + public String getUserDataDetails() { + return userDataDetails; + } + + @Override + public void setUserDataDetails(String userDataDetails) { + this.userDataDetails = userDataDetails; + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java index 55d8fa02db1..26849081215 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java @@ -42,14 +42,17 @@ public interface UserVmDao extends GenericDao { /** * Updates display name and group for vm; enables/disables ha - * @param id vm id. - * @param userData updates the userData of the vm - * @param displayVm updates the displayvm attribute signifying whether it has to be displayed to the end user or not. + * + * @param id vm id. + * @param userData updates the userData of the vm + * @param userDataId + * @param userDataDetails + * @param displayVm updates the displayvm attribute signifying whether it has to be displayed to the end user or not. * @param customId - * @param hostName TODO + * @param hostName TODO * @param instanceName */ - void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, boolean displayVm, boolean isDynamicallyScalable, String customId, String hostName, String instanceName); + void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, Long userDataId, String userDataDetails, boolean displayVm, boolean isDynamicallyScalable, String customId, String hostName, String instanceName); List findDestroyedVms(Date date); @@ -86,4 +89,7 @@ public interface UserVmDao extends GenericDao { List listByIsoId(Long isoId); List, Pair>> getVmsDetailByNames(Set vmNames, String detail); + + List findByUserDataId(long userdataId); + } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java index a903c3310bb..7501bf49433 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java @@ -78,6 +78,8 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use protected SearchBuilder UserVmSearch; protected SearchBuilder UserVmByIsoSearch; + + protected SearchBuilder listByUserdataId; protected Attribute _updateTimeAttr; // ResourceTagsDaoImpl _tagsDao = ComponentLocator.inject(ResourceTagsDaoImpl.class); @Inject @@ -205,6 +207,10 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use UserVmByIsoSearch.and("isoId", UserVmByIsoSearch.entity().getIsoId(), SearchCriteria.Op.EQ); UserVmByIsoSearch.done(); + listByUserdataId = createSearchBuilder(); + listByUserdataId.and("userDataId", listByUserdataId.entity().getUserDataId(), SearchCriteria.Op.EQ); + listByUserdataId.done(); + _updateTimeAttr = _allAttributes.get("updateTime"); assert _updateTimeAttr != null : "Couldn't get this updateTime attribute"; } @@ -228,13 +234,15 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use } @Override - public void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, boolean displayVm, - boolean isDynamicallyScalable, String customId, String hostName, String instanceName) { + public void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, Long userDataId, String userDataDetails, boolean displayVm, + boolean isDynamicallyScalable, String customId, String hostName, String instanceName) { UserVmVO vo = createForUpdate(); vo.setDisplayName(displayName); vo.setHaEnabled(enable); vo.setGuestOSId(osTypeId); vo.setUserData(userData); + vo.setUserDataId(userDataId); + vo.setUserDataDetails(userDataDetails); vo.setDisplayVm(displayVm); vo.setDynamicallyScalable(isDynamicallyScalable); if (hostName != null) { @@ -697,4 +705,11 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use return vmsDetailByNames; } + + @Override + public List findByUserDataId(long userdataId) { + SearchCriteria sc = listByUserdataId.create(); + sc.setParameters("userDataId", userdataId); + return listBy(sc); + } } diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 0cbf364e670..860dcbac627 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -180,6 +180,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41600to41610.sql b/engine/schema/src/main/resources/META-INF/db/schema-41600to41610.sql index c4d81eb8a3a..4d0f3f20803 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41600to41610.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41600to41610.sql @@ -399,3 +399,329 @@ CREATE TABLE `cloud`.`resource_reservation` ( `amount` bigint unsigned NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `cloud`.`user_data` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40) NOT NULL COMMENT 'UUID of the user data', + `name` varchar(256) NOT NULL COMMENT 'name of the user data', + `account_id` bigint unsigned NOT NULL COMMENT 'owner, foreign key to account table', + `domain_id` bigint unsigned NOT NULL COMMENT 'domain, foreign key to domain table', + `user_data` mediumtext COMMENT 'value of the userdata', + `params` mediumtext COMMENT 'value of the comma-separated list of parameters', + PRIMARY KEY (`id`), + CONSTRAINT `fk_userdata__account_id` FOREIGN KEY(`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_userdata__domain_id` FOREIGN KEY(`domain_id`) REFERENCES `domain` (`id`) ON DELETE CASCADE, + CONSTRAINT `uc_userdata__uuid` UNIQUE (`uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE `cloud`.`user_vm` ADD COLUMN `user_data_id` bigint unsigned DEFAULT NULL COMMENT 'id of the user data' AFTER `user_data`; +ALTER TABLE `cloud`.`user_vm` ADD COLUMN `user_data_details` mediumtext DEFAULT NULL COMMENT 'value of the comma-separated list of parameters' AFTER `user_data_id`; +ALTER TABLE `cloud`.`user_vm` ADD CONSTRAINT `fk_user_vm__user_data_id` FOREIGN KEY `fk_user_vm__user_data_id`(`user_data_id`) REFERENCES `user_data`(`id`); + +ALTER TABLE `cloud`.`vm_template` ADD COLUMN `user_data_id` bigint unsigned DEFAULT NULL COMMENT 'id of the user data'; +ALTER TABLE `cloud`.`vm_template` ADD COLUMN `user_data_link_policy` varchar(255) DEFAULT NULL COMMENT 'user data link policy with template'; +ALTER TABLE `cloud`.`vm_template` ADD CONSTRAINT `fk_vm_template__user_data_id` FOREIGN KEY `fk_vm_template__user_data_id`(`user_data_id`) REFERENCES `user_data`(`id`); + +-- Added userdata details to template +DROP VIEW IF EXISTS `cloud`.`template_view`; +CREATE VIEW `cloud`.`template_view` AS + SELECT + `vm_template`.`id` AS `id`, + `vm_template`.`uuid` AS `uuid`, + `vm_template`.`unique_name` AS `unique_name`, + `vm_template`.`name` AS `name`, + `vm_template`.`public` AS `public`, + `vm_template`.`featured` AS `featured`, + `vm_template`.`type` AS `type`, + `vm_template`.`hvm` AS `hvm`, + `vm_template`.`bits` AS `bits`, + `vm_template`.`url` AS `url`, + `vm_template`.`format` AS `format`, + `vm_template`.`created` AS `created`, + `vm_template`.`checksum` AS `checksum`, + `vm_template`.`display_text` AS `display_text`, + `vm_template`.`enable_password` AS `enable_password`, + `vm_template`.`dynamically_scalable` AS `dynamically_scalable`, + `vm_template`.`state` AS `template_state`, + `vm_template`.`guest_os_id` AS `guest_os_id`, + `guest_os`.`uuid` AS `guest_os_uuid`, + `guest_os`.`display_name` AS `guest_os_name`, + `vm_template`.`bootable` AS `bootable`, + `vm_template`.`prepopulate` AS `prepopulate`, + `vm_template`.`cross_zones` AS `cross_zones`, + `vm_template`.`hypervisor_type` AS `hypervisor_type`, + `vm_template`.`extractable` AS `extractable`, + `vm_template`.`template_tag` AS `template_tag`, + `vm_template`.`sort_key` AS `sort_key`, + `vm_template`.`removed` AS `removed`, + `vm_template`.`enable_sshkey` AS `enable_sshkey`, + `parent_template`.`id` AS `parent_template_id`, + `parent_template`.`uuid` AS `parent_template_uuid`, + `source_template`.`id` AS `source_template_id`, + `source_template`.`uuid` AS `source_template_uuid`, + `account`.`id` AS `account_id`, + `account`.`uuid` AS `account_uuid`, + `account`.`account_name` AS `account_name`, + `account`.`type` AS `account_type`, + `domain`.`id` AS `domain_id`, + `domain`.`uuid` AS `domain_uuid`, + `domain`.`name` AS `domain_name`, + `domain`.`path` AS `domain_path`, + `projects`.`id` AS `project_id`, + `projects`.`uuid` AS `project_uuid`, + `projects`.`name` AS `project_name`, + `data_center`.`id` AS `data_center_id`, + `data_center`.`uuid` AS `data_center_uuid`, + `data_center`.`name` AS `data_center_name`, + `launch_permission`.`account_id` AS `lp_account_id`, + `template_store_ref`.`store_id` AS `store_id`, + `image_store`.`scope` AS `store_scope`, + `template_store_ref`.`state` AS `state`, + `template_store_ref`.`download_state` AS `download_state`, + `template_store_ref`.`download_pct` AS `download_pct`, + `template_store_ref`.`error_str` AS `error_str`, + `template_store_ref`.`size` AS `size`, + `template_store_ref`.physical_size AS `physical_size`, + `template_store_ref`.`destroyed` AS `destroyed`, + `template_store_ref`.`created` AS `created_on_store`, + `vm_template_details`.`name` AS `detail_name`, + `vm_template_details`.`value` AS `detail_value`, + `resource_tags`.`id` AS `tag_id`, + `resource_tags`.`uuid` AS `tag_uuid`, + `resource_tags`.`key` AS `tag_key`, + `resource_tags`.`value` AS `tag_value`, + `resource_tags`.`domain_id` AS `tag_domain_id`, + `domain`.`uuid` AS `tag_domain_uuid`, + `domain`.`name` AS `tag_domain_name`, + `resource_tags`.`account_id` AS `tag_account_id`, + `account`.`account_name` AS `tag_account_name`, + `resource_tags`.`resource_id` AS `tag_resource_id`, + `resource_tags`.`resource_uuid` AS `tag_resource_uuid`, + `resource_tags`.`resource_type` AS `tag_resource_type`, + `resource_tags`.`customer` AS `tag_customer`, + CONCAT(`vm_template`.`id`, + '_', + IFNULL(`data_center`.`id`, 0)) AS `temp_zone_pair`, + `vm_template`.`direct_download` AS `direct_download`, + `vm_template`.`deploy_as_is` AS `deploy_as_is`, + `user_data`.`id` AS `user_data_id`, + `user_data`.`uuid` AS `user_data_uuid`, + `user_data`.`name` AS `user_data_name`, + `user_data`.`params` AS `user_data_params`, + `vm_template`.`user_data_link_policy` AS `user_data_policy` + FROM + (((((((((((((`vm_template` + JOIN `guest_os` ON ((`guest_os`.`id` = `vm_template`.`guest_os_id`))) + JOIN `account` ON ((`account`.`id` = `vm_template`.`account_id`))) + JOIN `domain` ON ((`domain`.`id` = `account`.`domain_id`))) + LEFT JOIN `projects` ON ((`projects`.`project_account_id` = `account`.`id`))) + LEFT JOIN `vm_template_details` ON ((`vm_template_details`.`template_id` = `vm_template`.`id`))) + LEFT JOIN `vm_template` `source_template` ON ((`source_template`.`id` = `vm_template`.`source_template_id`))) + LEFT JOIN `template_store_ref` ON (((`template_store_ref`.`template_id` = `vm_template`.`id`) + AND (`template_store_ref`.`store_role` = 'Image') + AND (`template_store_ref`.`destroyed` = 0)))) + LEFT JOIN `vm_template` `parent_template` ON ((`parent_template`.`id` = `vm_template`.`parent_template_id`))) + LEFT JOIN `image_store` ON ((ISNULL(`image_store`.`removed`) + AND (`template_store_ref`.`store_id` IS NOT NULL) + AND (`image_store`.`id` = `template_store_ref`.`store_id`)))) + LEFT JOIN `template_zone_ref` ON (((`template_zone_ref`.`template_id` = `vm_template`.`id`) + AND ISNULL(`template_store_ref`.`store_id`) + AND ISNULL(`template_zone_ref`.`removed`)))) + LEFT JOIN `data_center` ON (((`image_store`.`data_center_id` = `data_center`.`id`) + OR (`template_zone_ref`.`zone_id` = `data_center`.`id`)))) + LEFT JOIN `launch_permission` ON ((`launch_permission`.`template_id` = `vm_template`.`id`))) + LEFT JOIN `user_data` ON ((`user_data`.`id` = `vm_template`.`user_data_id`)) + LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_template`.`id`) + AND ((`resource_tags`.`resource_type` = 'Template') + OR (`resource_tags`.`resource_type` = 'ISO'))))); + +DROP VIEW IF EXISTS `cloud`.`user_vm_view`; +CREATE + VIEW `user_vm_view` AS +SELECT + `vm_instance`.`id` AS `id`, + `vm_instance`.`name` AS `name`, + `user_vm`.`display_name` AS `display_name`, + `user_vm`.`user_data` AS `user_data`, + `account`.`id` AS `account_id`, + `account`.`uuid` AS `account_uuid`, + `account`.`account_name` AS `account_name`, + `account`.`type` AS `account_type`, + `domain`.`id` AS `domain_id`, + `domain`.`uuid` AS `domain_uuid`, + `domain`.`name` AS `domain_name`, + `domain`.`path` AS `domain_path`, + `projects`.`id` AS `project_id`, + `projects`.`uuid` AS `project_uuid`, + `projects`.`name` AS `project_name`, + `instance_group`.`id` AS `instance_group_id`, + `instance_group`.`uuid` AS `instance_group_uuid`, + `instance_group`.`name` AS `instance_group_name`, + `vm_instance`.`uuid` AS `uuid`, + `vm_instance`.`user_id` AS `user_id`, + `vm_instance`.`last_host_id` AS `last_host_id`, + `vm_instance`.`vm_type` AS `type`, + `vm_instance`.`limit_cpu_use` AS `limit_cpu_use`, + `vm_instance`.`created` AS `created`, + `vm_instance`.`state` AS `state`, + `vm_instance`.`update_time` AS `update_time`, + `vm_instance`.`removed` AS `removed`, + `vm_instance`.`ha_enabled` AS `ha_enabled`, + `vm_instance`.`hypervisor_type` AS `hypervisor_type`, + `vm_instance`.`instance_name` AS `instance_name`, + `vm_instance`.`guest_os_id` AS `guest_os_id`, + `vm_instance`.`display_vm` AS `display_vm`, + `guest_os`.`uuid` AS `guest_os_uuid`, + `vm_instance`.`pod_id` AS `pod_id`, + `host_pod_ref`.`uuid` AS `pod_uuid`, + `vm_instance`.`private_ip_address` AS `private_ip_address`, + `vm_instance`.`private_mac_address` AS `private_mac_address`, + `vm_instance`.`vm_type` AS `vm_type`, + `data_center`.`id` AS `data_center_id`, + `data_center`.`uuid` AS `data_center_uuid`, + `data_center`.`name` AS `data_center_name`, + `data_center`.`is_security_group_enabled` AS `security_group_enabled`, + `data_center`.`networktype` AS `data_center_type`, + `host`.`id` AS `host_id`, + `host`.`uuid` AS `host_uuid`, + `host`.`name` AS `host_name`, + `host`.`cluster_id` AS `cluster_id`, + `vm_template`.`id` AS `template_id`, + `vm_template`.`uuid` AS `template_uuid`, + `vm_template`.`name` AS `template_name`, + `vm_template`.`display_text` AS `template_display_text`, + `vm_template`.`enable_password` AS `password_enabled`, + `iso`.`id` AS `iso_id`, + `iso`.`uuid` AS `iso_uuid`, + `iso`.`name` AS `iso_name`, + `iso`.`display_text` AS `iso_display_text`, + `service_offering`.`id` AS `service_offering_id`, + `svc_disk_offering`.`uuid` AS `service_offering_uuid`, + `disk_offering`.`uuid` AS `disk_offering_uuid`, + `disk_offering`.`id` AS `disk_offering_id`, + (CASE + WHEN ISNULL(`service_offering`.`cpu`) THEN `custom_cpu`.`value` + ELSE `service_offering`.`cpu` + END) AS `cpu`, + (CASE + WHEN ISNULL(`service_offering`.`speed`) THEN `custom_speed`.`value` + ELSE `service_offering`.`speed` + END) AS `speed`, + (CASE + WHEN ISNULL(`service_offering`.`ram_size`) THEN `custom_ram_size`.`value` + ELSE `service_offering`.`ram_size` + END) AS `ram_size`, + `backup_offering`.`uuid` AS `backup_offering_uuid`, + `backup_offering`.`id` AS `backup_offering_id`, + `svc_disk_offering`.`name` AS `service_offering_name`, + `disk_offering`.`name` AS `disk_offering_name`, + `backup_offering`.`name` AS `backup_offering_name`, + `storage_pool`.`id` AS `pool_id`, + `storage_pool`.`uuid` AS `pool_uuid`, + `storage_pool`.`pool_type` AS `pool_type`, + `volumes`.`id` AS `volume_id`, + `volumes`.`uuid` AS `volume_uuid`, + `volumes`.`device_id` AS `volume_device_id`, + `volumes`.`volume_type` AS `volume_type`, + `security_group`.`id` AS `security_group_id`, + `security_group`.`uuid` AS `security_group_uuid`, + `security_group`.`name` AS `security_group_name`, + `security_group`.`description` AS `security_group_description`, + `nics`.`id` AS `nic_id`, + `nics`.`uuid` AS `nic_uuid`, + `nics`.`device_id` AS `nic_device_id`, + `nics`.`network_id` AS `network_id`, + `nics`.`ip4_address` AS `ip_address`, + `nics`.`ip6_address` AS `ip6_address`, + `nics`.`ip6_gateway` AS `ip6_gateway`, + `nics`.`ip6_cidr` AS `ip6_cidr`, + `nics`.`default_nic` AS `is_default_nic`, + `nics`.`gateway` AS `gateway`, + `nics`.`netmask` AS `netmask`, + `nics`.`mac_address` AS `mac_address`, + `nics`.`broadcast_uri` AS `broadcast_uri`, + `nics`.`isolation_uri` AS `isolation_uri`, + `vpc`.`id` AS `vpc_id`, + `vpc`.`uuid` AS `vpc_uuid`, + `networks`.`uuid` AS `network_uuid`, + `networks`.`name` AS `network_name`, + `networks`.`traffic_type` AS `traffic_type`, + `networks`.`guest_type` AS `guest_type`, + `user_ip_address`.`id` AS `public_ip_id`, + `user_ip_address`.`uuid` AS `public_ip_uuid`, + `user_ip_address`.`public_ip_address` AS `public_ip_address`, + `ssh_keypairs`.`keypair_name` AS `keypair_name`, + `resource_tags`.`id` AS `tag_id`, + `resource_tags`.`uuid` AS `tag_uuid`, + `resource_tags`.`key` AS `tag_key`, + `resource_tags`.`value` AS `tag_value`, + `resource_tags`.`domain_id` AS `tag_domain_id`, + `domain`.`uuid` AS `tag_domain_uuid`, + `domain`.`name` AS `tag_domain_name`, + `resource_tags`.`account_id` AS `tag_account_id`, + `account`.`account_name` AS `tag_account_name`, + `resource_tags`.`resource_id` AS `tag_resource_id`, + `resource_tags`.`resource_uuid` AS `tag_resource_uuid`, + `resource_tags`.`resource_type` AS `tag_resource_type`, + `resource_tags`.`customer` AS `tag_customer`, + `async_job`.`id` AS `job_id`, + `async_job`.`uuid` AS `job_uuid`, + `async_job`.`job_status` AS `job_status`, + `async_job`.`account_id` AS `job_account_id`, + `affinity_group`.`id` AS `affinity_group_id`, + `affinity_group`.`uuid` AS `affinity_group_uuid`, + `affinity_group`.`name` AS `affinity_group_name`, + `affinity_group`.`description` AS `affinity_group_description`, + `vm_instance`.`dynamically_scalable` AS `dynamically_scalable`, + `user_data`.`id` AS `user_data_id`, + `user_data`.`uuid` AS `user_data_uuid`, + `user_data`.`name` AS `user_data_name`, + `user_vm`.`user_data_details` AS `user_data_details`, + `vm_template`.`user_data_link_policy` AS `user_data_policy` +FROM + ((((((((((((((((((((((((((((((((((`user_vm` + JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) + AND ISNULL(`vm_instance`.`removed`)))) + JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) + JOIN `domain` ON ((`vm_instance`.`domain_id` = `domain`.`id`))) + LEFT JOIN `guest_os` ON ((`vm_instance`.`guest_os_id` = `guest_os`.`id`))) + LEFT JOIN `host_pod_ref` ON ((`vm_instance`.`pod_id` = `host_pod_ref`.`id`))) + LEFT JOIN `projects` ON ((`projects`.`project_account_id` = `account`.`id`))) + LEFT JOIN `instance_group_vm_map` ON ((`vm_instance`.`id` = `instance_group_vm_map`.`instance_id`))) + LEFT JOIN `instance_group` ON ((`instance_group_vm_map`.`group_id` = `instance_group`.`id`))) + LEFT JOIN `data_center` ON ((`vm_instance`.`data_center_id` = `data_center`.`id`))) + LEFT JOIN `host` ON ((`vm_instance`.`host_id` = `host`.`id`))) + LEFT JOIN `vm_template` ON ((`vm_instance`.`vm_template_id` = `vm_template`.`id`))) + LEFT JOIN `vm_template` `iso` ON ((`iso`.`id` = `user_vm`.`iso_id`))) + LEFT JOIN `volumes` ON ((`vm_instance`.`id` = `volumes`.`instance_id`))) + LEFT JOIN `service_offering` ON ((`vm_instance`.`service_offering_id` = `service_offering`.`id`))) + LEFT JOIN `disk_offering` `svc_disk_offering` ON ((`vm_instance`.`service_offering_id` = `svc_disk_offering`.`id`))) + LEFT JOIN `disk_offering` ON ((`volumes`.`disk_offering_id` = `disk_offering`.`id`))) + LEFT JOIN `backup_offering` ON ((`vm_instance`.`backup_offering_id` = `backup_offering`.`id`))) + LEFT JOIN `storage_pool` ON ((`volumes`.`pool_id` = `storage_pool`.`id`))) + LEFT JOIN `security_group_vm_map` ON ((`vm_instance`.`id` = `security_group_vm_map`.`instance_id`))) + LEFT JOIN `security_group` ON ((`security_group_vm_map`.`security_group_id` = `security_group`.`id`))) + LEFT JOIN `user_data` ON ((`user_data`.`id` = `user_vm`.`user_data_id`))) + LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`) + AND ISNULL(`nics`.`removed`)))) + LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`))) + LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) + AND ISNULL(`vpc`.`removed`)))) + LEFT JOIN `user_ip_address` ON ((`user_ip_address`.`vm_id` = `vm_instance`.`id`))) + LEFT JOIN `user_vm_details` `ssh_details` ON (((`ssh_details`.`vm_id` = `vm_instance`.`id`) + AND (`ssh_details`.`name` = 'SSH.PublicKey')))) + LEFT JOIN `ssh_keypairs` ON (((`ssh_keypairs`.`public_key` = `ssh_details`.`value`) + AND (`ssh_keypairs`.`account_id` = `account`.`id`)))) + LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_instance`.`id`) + AND (`resource_tags`.`resource_type` = 'UserVm')))) + LEFT JOIN `async_job` ON (((`async_job`.`instance_id` = `vm_instance`.`id`) + AND (`async_job`.`instance_type` = 'VirtualMachine') + AND (`async_job`.`job_status` = 0)))) + LEFT JOIN `affinity_group_vm_map` ON ((`vm_instance`.`id` = `affinity_group_vm_map`.`instance_id`))) + LEFT JOIN `affinity_group` ON ((`affinity_group_vm_map`.`affinity_group_id` = `affinity_group`.`id`))) + LEFT JOIN `user_vm_details` `custom_cpu` ON (((`custom_cpu`.`vm_id` = `vm_instance`.`id`) + AND (`custom_cpu`.`name` = 'CpuNumber')))) + LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`) + AND (`custom_speed`.`name` = 'CpuSpeed')))) + LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`) + AND (`custom_ram_size`.`name` = 'memory')))); diff --git a/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java b/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java index 0675b43cb4a..f643a340dad 100644 --- a/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java +++ b/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java @@ -30,8 +30,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.collections.MapUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; @@ -91,7 +94,7 @@ public class ConfigDriveBuilder { * This method will build the metadata files required by OpenStack driver. Then, an ISO is going to be generated and returned as a String in base 64. * If vmData is null, we throw a {@link CloudRuntimeException}. Moreover, {@link IOException} are captured and re-thrown as {@link CloudRuntimeException}. */ - public static String buildConfigDrive(List vmData, String isoFileName, String driveLabel) { + public static String buildConfigDrive(List vmData, String isoFileName, String driveLabel, Map customUserdataParams) { if (vmData == null) { throw new CloudRuntimeException("No VM metadata provided"); } @@ -105,7 +108,7 @@ public class ConfigDriveBuilder { File openStackFolder = new File(tempDirName + ConfigDrive.openStackConfigDriveName); writeVendorAndNetworkEmptyJsonFile(openStackFolder); - writeVmMetadata(vmData, tempDirName, openStackFolder); + writeVmMetadata(vmData, tempDirName, openStackFolder, customUserdataParams); linkUserData(tempDirName); @@ -187,10 +190,10 @@ public class ConfigDriveBuilder { } /** - * First we generate a JSON object using {@link #createJsonObjectWithVmData(List, String)}, then we write it to a file called "meta_data.json". + * First we generate a JSON object using {@link #createJsonObjectWithVmData(List, String, Map)}, then we write it to a file called "meta_data.json". */ - static void writeVmMetadata(List vmData, String tempDirName, File openStackFolder) { - JsonObject metaData = createJsonObjectWithVmData(vmData, tempDirName); + static void writeVmMetadata(List vmData, String tempDirName, File openStackFolder, Map customUserdataParams) { + JsonObject metaData = createJsonObjectWithVmData(vmData, tempDirName, customUserdataParams); writeFile(openStackFolder, "meta_data.json", metaData.toString()); } @@ -220,7 +223,7 @@ public class ConfigDriveBuilder { *
  • [2]: config data file content * */ - static JsonObject createJsonObjectWithVmData(List vmData, String tempDirName) { + static JsonObject createJsonObjectWithVmData(List vmData, String tempDirName, Map customUserdataParams) { JsonObject metaData = new JsonObject(); for (String[] item : vmData) { String dataType = item[CONFIGDATA_DIR]; @@ -228,12 +231,12 @@ public class ConfigDriveBuilder { String content = item[CONFIGDATA_CONTENT]; LOG.debug(String.format("[createConfigDriveIsoForVM] dataType=%s, filename=%s, content=%s", dataType, fileName, (PASSWORD_FILE.equals(fileName) ? "********" : content))); - createFileInTempDirAnAppendOpenStackMetadataToJsonObject(tempDirName, metaData, dataType, fileName, content); + createFileInTempDirAnAppendOpenStackMetadataToJsonObject(tempDirName, metaData, dataType, fileName, content, customUserdataParams); } return metaData; } - static void createFileInTempDirAnAppendOpenStackMetadataToJsonObject(String tempDirName, JsonObject metaData, String dataType, String fileName, String content) { + static void createFileInTempDirAnAppendOpenStackMetadataToJsonObject(String tempDirName, JsonObject metaData, String dataType, String fileName, String content, Map customUserdataParams) { if (StringUtils.isBlank(dataType)) { return; } @@ -258,6 +261,22 @@ public class ConfigDriveBuilder { //now write the file to the OpenStack directory buildOpenStackMetaData(metaData, dataType, fileName, content); + buildCustomUserdataParamsMetaData(metaData, dataType, fileName, content, customUserdataParams); + } + + protected static void buildCustomUserdataParamsMetaData(JsonObject metaData, String dataType, String fileName, String content, Map customUserdataParams) { + if (!NetworkModel.METATDATA_DIR.equals(dataType)) { + return; + } + if (StringUtils.isEmpty(content)) { + return; + } + if (MapUtils.isNotEmpty(customUserdataParams)) { + Set userdataVariableFileNames = customUserdataParams.keySet(); + if (userdataVariableFileNames.contains(fileName)) { + metaData.addProperty(fileName, content); + } + } } /** diff --git a/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java b/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java index 50cab35e78d..cefa368c251 100644 --- a/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java +++ b/engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.storage.configdrive; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.times; @@ -28,7 +29,9 @@ import java.lang.reflect.Method; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; @@ -124,7 +127,7 @@ public class ConfigDriveBuilderTest { @Test(expected = CloudRuntimeException.class) public void buildConfigDriveTestNoVmData() { - ConfigDriveBuilder.buildConfigDrive(null, "teste", "C:"); + ConfigDriveBuilder.buildConfigDrive(null, "teste", "C:", null); } @SuppressWarnings("unchecked") @@ -140,9 +143,9 @@ public class ConfigDriveBuilderTest { //This is odd, but it was necessary to allow us to check if we catch the IOexception and re-throw as a CloudRuntimeException //We are mocking the class being tested; therefore, we needed to force the execution of the real method we want to test. - PowerMockito.when(ConfigDriveBuilder.class, new ArrayList<>(), "teste", "C:").thenCallRealMethod(); + PowerMockito.when(ConfigDriveBuilder.class, new ArrayList<>(), "teste", "C:", null).thenCallRealMethod(); - ConfigDriveBuilder.buildConfigDrive(new ArrayList<>(), "teste", "C:"); + ConfigDriveBuilder.buildConfigDrive(new ArrayList<>(), "teste", "C:", null); } @Test @@ -155,7 +158,7 @@ public class ConfigDriveBuilderTest { PowerMockito.doNothing().when(ConfigDriveBuilder.class, writeVendorAndNetworkEmptyJsonFileMethod).withArguments(Mockito.any(File.class)); Method writeVmMetadataMethod = getWriteVmMetadataMethod(); - PowerMockito.doNothing().when(ConfigDriveBuilder.class, writeVmMetadataMethod).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.any(File.class)); + PowerMockito.doNothing().when(ConfigDriveBuilder.class, writeVmMetadataMethod).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.any(File.class), anyMap()); Method linkUserDataMethod = ReflectionUtils.getMethods(ConfigDriveBuilder.class, ReflectionUtils.withName("linkUserData")).iterator().next(); PowerMockito.doNothing().when(ConfigDriveBuilder.class, linkUserDataMethod).withArguments(Mockito.anyString()); @@ -164,15 +167,15 @@ public class ConfigDriveBuilderTest { PowerMockito.doReturn("mockIsoDataBase64").when(ConfigDriveBuilder.class, generateAndRetrieveIsoAsBase64IsoMethod).withArguments(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); //force execution of real method - PowerMockito.when(ConfigDriveBuilder.class, new ArrayList<>(), "teste", "C:").thenCallRealMethod(); + PowerMockito.when(ConfigDriveBuilder.class, new ArrayList<>(), "teste", "C:", null).thenCallRealMethod(); - String returnedIsoData = ConfigDriveBuilder.buildConfigDrive(new ArrayList<>(), "teste", "C:"); + String returnedIsoData = ConfigDriveBuilder.buildConfigDrive(new ArrayList<>(), "teste", "C:", null); Assert.assertEquals("mockIsoDataBase64", returnedIsoData); PowerMockito.verifyStatic(ConfigDriveBuilder.class); ConfigDriveBuilder.writeVendorAndNetworkEmptyJsonFile(Mockito.any(File.class)); - ConfigDriveBuilder.writeVmMetadata(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.any(File.class)); + ConfigDriveBuilder.writeVmMetadata(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.any(File.class), anyMap()); ConfigDriveBuilder.linkUserData(Mockito.anyString()); ConfigDriveBuilder.generateAndRetrieveIsoAsBase64Iso(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); } @@ -233,20 +236,20 @@ public class ConfigDriveBuilderTest { PowerMockito.mockStatic(ConfigDriveBuilder.class); Method method = getWriteVmMetadataMethod(); - PowerMockito.when(ConfigDriveBuilder.class, method).withArguments(Mockito.anyListOf(String[].class), anyString(), any(File.class)).thenCallRealMethod(); + PowerMockito.when(ConfigDriveBuilder.class, method).withArguments(Mockito.anyListOf(String[].class), anyString(), any(File.class), anyMap()).thenCallRealMethod(); Method createJsonObjectWithVmDataMethod = ReflectionUtils.getMethods(ConfigDriveBuilder.class, ReflectionUtils.withName("createJsonObjectWithVmData")).iterator().next(); - PowerMockito.when(ConfigDriveBuilder.class, createJsonObjectWithVmDataMethod).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString()).thenReturn(new JsonObject()); + PowerMockito.when(ConfigDriveBuilder.class, createJsonObjectWithVmDataMethod).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.anyMap()).thenReturn(new JsonObject()); List vmData = new ArrayList<>(); vmData.add(new String[] {"dataType", "fileName", "content"}); vmData.add(new String[] {"dataType2", "fileName2", "content2"}); - ConfigDriveBuilder.writeVmMetadata(vmData, "metadataFile", new File("folder")); + ConfigDriveBuilder.writeVmMetadata(vmData, "metadataFile", new File("folder"), new HashMap<>()); PowerMockito.verifyStatic(ConfigDriveBuilder.class); - ConfigDriveBuilder.createJsonObjectWithVmData(vmData, "metadataFile"); + ConfigDriveBuilder.createJsonObjectWithVmData(vmData, "metadataFile", new HashMap<>()); ConfigDriveBuilder.writeFile(Mockito.any(File.class), Mockito.eq("meta_data.json"), Mockito.eq("{}")); } @@ -398,19 +401,58 @@ public class ConfigDriveBuilderTest { PowerMockito.mockStatic(ConfigDriveBuilder.class); Method method = ReflectionUtils.getMethods(ConfigDriveBuilder.class, ReflectionUtils.withName("createJsonObjectWithVmData")).iterator().next(); - PowerMockito.when(ConfigDriveBuilder.class, method).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString()).thenCallRealMethod(); + PowerMockito.when(ConfigDriveBuilder.class, method).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.nullable(Map.class)).thenCallRealMethod(); List vmData = new ArrayList<>(); vmData.add(new String[] {"dataType", "fileName", "content"}); vmData.add(new String[] {"dataType2", "fileName2", "content2"}); - ConfigDriveBuilder.createJsonObjectWithVmData(vmData, "tempDirName"); + ConfigDriveBuilder.createJsonObjectWithVmData(vmData, "tempDirName", new HashMap<>()); PowerMockito.verifyStatic(ConfigDriveBuilder.class, Mockito.times(1)); ConfigDriveBuilder.createFileInTempDirAnAppendOpenStackMetadataToJsonObject(Mockito.eq("tempDirName"), Mockito.any(JsonObject.class), Mockito.eq("dataType"), Mockito.eq("fileName"), - Mockito.eq("content")); + Mockito.eq("content"), Mockito.anyMap()); ConfigDriveBuilder.createFileInTempDirAnAppendOpenStackMetadataToJsonObject(Mockito.eq("tempDirName"), Mockito.any(JsonObject.class), Mockito.eq("dataType2"), Mockito.eq("fileName2"), - Mockito.eq("content2")); + Mockito.eq("content2"), Mockito.anyMap()); + } + + @Test + @SuppressWarnings("unchecked") + @PrepareForTest({ConfigDriveBuilder.class}) + public void buildCustomUserdataParamsMetadataTestNullContent() throws Exception { + PowerMockito.mockStatic(ConfigDriveBuilder.class); + + JsonObject metadata = new JsonObject(); + String dataType = "dataType1"; + String fileName = "testFileName"; + String content = null; + Map customUserdataParams = new HashMap<>(); + customUserdataParams.put(fileName, content); + + PowerMockito.when(ConfigDriveBuilder.class, metadata, dataType, fileName, content, customUserdataParams).thenCallRealMethod(); + + ConfigDriveBuilder.buildCustomUserdataParamsMetaData(metadata, dataType, fileName, content, customUserdataParams); + + Assert.assertEquals(null, metadata.getAsJsonPrimitive(fileName)); + } + + @Test + @SuppressWarnings("unchecked") + @PrepareForTest({ConfigDriveBuilder.class}) + public void buildCustomUserdataParamsMetadataTestWithContent() throws Exception { + PowerMockito.mockStatic(ConfigDriveBuilder.class); + + JsonObject metadata = new JsonObject(); + String dataType = "metadata"; + String fileName = "testFileName"; + String content = "testContent"; + Map customUserdataParams = new HashMap<>(); + customUserdataParams.put(fileName, content); + + PowerMockito.when(ConfigDriveBuilder.class, metadata, dataType, fileName, content, customUserdataParams).thenCallRealMethod(); + ConfigDriveBuilder.buildCustomUserdataParamsMetaData(metadata, dataType, fileName, content, customUserdataParams); + + Assert.assertEquals(content, metadata.getAsJsonPrimitive(fileName).getAsString()); } @Test diff --git a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/store/TemplateObject.java b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/store/TemplateObject.java index 8f857a2da97..57dd5cd04aa 100644 --- a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/store/TemplateObject.java +++ b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/store/TemplateObject.java @@ -23,6 +23,7 @@ import java.util.Map; import javax.inject.Inject; +import com.cloud.user.UserData; import org.apache.log4j.Logger; import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore; @@ -327,6 +328,16 @@ public class TemplateObject implements TemplateInfo { return deployAsIsConfiguration; } + @Override + public Long getUserDataId() { + return imageVO.getUserDataId(); + } + + @Override + public UserData.UserDataOverridePolicy getUserDataOverridePolicy() { + return imageVO.getUserDataOverridePolicy(); + } + @Override public DataTO getTO() { DataTO to = null; diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java index d1bce216f18..8f6414d7476 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java @@ -656,7 +656,7 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co } else { long id = userVmDao.getNextInSequence(Long.class, "id"); UserVmVO vmInstanceVO = new UserVmVO(id, vmInternalName, vmInternalName, templateId, HypervisorType.VMware, guestOsId, false, false, domainId, accountId, userId, - serviceOfferingId, null, vmInternalName, null); + serviceOfferingId, null, null, null, vmInternalName, null); vmInstanceVO.setDataCenterId(zoneId); return userVmDao.persist(vmInstanceVO); } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java index 67fb093534b..dd88c6db70a 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java @@ -379,7 +379,7 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu String base64UserData = Base64.encodeBase64String(k8sNodeConfig.getBytes(StringUtils.getPreferredCharset())); nodeVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner, hostName, hostName, null, null, null, - Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, kubernetesCluster.getKeyPair(), + Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, kubernetesCluster.getKeyPair(), null, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE); if (LOGGER.isInfoEnabled()) { LOGGER.info(String.format("Created node VM : %s, %s in the Kubernetes cluster : %s", hostName, nodeVm.getUuid(), kubernetesCluster.getName())); diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java index f9d42f61756..bee00fff8e2 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java @@ -216,7 +216,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif String base64UserData = Base64.encodeBase64String(k8sControlNodeConfig.getBytes(StringUtils.getPreferredCharset())); controlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner, hostName, hostName, null, null, null, - Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, kubernetesCluster.getKeyPair(), + Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, kubernetesCluster.getKeyPair(), requestedIps, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE); if (LOGGER.isInfoEnabled()) { LOGGER.info(String.format("Created control VM ID: %s, %s in the Kubernetes cluster : %s", controlVm.getUuid(), hostName, kubernetesCluster.getName())); @@ -276,7 +276,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif String base64UserData = Base64.encodeBase64String(k8sControlNodeConfig.getBytes(StringUtils.getPreferredCharset())); additionalControlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner, hostName, hostName, null, null, null, - Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, kubernetesCluster.getKeyPair(), + Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, kubernetesCluster.getKeyPair(), null, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE); if (LOGGER.isInfoEnabled()) { LOGGER.info(String.format("Created control VM ID : %s, %s in the Kubernetes cluster : %s", additionalControlVm.getUuid(), hostName, kubernetesCluster.getName())); diff --git a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ServiceVirtualMachine.java b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ServiceVirtualMachine.java index 676afc9ae8a..17384c28f0d 100644 --- a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ServiceVirtualMachine.java +++ b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ServiceVirtualMachine.java @@ -23,6 +23,6 @@ import com.cloud.vm.UserVmVO; public class ServiceVirtualMachine extends UserVmVO { public ServiceVirtualMachine(long id, String instanceName, String name, long templateId, long serviceOfferingId, HypervisorType hypervisorType, long guestOSId, long dataCenterId, long domainId, long accountId, long userId, boolean haEnabled) { - super(id, instanceName, name, templateId, hypervisorType, guestOSId, false, false, domainId, accountId, userId, serviceOfferingId, null, name, null); + super(id, instanceName, name, templateId, hypervisorType, guestOSId, false, false, domainId, accountId, userId, serviceOfferingId, null, null, null, name, null); } } diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java index 64a036a2c62..631341e39f6 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java @@ -240,7 +240,7 @@ public class ManagementServerMock { long id = _userVmDao.getNextInSequence(Long.class, "id"); UserVmVO vm = new UserVmVO(id, name, name, tmpl.getId(), HypervisorType.XenServer, tmpl.getGuestOSId(), false, false, _zone.getDomainId(), Account.ACCOUNT_ID_SYSTEM, - 1, small.getId(), null, name, null); + 1, small.getId(), null, null, null, name, null); vm.setState(com.cloud.vm.VirtualMachine.State.Running); vm.setHostId(_hostId); vm.setDataCenterId(network.getDataCenterId()); diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 79493403a08..8c58ab6822e 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -37,6 +37,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import com.cloud.utils.security.CertificateHelper; +import com.cloud.user.UserData; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.affinity.AffinityGroup; @@ -149,6 +150,7 @@ import org.apache.cloudstack.api.response.TrafficMonitorResponse; import org.apache.cloudstack.api.response.TrafficTypeResponse; import org.apache.cloudstack.api.response.UpgradeRouterTemplateResponse; import org.apache.cloudstack.api.response.UsageRecordResponse; +import org.apache.cloudstack.api.response.UserDataResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VMSnapshotResponse; @@ -4483,6 +4485,20 @@ public class ApiResponseHelper implements ResponseGenerator { return response; } + @Override + public UserDataResponse createUserDataResponse(UserData userData) { + UserDataResponse response = new UserDataResponse(userData.getUuid(), userData.getName(), userData.getUserData(), userData.getParams()); + Account account = ApiDBUtils.findAccountById(userData.getAccountId()); + response.setAccountId(account.getUuid()); + response.setAccountName(account.getAccountName()); + Domain domain = ApiDBUtils.findDomainById(userData.getDomainId()); + response.setDomainId(domain.getUuid()); + response.setDomainName(domain.getName()); + response.setHasAnnotation(annotationDao.hasAnnotations(userData.getUuid(), AnnotationService.EntityType.USER_DATA.name(), + _accountMgr.isRootAdmin(CallContext.current().getCallingAccount().getId()))); + return response; + } + @Override public BackupResponse createBackupResponse(Backup backup) { return ApiDBUtils.newBackupResponse(backup); diff --git a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java index fe00ef0d11d..6a856e71737 100644 --- a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java @@ -29,6 +29,7 @@ import javax.inject.Inject; import com.cloud.deployasis.DeployAsIsConstants; import com.cloud.deployasis.TemplateDeployAsIsDetailVO; import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao; +import com.cloud.user.dao.UserDataDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; @@ -90,6 +91,8 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation tmpltIdPairSearch; @@ -290,6 +293,12 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation 0) { @@ -451,6 +467,13 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation 0) { diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 03fdb542d2f..28fb84de671 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -379,6 +379,13 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation generateVmData(String userData, String serviceOffering, long datacenterId, + public List generateVmData(String userData, String userDataDetails, String serviceOffering, long datacenterId, String vmName, String vmHostName, long vmId, String vmUuid, String guestIpAddress, String publicKey, String password, Boolean isWindows, String hostname) { @@ -2448,6 +2448,8 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi vmData.add(new String[]{METATDATA_DIR, LOCAL_HOSTNAME_FILE, StringUtils.unicodeEscape(vmHostName)}); vmData.add(new String[]{METATDATA_DIR, LOCAL_IPV4_FILE, guestIpAddress}); + addUserDataDetailsToCommand(vmData, userDataDetails); + String publicIpAddress = guestIpAddress; String publicHostName = StringUtils.unicodeEscape(vmHostName); @@ -2516,6 +2518,20 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi return vmData; } + protected void addUserDataDetailsToCommand(List vmData, String userDataDetails) { + if(userDataDetails != null && !userDataDetails.isEmpty()) { + userDataDetails = userDataDetails.substring(1, userDataDetails.length()-1); + String[] keyValuePairs = userDataDetails.split(","); + for(String pair : keyValuePairs) + { + String[] entry = pair.split("="); + String key = entry[0].trim(); + String value = entry[1].trim(); + vmData.add(new String[]{METATDATA_DIR, key, StringUtils.unicodeEscape(value)}); + } + } + } + @Override public String getConfigComponentName() { return NetworkModel.class.getSimpleName(); diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java index 11ecfb503a2..3867773e225 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -1324,19 +1324,19 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScale if (zone.getNetworkType() == NetworkType.Basic) { vm = _userVmService.createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, null, owner, "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), - "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, + "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, null, null, null, true, null, null, null, null, null, null, null, true); } else { if (zone.isSecurityGroupEnabled()) { vm = _userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, null, null, owner, "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), - "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, + "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, null, null, null, true, null, null, null, null, null, null, null, true); } else { vm = _userVmService.createAdvancedVirtualMachine(zone, serviceOffering, template, null, owner, "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), - null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, addrs, true, null, null, null, null, null, null, null, true, null); + null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, null, null, addrs, true, null, null, null, null, null, null, null, true, null); } } diff --git a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java index f0f042256ec..39e2d9b008f 100644 --- a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java +++ b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java @@ -392,7 +392,7 @@ public class ConfigDriveNetworkElement extends AdapterBase implements NetworkEle if (userVm != null) { final boolean isWindows = isWindows(userVm.getGuestOSId()); - List vmData = _networkModel.generateVmData(userVm.getUserData(), _serviceOfferingDao.findById(userVm.getServiceOfferingId()).getName(), userVm.getDataCenterId(), userVm.getInstanceName(), vm.getHostName(), vm.getId(), + List vmData = _networkModel.generateVmData(userVm.getUserData(), userVm.getUserDataDetails(), _serviceOfferingDao.findById(userVm.getServiceOfferingId()).getName(), userVm.getDataCenterId(), userVm.getInstanceName(), vm.getHostName(), vm.getId(), vm.getUuid(), nic.getMacAddress(), userVm.getDetail("SSH.PublicKey"), (String) vm.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows, VirtualMachineManager.getHypervisorHostname(dest.getHost() != null ? dest.getHost().getName() : "")); vm.setVmData(vmData); vm.setConfigDriveLabel(VirtualMachineManager.VmConfigDriveLabel.value()); @@ -534,9 +534,11 @@ public class ConfigDriveNetworkElement extends AdapterBase implements NetworkEle LOG.debug("Creating config drive ISO for vm: " + profile.getInstanceName() + " on host: " + hostId); + Map customUserdataParamMap = getVMCustomUserdataParamMap(profile.getId()); + final String isoFileName = ConfigDrive.configIsoFileName(profile.getInstanceName()); final String isoPath = ConfigDrive.createConfigDrivePath(profile.getInstanceName()); - final String isoData = ConfigDriveBuilder.buildConfigDrive(profile.getVmData(), isoFileName, profile.getConfigDriveLabel()); + final String isoData = ConfigDriveBuilder.buildConfigDrive(profile.getVmData(), isoFileName, profile.getConfigDriveLabel(), customUserdataParamMap); final HandleConfigDriveIsoCommand configDriveIsoCommand = new HandleConfigDriveIsoCommand(isoPath, isoData, null, false, true, true); final HandleConfigDriveIsoAnswer answer = (HandleConfigDriveIsoAnswer) agentManager.easySend(hostId, configDriveIsoCommand); @@ -593,9 +595,11 @@ public class ConfigDriveNetworkElement extends AdapterBase implements NetworkEle LOG.debug("Creating config drive ISO for vm: " + profile.getInstanceName()); + Map customUserdataParamMap = getVMCustomUserdataParamMap(profile.getId()); + final String isoFileName = ConfigDrive.configIsoFileName(profile.getInstanceName()); final String isoPath = ConfigDrive.createConfigDrivePath(profile.getInstanceName()); - final String isoData = ConfigDriveBuilder.buildConfigDrive(profile.getVmData(), isoFileName, profile.getConfigDriveLabel()); + final String isoData = ConfigDriveBuilder.buildConfigDrive(profile.getVmData(), isoFileName, profile.getConfigDriveLabel(), customUserdataParamMap); boolean useHostCacheOnUnsupportedPool = VirtualMachineManager.VmConfigDriveUseHostCacheOnUnsupportedPool.valueIn(dest.getDataCenter().getId()); boolean preferHostCache = VirtualMachineManager.VmConfigDriveForceHostCacheUse.valueIn(dest.getDataCenter().getId()); final HandleConfigDriveIsoCommand configDriveIsoCommand = new HandleConfigDriveIsoCommand(isoPath, isoData, dataStore.getTO(), useHostCacheOnUnsupportedPool, preferHostCache, true); @@ -611,6 +615,23 @@ public class ConfigDriveNetworkElement extends AdapterBase implements NetworkEle return true; } + private Map getVMCustomUserdataParamMap(long vmId) { + UserVmVO userVm = _userVmDao.findById(vmId); + String userDataDetails = userVm.getUserDataDetails(); + Map customUserdataParamMap = new HashMap<>(); + if(userDataDetails != null && !userDataDetails.isEmpty()) { + userDataDetails = userDataDetails.substring(1, userDataDetails.length()-1); + String[] keyValuePairs = userDataDetails.split(","); + for(String pair : keyValuePairs) + { + String[] entry = pair.split("="); + customUserdataParamMap.put(entry[0].trim(), entry[1].trim()); + } + } + + return customUserdataParamMap; + } + private DataStore getDatastoreForConfigDriveIso(DiskTO disk, VirtualMachineProfile profile, DeployDestination dest) { DataStore dataStore = null; if (disk != null) { @@ -723,7 +744,7 @@ public class ConfigDriveNetworkElement extends AdapterBase implements NetworkEle } else { destHostname = VirtualMachineManager.getHypervisorHostname(dest.getHost().getName()); } - final List vmData = _networkModel.generateVmData(vm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), + final List vmData = _networkModel.generateVmData(vm.getUserData(), vm.getUserDataDetails(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), vm.getUuid(), nic.getIPv4Address(), sshPublicKey, (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows, destHostname); profile.setVmData(vmData); profile.setConfigDriveLabel(VirtualMachineManager.VmConfigDriveLabel.value()); diff --git a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java index 4f809ec2350..d15263f70aa 100644 --- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java +++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java @@ -210,7 +210,7 @@ public class CommandSetupHelper { Host host = _hostDao.findById(vm.getHostId()); String destHostname = VirtualMachineManager.getHypervisorHostname(host != null ? host.getName() : ""); - VmDataCommand vmDataCommand = generateVmDataCommand(router, nic.getIPv4Address(), vm.getUserData(), serviceOffering, zoneName, + VmDataCommand vmDataCommand = generateVmDataCommand(router, nic.getIPv4Address(), vm.getUserData(), vm.getUserDataDetails(), serviceOffering, zoneName, staticNatIp == null || staticNatIp.getState() != IpAddress.State.Allocated ? null : staticNatIp.getAddress().addr(), vm.getHostName(), vm.getInstanceName(), vm.getId(), vm.getUuid(), publicKey, nic.getNetworkId(), destHostname); @@ -1075,9 +1075,9 @@ public class CommandSetupHelper { return setupCmd; } - private VmDataCommand generateVmDataCommand(final VirtualRouter router, final String vmPrivateIpAddress, final String userData, final String serviceOffering, - final String zoneName, final String publicIpAddress, final String vmName, final String vmInstanceName, final long vmId, final String vmUuid, final String publicKey, - final long guestNetworkId, String hostname) { + private VmDataCommand generateVmDataCommand(final VirtualRouter router, final String vmPrivateIpAddress, final String userData, String userDataDetails, final String serviceOffering, + final String zoneName, final String publicIpAddress, final String vmName, final String vmInstanceName, final long vmId, final String vmUuid, final String publicKey, + final long guestNetworkId, String hostname) { final VmDataCommand cmd = new VmDataCommand(vmPrivateIpAddress, vmName, _networkModel.getExecuteInSeqNtwkElmtCmd()); cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, _routerControlHelper.getRouterControlIp(router.getId())); @@ -1093,6 +1093,8 @@ public class CommandSetupHelper { cmd.addVmData("metadata", "local-ipv4", vmPrivateIpAddress); cmd.addVmData("metadata", "local-hostname", StringUtils.unicodeEscape(vmName)); + addUserDataDetailsToCommand(cmd, userDataDetails); + Network network = _networkDao.findById(guestNetworkId); if (dcVo.getNetworkType() == NetworkType.Basic || network.getGuestType() == Network.GuestType.Shared) { cmd.addVmData("metadata", "public-ipv4", vmPrivateIpAddress); @@ -1127,6 +1129,20 @@ public class CommandSetupHelper { return cmd; } + protected void addUserDataDetailsToCommand(VmDataCommand cmd, String userDataDetails) { + if(userDataDetails != null && !userDataDetails.isEmpty()) { + userDataDetails = userDataDetails.substring(1, userDataDetails.length()-1); + String[] keyValuePairs = userDataDetails.split(","); + for(String pair : keyValuePairs) + { + String[] entry = pair.split("="); + String key = entry[0].trim(); + String value = entry[1].trim(); + cmd.addVmData("metadata", key, value); + } + } + } + private NicVO findGatewayIp(final long userVmId) { final NicVO defaultNic = _nicDao.findDefaultNicForVM(userVmId); return defaultNic; diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index c5aa4eac0d9..7a79773b0d4 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -16,7 +16,9 @@ // under the License. package com.cloud.server; +import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -45,6 +47,14 @@ import javax.naming.ConfigurationException; import com.cloud.agent.api.Answer; import com.cloud.agent.api.proxy.AllowConsoleAccessCommand; import com.cloud.exception.AgentUnavailableException; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.user.UserData; +import com.cloud.user.UserDataVO; +import com.cloud.user.dao.UserDataDao; +import com.cloud.utils.db.UUIDManager; +import com.cloud.vm.dao.DomainRouterDao; +import com.cloud.vm.dao.NicDao; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.affinity.AffinityGroupProcessor; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; @@ -52,6 +62,7 @@ import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; import org.apache.cloudstack.api.command.admin.account.DeleteAccountCmd; import org.apache.cloudstack.api.command.admin.account.DisableAccountCmd; @@ -482,6 +493,10 @@ import org.apache.cloudstack.api.command.user.template.ListTemplatesCmd; import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateTemplatePermissionsCmd; +import org.apache.cloudstack.api.command.user.userdata.DeleteUserDataCmd; +import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; +import org.apache.cloudstack.api.command.user.userdata.ListUserDataCmd; +import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd; import org.apache.cloudstack.api.command.user.vm.AddIpToVmNicCmd; import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; @@ -494,6 +509,7 @@ import org.apache.cloudstack.api.command.user.vm.RemoveIpFromVmNicCmd; import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; import org.apache.cloudstack.api.command.user.vm.StartVMCmd; @@ -760,6 +776,9 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; +import static com.cloud.configuration.ConfigurationManagerImpl.VM_USERDATA_MAX_LENGTH; +import static com.cloud.vm.UserVmManager.MAX_USER_DATA_LENGTH_BYTES; + public class ManagementServerImpl extends ManagerBase implements ManagementServer, Configurable { public static final Logger s_logger = Logger.getLogger(ManagementServerImpl.class.getName()); @@ -768,6 +787,10 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe static final ConfigKey humanReadableSizes = new ConfigKey("Advanced", Boolean.class, "display.human.readable.sizes", "true", "Enables outputting human readable byte sizes to logs and usage records.", false, ConfigKey.Scope.Global); public static final ConfigKey customCsIdentifier = new ConfigKey("Advanced", String.class, "custom.cs.identifier", UUID.randomUUID().toString().split("-")[0].substring(4), "Custom identifier for the cloudstack installation", true, ConfigKey.Scope.Global); + private static final int MAX_HTTP_GET_LENGTH = 2 * MAX_USER_DATA_LENGTH_BYTES; + private static final int NUM_OF_2K_BLOCKS = 512; + private static final int MAX_HTTP_POST_LENGTH = NUM_OF_2K_BLOCKS * MAX_USER_DATA_LENGTH_BYTES; + @Inject public AccountManager _accountMgr; @Inject @@ -801,7 +824,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe @Inject private UserDao _userDao; @Inject - private UserVmDao _userVmDao; + protected UserVmDao _userVmDao; @Inject private ConfigurationDao _configDao; @Inject @@ -899,9 +922,19 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe @Inject private VpcDao _vpcDao; @Inject - private AnnotationDao annotationDao; - @Inject private DomainVlanMapDao _domainVlanMapDao; + @Inject + private NicDao nicDao; + @Inject + DomainRouterDao routerDao; + @Inject + public UUIDManager uuidMgr; + @Inject + protected UserDataDao userDataDao; + @Inject + protected VMTemplateDao templateDao; + @Inject + protected AnnotationDao annotationDao; private LockControllerListener _lockControllerListener; private final ScheduledExecutorService _eventExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("EventChecker")); @@ -3332,6 +3365,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(RemoveNicFromVMCmd.class); cmdList.add(ResetVMPasswordCmd.class); cmdList.add(ResetVMSSHKeyCmd.class); + cmdList.add(ResetVMUserDataCmd.class); cmdList.add(RestoreVMCmd.class); cmdList.add(StartVMCmd.class); cmdList.add(StopVMCmd.class); @@ -3544,6 +3578,12 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(ChangeOutOfBandManagementPasswordCmd.class); cmdList.add(GetUserKeysCmd.class); cmdList.add(CreateConsoleEndpointCmd.class); + + //user data APIs + cmdList.add(RegisterUserDataCmd.class); + cmdList.add(DeleteUserDataCmd.class); + cmdList.add(ListUserDataCmd.class); + cmdList.add(LinkUserDataToTemplateCmd.class); return cmdList; } @@ -4255,6 +4295,180 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe return createAndSaveSSHKeyPair(name, fingerprint, publicKey, null, owner); } + @Override + public boolean deleteUserData(final DeleteUserDataCmd cmd) { + final Account caller = getCaller(); + final String accountName = cmd.getAccountName(); + final Long domainId = cmd.getDomainId(); + final Long projectId = cmd.getProjectId(); + + Account owner = null; + try { + owner = _accountMgr.finalizeOwner(caller, accountName, domainId, projectId); + } catch (InvalidParameterValueException ex) { + if (caller.getType() == Account.ACCOUNT_TYPE_ADMIN && accountName != null && domainId != null) { + owner = _accountDao.findAccountIncludingRemoved(accountName, domainId); + } + if (owner == null) { + throw ex; + } + } + + final UserDataVO userData = userDataDao.findById(cmd.getId()); + if (userData == null) { + final InvalidParameterValueException ex = new InvalidParameterValueException( + "A UserData with id '" + cmd.getId() + "' does not exist for account " + owner.getAccountName() + " in specified domain id"); + final DomainVO domain = ApiDBUtils.findDomainById(owner.getDomainId()); + String domainUuid = String.valueOf(owner.getDomainId()); + if (domain != null) { + domainUuid = domain.getUuid(); + } + ex.addProxyObject(domainUuid, "domainId"); + throw ex; + } + + List templatesLinkedToUserData = templateDao.findTemplatesLinkedToUserdata(userData.getId()); + if (CollectionUtils.isNotEmpty(templatesLinkedToUserData)) { + throw new CloudRuntimeException(String.format("Userdata %s cannot be removed as it is linked to active template/templates", userData.getName())); + } + + List userVMsHavingUserdata = _userVmDao.findByUserDataId(userData.getId()); + if (CollectionUtils.isNotEmpty(userVMsHavingUserdata)) { + throw new CloudRuntimeException(String.format("Userdata %s cannot be removed as it is being used by some VMs", userData.getName())); + } + + annotationDao.removeByEntityType(AnnotationService.EntityType.USER_DATA.name(), userData.getUuid()); + + return userDataDao.remove(userData.getId()); + } + + @Override + public Pair, Integer> listUserDatas(final ListUserDataCmd cmd) { + final Long id = cmd.getId(); + final String name = cmd.getName(); + final String keyword = cmd.getKeyword(); + + final Account caller = getCaller(); + final List permittedAccounts = new ArrayList(); + + final Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); + _accountMgr.buildACLSearchParameters(caller, null, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + final Long domainId = domainIdRecursiveListProject.first(); + final Boolean isRecursive = domainIdRecursiveListProject.second(); + final ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); + final SearchBuilder sb = userDataDao.createSearchBuilder(); + _accountMgr.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + final Filter searchFilter = new Filter(UserDataVO.class, "id", false, cmd.getStartIndex(), cmd.getPageSizeVal()); + + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.LIKE); + final SearchCriteria sc = sb.create(); + _accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + + if (id != null) { + sc.setParameters("id", id); + } + + if (name != null) { + sc.setParameters("name", name); + } + + if (keyword != null) { + sc.setParameters("name", "%" + keyword + "%"); + } + + final Pair, Integer> result = userDataDao.searchAndCount(sc, searchFilter); + return new Pair<>(result.first(), result.second()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_REGISTER_USER_DATA, eventDescription = "registering userdata", async = true) + public UserData registerUserData(final RegisterUserDataCmd cmd) { + final Account owner = getOwner(cmd); + checkForUserDataByName(cmd, owner); + checkForUserData(cmd, owner); + + final String name = cmd.getName(); + String userdata = cmd.getUserData(); + final String params = cmd.getParams(); + + userdata = validateUserData(userdata, cmd.getHttpMethod()); + + return createAndSaveUserData(name, userdata, params, owner); + } + + private String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) { + byte[] decodedUserData = null; + if (userData != null) { + + if (userData.contains("%")) { + try { + userData = URLDecoder.decode(userData, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new InvalidParameterValueException("Url decoding of userdata failed."); + } + } + + if (!Base64.isBase64(userData)) { + throw new InvalidParameterValueException("User data is not base64 encoded"); + } + // If GET, use 4K. If POST, support up to 1M. + if (httpmethod.equals(BaseCmd.HTTPMethod.GET)) { + decodedUserData = validateAndDecodeByHTTPmethod(userData, MAX_HTTP_GET_LENGTH, BaseCmd.HTTPMethod.GET); + } else if (httpmethod.equals(BaseCmd.HTTPMethod.POST)) { + decodedUserData = validateAndDecodeByHTTPmethod(userData, MAX_HTTP_POST_LENGTH, BaseCmd.HTTPMethod.POST); + } + + if (decodedUserData == null || decodedUserData.length < 1) { + throw new InvalidParameterValueException("User data is too short"); + } + // Re-encode so that the '=' paddings are added if necessary since 'isBase64' does not require it, but python does on the VR. + return Base64.encodeBase64String(decodedUserData); + } + return null; + } + + private byte[] validateAndDecodeByHTTPmethod(String userData, int maxHTTPlength, BaseCmd.HTTPMethod httpMethod) { + byte[] decodedUserData = null; + + if (userData.length() >= maxHTTPlength) { + throw new InvalidParameterValueException(String.format("User data is too long for an http %s request", httpMethod.toString())); + } + if (userData.length() > VM_USERDATA_MAX_LENGTH.value()) { + throw new InvalidParameterValueException("User data has exceeded configurable max length : " + VM_USERDATA_MAX_LENGTH.value()); + } + decodedUserData = Base64.decodeBase64(userData.getBytes()); + if (decodedUserData.length > maxHTTPlength) { + throw new InvalidParameterValueException(String.format("User data is too long for http %s request", httpMethod.toString())); + } + return decodedUserData; + } + + /** + * @param cmd + * @param owner + * @throws InvalidParameterValueException + */ + private void checkForUserData(final RegisterUserDataCmd cmd, final Account owner) throws InvalidParameterValueException { + final UserDataVO userData = userDataDao.findByUserData(owner.getAccountId(), owner.getDomainId(), cmd.getUserData()); + if (userData != null) { + throw new InvalidParameterValueException(String.format("Userdata %s with same content already exists for this account.", userData.getName())); + } + } + + /** + * @param cmd + * @param owner + * @throws InvalidParameterValueException + */ + private void checkForUserDataByName(final RegisterUserDataCmd cmd, final Account owner) throws InvalidParameterValueException { + final UserDataVO userData = userDataDao.findByName(owner.getAccountId(), owner.getDomainId(), cmd.getName()); + if (userData != null) { + throw new InvalidParameterValueException(String.format("A userdata with name %s already exists for this account.", cmd.getName())); + } + } + /** * @param cmd * @param owner @@ -4313,6 +4527,15 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe return owner; } + /** + * @param cmd + * @return Account + */ + protected Account getOwner(final RegisterUserDataCmd cmd) { + final Account caller = getCaller(); + return _accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + } + /** * @return */ @@ -4336,6 +4559,20 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe return newPair; } + private UserData createAndSaveUserData(final String name, final String userdata, final String params, final Account owner) { + final UserDataVO userDataVO = new UserDataVO(); + + userDataVO.setAccountId(owner.getAccountId()); + userDataVO.setDomainId(owner.getDomainId()); + userDataVO.setName(name); + userDataVO.setUserData(userdata); + userDataVO.setParams(params); + + userDataDao.persist(userDataVO); + + return userDataVO; + } + @Override public String getVMPassword(GetVMPasswordCmd cmd) { Account caller = getCaller(); diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index dc7b262fca2..203c9172d9c 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -33,6 +33,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.user.UserData; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListTemplateOrIsoPermissionsCmd; @@ -54,6 +55,7 @@ import org.apache.cloudstack.api.command.user.template.ListTemplatePermissionsCm import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateTemplatePermissionsCmd; +import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; import org.apache.cloudstack.api.response.GetUploadParamsResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; @@ -2265,4 +2267,41 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, return _tmpltSvr.getTemplateDatadisksOnImageStore(templateObject, configurationId); } + @Override + public VirtualMachineTemplate linkUserDataToTemplate(LinkUserDataToTemplateCmd cmd) { + Long templateId = cmd.getTemplateId(); + Long isoId = cmd.getIsoId(); + Long userDataId = cmd.getUserdataId(); + UserData.UserDataOverridePolicy overridePolicy = cmd.getUserdataPolicy(); + Account caller = CallContext.current().getCallingAccount(); + + if (templateId != null && isoId != null) { + throw new InvalidParameterValueException("Both template ID and ISO ID are passed, API accepts only one"); + } + if (templateId == null && isoId == null) { + throw new InvalidParameterValueException("Atleast one of template ID or ISO ID needs to be passed"); + } + + VMTemplateVO template = null; + if (templateId != null) { + template = _tmpltDao.findById(templateId); + } else { + template = _tmpltDao.findById(isoId); + } + if (template == null) { + throw new InvalidParameterValueException(String.format("unable to find template/ISO with id %s", templateId == null? isoId : templateId)); + } + + _accountMgr.checkAccess(caller, AccessType.OperateEntry, true, template); + + template.setUserDataId(userDataId); + if (userDataId != null) { + template.setUserDataLinkPolicy(overridePolicy); + } else { + template.setUserDataLinkPolicy(null); + } + _tmpltDao.update(template.getId(), template); + + return _tmpltDao.findById(template.getId()); + } } diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index cde2d04970d..98246396f8a 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -120,7 +120,7 @@ public interface UserVmManager extends UserVmService { boolean setupVmForPvlan(boolean add, Long hostId, NicProfile nic); UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha, Boolean isDisplayVmEnabled, Long osTypeId, String userData, - Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List securityGroupIdList, Map> extraDhcpOptionsMap) throws ResourceUnavailableException, InsufficientCapacityException; + Long userDataId, String userDataDetails, Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List securityGroupIdList, Map> extraDhcpOptionsMap) throws ResourceUnavailableException, InsufficientCapacityException; //the validateCustomParameters, save and remove CustomOfferingDetils functions can be removed from the interface once we can //find a common place for all the scaling and upgrading code of both user and systemvms. diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 58e1d7194a9..3bc93b39917 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -76,6 +76,7 @@ import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction; @@ -323,8 +324,10 @@ import com.cloud.user.VmDiskStatisticsVO; import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.SSHKeyPairDao; import com.cloud.user.dao.UserDao; +import com.cloud.user.dao.UserDataDao; import com.cloud.user.dao.UserStatisticsDao; import com.cloud.user.dao.VmDiskStatisticsDao; +import com.cloud.user.UserData; import com.cloud.uservm.UserVm; import com.cloud.utils.DateUtil; import com.cloud.utils.Journal; @@ -552,10 +555,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private AnnotationDao annotationDao; @Inject protected CommandSetupHelper commandSetupHelper; + @Inject + private UserDataDao _userDataDao; @Autowired @Qualifier("networkHelper") protected NetworkHelper nwHelper; @Inject + private UserDataDao userDataDao; + @Inject ReservationDao reservationDao; @Inject ResourceLimitService resourceLimitService; @@ -884,6 +891,49 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_RESETUSERDATA, eventDescription = "resetting VM userdata", async = true) + public UserVm resetVMUserData(ResetVMUserDataCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException { + + Account caller = CallContext.current().getCallingAccount(); + Long vmId = cmd.getId(); + UserVmVO userVm = _vmDao.findById(cmd.getId()); + + if (userVm == null) { + throw new InvalidParameterValueException("unable to find a virtual machine by id" + cmd.getId()); + } + _accountMgr.checkAccess(caller, null, true, userVm); + + VMTemplateVO template = _templateDao.findByIdIncludingRemoved(userVm.getTemplateId()); + + // Do parameters input validation + + if (userVm.getState() != State.Stopped) { + s_logger.error("vm is not in the right state: " + vmId); + throw new InvalidParameterValueException(String.format("VM %s should be stopped to do UserData reset", userVm)); + } + + String userData = cmd.getUserData(); + Long userDataId = cmd.getUserdataId(); + String userDataDetails = null; + if (MapUtils.isNotEmpty(cmd.getUserdataDetails())) { + userDataDetails = cmd.getUserdataDetails().toString(); + } + userData = finalizeUserData(userData, userDataId, template); + userData = validateUserData(userData, cmd.getHttpMethod()); + + userVm.setUserDataId(userDataId); + userVm.setUserData(userData); + userVm.setUserDataDetails(userDataDetails); + _vmDao.update(userVm.getId(), userVm); + + updateUserData(userVm); + + UserVmVO vm = _vmDao.findById(vmId); + _vmDao.loadDetails(vm); + return vm; + } + @Override @ActionEvent(eventType = EventTypes.EVENT_VM_RESETSSHKEY, eventDescription = "resetting Vm SSHKey", async = true) public UserVm resetVMSSHKey(ResetVMSSHKeyCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException { @@ -2430,6 +2480,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return false; } + if (vm.getUserDataId() != null) { + vm.setUserDataId(null); + _vmDao.update(vm.getId(), vm); + } + _vmDao.remove(vm.getId()); } @@ -2710,7 +2765,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Boolean isDisplayVm = cmd.getDisplayVm(); Long id = cmd.getId(); Long osTypeId = cmd.getOsTypeId(); - String userData = cmd.getUserData(); Boolean isDynamicallyScalable = cmd.isDynamicallyScalable(); String hostName = cmd.getHostName(); Map details = cmd.getDetails(); @@ -2719,12 +2773,19 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir String extraConfig = cmd.getExtraConfig(); UserVmVO vmInstance = _vmDao.findById(cmd.getId()); + VMTemplateVO template = _templateDao.findById(vmInstance.getTemplateId()); if (MapUtils.isNotEmpty(details) || cmd.isCleanupDetails()) { - VMTemplateVO template = _templateDao.findById(vmInstance.getTemplateId()); if (template != null && template.isDeployAsIs()) { throw new CloudRuntimeException("Detail settings are read from OVA, it cannot be changed by API call."); } } + String userData = cmd.getUserData(); + Long userDataId = cmd.getUserdataId(); + String userDataDetails = null; + if (MapUtils.isNotEmpty(cmd.getUserdataDetails())) { + userDataDetails = cmd.getUserdataDetails().toString(); + } + userData = finalizeUserData(userData, userDataId, template); long accountId = vmInstance.getAccountId(); @@ -2786,7 +2847,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } } - return updateVirtualMachine(id, displayName, group, ha, isDisplayVm, osTypeId, userData, isDynamicallyScalable, + return updateVirtualMachine(id, displayName, group, ha, isDisplayVm, osTypeId, userData, userDataId, userDataDetails, isDynamicallyScalable, cmd.getHttpMethod(), cmd.getCustomId(), hostName, cmd.getInstanceName(), securityGroupIdList, cmd.getDhcpOptionsMap()); } @@ -2884,7 +2945,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override public UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha, Boolean isDisplayVmEnabled, Long osTypeId, String userData, - Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List securityGroupIdList, Map> extraDhcpOptionsMap) + Long userDataId, String userDataDetails, Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List securityGroupIdList, Map> extraDhcpOptionsMap) throws ResourceUnavailableException, InsufficientCapacityException { UserVmVO vm = _vmDao.findById(id); if (vm == null) { @@ -2931,6 +2992,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir userData = vm.getUserData(); } + if (userDataId == null) { + userDataId = vm.getUserDataId(); + } + + if (userDataDetails == null) { + userDataDetails = vm.getUserDataDetails(); + } + if (osTypeId == null) { osTypeId = vm.getGuestOSId(); } @@ -3026,7 +3095,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir .getUuid(), nic.getId(), extraDhcpOptionsMap); } - _vmDao.updateVM(id, displayName, ha, osTypeId, userData, isDisplayVmEnabled, isDynamicallyScalable, customId, hostName, instanceName); + _vmDao.updateVM(id, displayName, ha, osTypeId, userData, userDataId, userDataDetails, isDisplayVmEnabled, isDynamicallyScalable, customId, hostName, instanceName); if (updateUserdata) { updateUserData(vm); @@ -3039,7 +3108,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return _vmDao.findById(id); } - private void updateUserData(UserVm vm) throws ResourceUnavailableException, InsufficientCapacityException { + protected void updateUserData(UserVm vm) throws ResourceUnavailableException, InsufficientCapacityException { boolean result = updateUserDataInternal(vm); if (result) { s_logger.debug(String.format("User data successfully updated for vm id: %s", vm.getId())); @@ -3448,7 +3517,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm", create = true) public UserVm createBasicSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, - String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, + String userData, Long userDataId, String userDataDetails, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParametes, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { @@ -3498,7 +3567,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, - userData, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParametes, customId, dhcpOptionMap, + userData, userDataId, userDataDetails, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParametes, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null); } @@ -3507,7 +3576,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm", create = true) public UserVm createAdvancedSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List networkIdList, List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, - HTTPMethod httpmethod, String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, + HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { @@ -3609,15 +3678,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, - userData, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap, + userData, userDataId, userDataDetails, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null); } @Override @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm", create = true) public UserVm createAdvancedVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List networkIdList, Account owner, - String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, - String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayvm, String keyboard, List affinityGroupIdList, + String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, Long userDataId, + String userDataDetails, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayvm, String keyboard, List affinityGroupIdList, Map customParametrs, String customId, Map> dhcpOptionsMap, Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String type) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { @@ -3670,7 +3739,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir verifyExtraDhcpOptionsNetwork(dhcpOptionsMap, networkList); return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, null, group, httpmethod, userData, - sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayvm, keyboard, affinityGroupIdList, customParametrs, customId, dhcpOptionsMap, + userDataId, userDataDetails, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayvm, keyboard, affinityGroupIdList, customParametrs, customId, dhcpOptionsMap, dataDiskTemplateToDiskOfferingMap, userVmOVFPropertiesMap, dynamicScalingEnabled, type); } @@ -3787,7 +3856,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @DB private UserVm createVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate tmplt, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List networkList, List securityGroupIdList, String group, HTTPMethod httpmethod, String userData, - String sshKeyPair, HypervisorType hypervisor, Account caller, Map requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, + Long userDataId, String userDataDetails, String sshKeyPair, HypervisorType hypervisor, Account caller, Map requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map datadiskTemplateToDiskOfferringMap, Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String type) throws InsufficientCapacityException, ResourceUnavailableException, @@ -3885,7 +3954,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir _volumeService.validateVolumeSizeInBytes(size); volumesSize += size; } - UserVm vm = getCheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, type, template, hypervisorType, accountId, offering, isIso, volumesSize); + UserVm vm = getCheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, type, template, hypervisorType, accountId, offering, isIso, volumesSize); _securityGroupMgr.addInstanceToGroups(vm.getId(), securityGroupIdList); @@ -3897,13 +3966,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return vm; } - private UserVm getCheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List networkList, List securityGroupIdList, String group, HTTPMethod httpmethod, String userData, String sshKeyPair, Account caller, Map requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map datadiskTemplateToDiskOfferringMap, Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, long volumesSize) throws ResourceAllocationException, StorageUnavailableException, InsufficientCapacityException { + private UserVm getCheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List networkList, List securityGroupIdList, String group, HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, String sshKeyPair, Account caller, Map requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map datadiskTemplateToDiskOfferringMap, Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, long volumesSize) throws ResourceAllocationException, StorageUnavailableException, InsufficientCapacityException { if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { try (CheckedReservation vmReservation = new CheckedReservation(owner, ResourceType.user_vm, 1l, reservationDao, resourceLimitService); CheckedReservation cpuReservation = new CheckedReservation(owner, ResourceType.cpu, Long.valueOf(offering.getCpu()), reservationDao, resourceLimitService); CheckedReservation memReservation = new CheckedReservation(owner, ResourceType.memory, Long.valueOf(offering.getRamSize()), reservationDao, resourceLimitService); ) { - return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, volumesSize); + return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, volumesSize); } catch (ResourceAllocationException | CloudRuntimeException e) { throw e; } catch (Exception e) { @@ -3912,11 +3981,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } else { - return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, volumesSize); + return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, volumesSize); } } - private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List networkList, List securityGroupIdList, String group, HTTPMethod httpmethod, String userData, String sshKeyPair, Account caller, Map requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map datadiskTemplateToDiskOfferringMap, Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, long volumesSize) throws ResourceAllocationException, StorageUnavailableException, InsufficientCapacityException { + private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List networkList, List securityGroupIdList, String group, HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, String sshKeyPair, Account caller, Map requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map datadiskTemplateToDiskOfferringMap, Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, long volumesSize) throws ResourceAllocationException, StorageUnavailableException, InsufficientCapacityException { try (CheckedReservation volumeReservation = new CheckedReservation(owner, ResourceType.volume, (isIso || diskOfferingId == null ? 1l : 2), reservationDao, resourceLimitService); CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, volumesSize, reservationDao, resourceLimitService)) { @@ -4196,7 +4265,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir dynamicScalingEnabled = dynamicScalingEnabled && checkIfDynamicScalingCanBeEnabled(null, offering, template, zone.getId()); - UserVm vm = commitUserVm(zone, template, hostName, displayName, owner, diskOfferingId, diskSize, userData, caller, isDisplayVm, keyboard, accountId, userId, offering, + UserVm vm = commitUserVm(zone, template, hostName, displayName, owner, diskOfferingId, diskSize, userData, userDataId, userDataDetails, caller, isDisplayVm, keyboard, accountId, userId, offering, isIso, sshPublicKey, networkNicMap, id, instanceName, uuidName, hypervisorType, customParameters, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType); // Assign instance to the group @@ -4308,7 +4377,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } private UserVmVO commitUserVm(final boolean isImport, final DataCenter zone, final Host host, final Host lastHost, final VirtualMachineTemplate template, final String hostName, final String displayName, final Account owner, - final Long diskOfferingId, final Long diskSize, final String userData, final Account caller, final Boolean isDisplayVm, final String keyboard, + final Long diskOfferingId, final Long diskSize, final String userData, Long userDataId, String userDataDetails, final Account caller, final Boolean isDisplayVm, final String keyboard, final long accountId, final long userId, final ServiceOffering offering, final boolean isIso, final String sshPublicKey, final LinkedHashMap> networkNicMap, final long id, final String instanceName, final String uuidName, final HypervisorType hypervisorType, final Map customParameters, final Map> extraDhcpOptionMap, final Map dataDiskTemplateToDiskOfferingMap, @@ -4317,7 +4386,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override public UserVmVO doInTransaction(TransactionStatus status) throws InsufficientCapacityException { UserVmVO vm = new UserVmVO(id, instanceName, displayName, template.getId(), hypervisorType, template.getGuestOSId(), offering.isOfferHA(), - offering.getLimitCpuUse(), owner.getDomainId(), owner.getId(), userId, offering.getId(), userData, hostName, diskOfferingId); + offering.getLimitCpuUse(), owner.getDomainId(), owner.getId(), userId, offering.getId(), userData, userDataId, userDataDetails, hostName, diskOfferingId); vm.setUuid(uuidName); vm.setDynamicallyScalable(dynamicScalingEnabled); @@ -4503,13 +4572,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } private UserVmVO commitUserVm(final DataCenter zone, final VirtualMachineTemplate template, final String hostName, final String displayName, final Account owner, - final Long diskOfferingId, final Long diskSize, final String userData, final Account caller, final Boolean isDisplayVm, final String keyboard, + final Long diskOfferingId, final Long diskSize, final String userData, Long userDataId, String userDataDetails, final Account caller, final Boolean isDisplayVm, final String keyboard, final long accountId, final long userId, final ServiceOfferingVO offering, final boolean isIso, final String sshPublicKey, final LinkedHashMap> networkNicMap, final long id, final String instanceName, final String uuidName, final HypervisorType hypervisorType, final Map customParameters, final Map> extraDhcpOptionMap, final Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFPropertiesMap, final boolean dynamicScalingEnabled, String type) throws InsufficientCapacityException { return commitUserVm(false, zone, null, null, template, hostName, displayName, owner, - diskOfferingId, diskSize, userData, caller, isDisplayVm, keyboard, + diskOfferingId, diskSize, userData, userDataId, userDataDetails, caller, isDisplayVm, keyboard, accountId, userId, offering, isIso, sshPublicKey, networkNicMap, id, instanceName, uuidName, hypervisorType, customParameters, extraDhcpOptionMap, dataDiskTemplateToDiskOfferingMap, @@ -4880,7 +4949,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir final String serviceOffering = _serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId()).getDisplayText(); boolean isWindows = _guestOSCategoryDao.findById(_guestOSDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows"); String destHostname = VirtualMachineManager.getHypervisorHostname(dest.getHost() != null ? dest.getHost().getName() : ""); - List vmData = _networkModel.generateVmData(vm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), + List vmData = _networkModel.generateVmData(vm.getUserData(), vm.getUserDataDetails(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), vm.getUuid(), defaultNic.getIPv4Address(), vm.getDetail(VmDetailConstants.SSH_PUBLIC_KEY), (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows, destHostname); String vmName = vm.getInstanceName(); String configDriveIsoRootFolder = "/tmp"; @@ -5629,6 +5698,73 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return userVm.getHypervisorType(); } + protected String finalizeUserData(String userData, Long userDataId, VirtualMachineTemplate template) { + if (StringUtils.isEmpty(userData) && userDataId == null && (template == null || template.getUserDataId() == null)) { + return null; + } + + if (userDataId != null && StringUtils.isNotEmpty(userData)) { + throw new InvalidParameterValueException("Both userdata and userdata ID inputs are not allowed, please provide only one"); + } + if (template != null && template.getUserDataId() != null) { + switch (template.getUserDataOverridePolicy()) { + case DENYOVERRIDE: + if (StringUtils.isNotEmpty(userData) || userDataId != null) { + String msg = String.format("UserData input is not allowed here since template %s is configured to deny any userdata", template.getName()); + throw new CloudRuntimeException(msg); + } + case ALLOWOVERRIDE: + if (userDataId != null) { + UserData apiUserDataVO = userDataDao.findById(userDataId); + return apiUserDataVO.getUserData(); + } else if (StringUtils.isNotEmpty(userData)) { + return userData; + } else { + UserData templateUserDataVO = userDataDao.findById(template.getUserDataId()); + if (templateUserDataVO == null) { + String msg = String.format("UserData linked to the template %s is not found", template.getName()); + throw new CloudRuntimeException(msg); + } + return templateUserDataVO.getUserData(); + } + case APPEND: + UserData templateUserDataVO = userDataDao.findById(template.getUserDataId()); + if (templateUserDataVO == null) { + String msg = String.format("UserData linked to the template %s is not found", template.getName()); + throw new CloudRuntimeException(msg); + } + if (userDataId != null) { + UserData apiUserDataVO = userDataDao.findById(userDataId); + return doConcateUserDatas(templateUserDataVO.getUserData(), apiUserDataVO.getUserData()); + } else if (StringUtils.isNotEmpty(userData)) { + return doConcateUserDatas(templateUserDataVO.getUserData(), userData); + } else { + return templateUserDataVO.getUserData(); + } + default: + String msg = String.format("This userdataPolicy %s is not supported for use with this feature", template.getUserDataOverridePolicy().toString()); + throw new CloudRuntimeException(msg); } + } else { + if (userDataId != null) { + UserData apiUserDataVO = userDataDao.findById(userDataId); + return apiUserDataVO.getUserData(); + } else if (StringUtils.isNotEmpty(userData)) { + return userData; + } + } + return null; + } + + private String doConcateUserDatas(String userdata1, String userdata2) { + byte[] userdata1Bytes = Base64.decodeBase64(userdata1.getBytes()); + byte[] userdata2Bytes = Base64.decodeBase64(userdata2.getBytes()); + byte[] finalUserDataBytes = new byte[userdata1Bytes.length + userdata2Bytes.length]; + System.arraycopy(userdata1Bytes, 0, finalUserDataBytes, 0, userdata1Bytes.length); + System.arraycopy(userdata2Bytes, 0, finalUserDataBytes, userdata1Bytes.length, userdata2Bytes.length); + + return Base64.encodeBase64String(finalUserDataBytes); + } + @Override public UserVm createVirtualMachine(DeployVMCmd cmd) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException, StorageUnavailableException, ResourceAllocationException { @@ -5710,6 +5846,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir networkIds = new ArrayList<>(userVmNetworkMap.values()); } + String userData = cmd.getUserData(); + Long userDataId = cmd.getUserdataId(); + String userDataDetails = null; + if (MapUtils.isNotEmpty(cmd.getUserdataDetails())) { + userDataDetails = cmd.getUserdataDetails().toString(); + } + userData = finalizeUserData(userData, userDataId, template); + Account caller = CallContext.current().getCallingAccount(); Long callerId = caller.getId(); @@ -5727,7 +5871,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir IpAddresses addrs = new IpAddresses(ipAddress, ip6Address, macAddress); Long size = cmd.getSize(); String group = cmd.getGroup(); - String userData = cmd.getUserData(); String sshKeyPairName = cmd.getSSHKeyPairName(); Boolean displayVm = cmd.isDisplayVm(); String keyboard = cmd.getKeyboard(); @@ -5738,14 +5881,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir throw new InvalidParameterValueException("Can't specify network Ids in Basic zone"); } else { vm = createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, getSecurityGroupIdList(cmd), owner, name, displayName, diskOfferingId, - size , group , cmd.getHypervisor(), cmd.getHttpMethod(), userData , sshKeyPairName , cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(), + size , group , cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairName , cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled); } } else { if (zone.isSecurityGroupEnabled()) { vm = createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, networkIds, getSecurityGroupIdList(cmd), owner, name, - displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, + displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled); @@ -5754,7 +5897,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir throw new InvalidParameterValueException("Can't create vm with security groups; security group feature is not enabled per zone"); } vm = createAdvancedVirtualMachine(zone, serviceOffering, template, networkIds, owner, name, displayName, diskOfferingId, size, group, - cmd.getHypervisor(), cmd.getHttpMethod(), userData, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), + cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null); } } @@ -7900,7 +8043,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir final Host lastHost = powerState != VirtualMachine.PowerState.PowerOn ? host : null; final Boolean dynamicScalingEnabled = checkIfDynamicScalingCanBeEnabled(null, serviceOffering, template, zone.getId()); return commitUserVm(true, zone, host, lastHost, template, hostName, displayName, owner, - null, null, userData, caller, isDisplayVm, keyboard, + null, null, userData, null, null, caller, isDisplayVm, keyboard, accountId, userId, serviceOffering, template.getFormat().equals(ImageFormat.ISO), sshPublicKey, null, id, instanceName, uuidName, hypervisorType, customParameters, null, null, null, powerState, dynamicScalingEnabled, null); diff --git a/server/src/main/java/org/apache/cloudstack/annotation/AnnotationManagerImpl.java b/server/src/main/java/org/apache/cloudstack/annotation/AnnotationManagerImpl.java index ecbce898d6d..c65b6d12822 100644 --- a/server/src/main/java/org/apache/cloudstack/annotation/AnnotationManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/annotation/AnnotationManagerImpl.java @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.user.dao.UserDataDao; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; @@ -155,6 +156,8 @@ public final class AnnotationManagerImpl extends ManagerBase implements Annotati @Inject private NetworkOfferingDao networkOfferingDao; @Inject + private UserDataDao userDataDao; + @Inject EntityManager entityManager; private static final List adminRoles = Collections.singletonList(RoleType.Admin); @@ -168,6 +171,7 @@ public final class AnnotationManagerImpl extends ManagerBase implements Annotati s_typeMap.put(EntityType.VM_SNAPSHOT, ApiCommandResourceType.VmSnapshot); s_typeMap.put(EntityType.INSTANCE_GROUP, ApiCommandResourceType.None); s_typeMap.put(EntityType.SSH_KEYPAIR, ApiCommandResourceType.None); + s_typeMap.put(EntityType.USER_DATA, ApiCommandResourceType.None); s_typeMap.put(EntityType.NETWORK, ApiCommandResourceType.Network); s_typeMap.put(EntityType.VPC, ApiCommandResourceType.Vpc); s_typeMap.put(EntityType.PUBLIC_IP_ADDRESS, ApiCommandResourceType.IpAddress); @@ -510,6 +514,8 @@ public final class AnnotationManagerImpl extends ManagerBase implements Annotati return instanceGroupDao.findByUuid(entityUuid); case SSH_KEYPAIR: return sshKeyPairDao.findByUuid(entityUuid); + case USER_DATA: + return userDataDao.findByUuid(entityUuid); case NETWORK: return networkDao.findByUuid(entityUuid); case VPC: diff --git a/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java b/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java index 6fd57fc04d8..360d7378d36 100644 --- a/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java +++ b/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java @@ -898,7 +898,7 @@ public class MockNetworkModelImpl extends ManagerBase implements NetworkModel { } @Override - public List generateVmData(String userData, String serviceOffering, long datacenterId, String vmName, String vmHostName, long vmId, String vmUuid, String guestIpAddress, String publicKey, String password, Boolean isWindows, String hostname) { + public List generateVmData(String userData, String userDataDetails, String serviceOffering, long datacenterId, String vmName, String vmHostName, long vmId, String vmUuid, String guestIpAddress, String publicKey, String password, Boolean isWindows, String hostname) { return null; } diff --git a/server/src/test/java/com/cloud/network/element/ConfigDriveNetworkElementTest.java b/server/src/test/java/com/cloud/network/element/ConfigDriveNetworkElementTest.java index 06ec0b019b7..e93fcae48c9 100644 --- a/server/src/test/java/com/cloud/network/element/ConfigDriveNetworkElementTest.java +++ b/server/src/test/java/com/cloud/network/element/ConfigDriveNetworkElementTest.java @@ -274,7 +274,7 @@ public class ConfigDriveNetworkElementTest { PowerMockito.when(CallContext.current()).thenReturn(callContextMock); Mockito.doReturn(Mockito.mock(Account.class)).when(callContextMock).getCallingAccount(); Method method = ReflectionUtils.getMethods(ConfigDriveBuilder.class, ReflectionUtils.withName("buildConfigDrive")).iterator().next(); - PowerMockito.when(ConfigDriveBuilder.class, method).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.anyString()).thenReturn("content"); + PowerMockito.when(ConfigDriveBuilder.class, method).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.anyString(), Mockito.anyMap()).thenReturn("content"); final HandleConfigDriveIsoAnswer answer = mock(HandleConfigDriveIsoAnswer.class); final UserVmDetailVO userVmDetailVO = mock(UserVmDetailVO.class); diff --git a/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java b/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java new file mode 100644 index 00000000000..30e79edc67a --- /dev/null +++ b/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.network.router; + +import com.cloud.agent.api.routing.VmDataCommand; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(PowerMockRunner.class) +public class CommandSetupHelperTest { + + @InjectMocks + protected CommandSetupHelper commandSetupHelper = new CommandSetupHelper(); + + @Test + public void testUserDataDetails() { + VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); + String testUserDataDetails = new String("{test1=value1,test2=value2}"); + commandSetupHelper.addUserDataDetailsToCommand(vmDataCommand, testUserDataDetails); + + List metadata = vmDataCommand.getVmData(); + String[] metadataFile1 = metadata.get(0); + String[] metadataFile2 = metadata.get(1); + + Assert.assertEquals("metadata", metadataFile1[0]); + Assert.assertEquals("metadata", metadataFile2[0]); + + Assert.assertEquals("test1", metadataFile1[1]); + Assert.assertEquals("test2", metadataFile2[1]); + + Assert.assertEquals("value1", metadataFile1[2]); + Assert.assertEquals("value2", metadataFile2[2]); + } + + @Test + public void testNullUserDataDetails() { + VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); + String testUserDataDetails = null; + commandSetupHelper.addUserDataDetailsToCommand(vmDataCommand, testUserDataDetails); + Assert.assertEquals(new ArrayList<>(), vmDataCommand.getVmData()); + } + + @Test + public void testUserDataDetailsWithWhiteSpaces() { + VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); + String testUserDataDetails = new String("{test1 =value1,test2= value2 }"); + commandSetupHelper.addUserDataDetailsToCommand(vmDataCommand, testUserDataDetails); + + List metadata = vmDataCommand.getVmData(); + String[] metadataFile1 = metadata.get(0); + String[] metadataFile2 = metadata.get(1); + + Assert.assertEquals("metadata", metadataFile1[0]); + Assert.assertEquals("metadata", metadataFile2[0]); + + Assert.assertEquals("test1", metadataFile1[1]); + Assert.assertEquals("test2", metadataFile2[1]); + + Assert.assertEquals("value1", metadataFile1[2]); + Assert.assertEquals("value2", metadataFile2[2]); + } +} diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index 488c5f5717a..247009f6361 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -16,28 +16,66 @@ // under the License. package com.cloud.server; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.user.AccountManager; +import com.cloud.user.User; +import com.cloud.user.UserData; +import com.cloud.user.UserDataVO; +import com.cloud.user.dao.UserDataDao; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.UserVmDao; +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.user.ssh.RegisterSSHKeyPairCmd; +import org.apache.cloudstack.api.command.user.userdata.DeleteUserDataCmd; +import org.apache.cloudstack.api.command.user.userdata.ListUserDataCmd; +import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.IpAddressManagerImpl; +import com.cloud.network.dao.IPAddressVO; import com.cloud.user.Account; import com.cloud.user.SSHKeyPair; import com.cloud.user.SSHKeyPairVO; import com.cloud.user.dao.SSHKeyPairDao; +import com.cloud.utils.db.SearchCriteria; -@RunWith(MockitoJUnitRunner.class) +import java.util.ArrayList; +import java.util.List; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(CallContext.class) public class ManagementServerImplTest { + @Mock + SearchCriteria sc; + @Mock RegisterSSHKeyPairCmd regCmd; @@ -49,13 +87,50 @@ public class ManagementServerImplTest { @Mock SSHKeyPairDao sshKeyPairDao; - ManagementServerImpl ms = new ManagementServerImpl(); @Mock SSHKeyPair sshKeyPair; + @Mock + IpAddressManagerImpl ipAddressManagerImpl; + + @Mock + AccountManager _accountMgr; + + @Mock + UserDataDao _userDataDao; + + @Mock + VMTemplateDao _templateDao; + + @Mock + AnnotationDao annotationDao; + + @Mock + UserVmDao _userVmDao; + @Spy - ManagementServerImpl spy; + ManagementServerImpl spy = new ManagementServerImpl(); + + ConfigKey mockConfig; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + CallContext.register(Mockito.mock(User.class), Mockito.mock(Account.class)); + mockConfig = Mockito.mock(ConfigKey.class); + Whitebox.setInternalState(ipAddressManagerImpl.getClass(), "SystemVmPublicIpReservationModeStrictness", mockConfig); + spy._accountMgr = _accountMgr; + spy.userDataDao = _userDataDao; + spy.templateDao = _templateDao; + spy._userVmDao = _userVmDao; + spy.annotationDao = annotationDao; + } + + @After + public void tearDown() throws Exception { + CallContext.unregister(); + } @Test(expected = InvalidParameterValueException.class) public void testDuplicateRegistraitons(){ @@ -107,4 +182,264 @@ public class ManagementServerImplTest { spy.registerSSHKeyPair(regCmd); Mockito.verify(spy, Mockito.times(3)).getPublicKeyFromKeyKeyMaterial(anyString()); } + + @Test + public void testSuccessfulRegisterUserdata() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + RegisterUserDataCmd cmd = Mockito.mock(RegisterUserDataCmd.class); + when(cmd.getUserData()).thenReturn("testUserdata"); + when(cmd.getName()).thenReturn("testName"); + when(cmd.getHttpMethod()).thenReturn(BaseCmd.HTTPMethod.GET); + + when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(null); + when(_userDataDao.findByUserData(account.getAccountId(), account.getDomainId(), "testUserdata")).thenReturn(null); + + UserData userData = spy.registerUserData(cmd); + Assert.assertEquals("testName", userData.getName()); + Assert.assertEquals("testUserdata", userData.getUserData()); + Assert.assertEquals(1L, userData.getAccountId()); + Assert.assertEquals(2L, userData.getDomainId()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testRegisterExistingUserdata() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + RegisterUserDataCmd cmd = Mockito.mock(RegisterUserDataCmd.class); + when(cmd.getUserData()).thenReturn("testUserdata"); + when(cmd.getName()).thenReturn("testName"); + + UserDataVO userData = Mockito.mock(UserDataVO.class); + when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(null); + when(_userDataDao.findByUserData(account.getAccountId(), account.getDomainId(), "testUserdata")).thenReturn(userData); + + spy.registerUserData(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testRegisterExistingName() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + PowerMockito.when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + RegisterUserDataCmd cmd = Mockito.mock(RegisterUserDataCmd.class); + when(cmd.getUserData()).thenReturn("testUserdata"); + when(cmd.getName()).thenReturn("testName"); + + UserDataVO userData = Mockito.mock(UserDataVO.class); + when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(userData); + + spy.registerUserData(cmd); + } + + @Test + public void testSuccessfulDeleteUserdata() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + DeleteUserDataCmd cmd = Mockito.mock(DeleteUserDataCmd.class); + when(cmd.getAccountName()).thenReturn("testAccountName"); + when(cmd.getDomainId()).thenReturn(1L); + when(cmd.getProjectId()).thenReturn(2L); + when(cmd.getId()).thenReturn(1L); + UserDataVO userData = Mockito.mock(UserDataVO.class); + + Mockito.when(userData.getId()).thenReturn(1L); + when(_userDataDao.findById(1L)).thenReturn(userData); + when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(null); + when(_templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(new ArrayList()); + when(_userVmDao.findByUserDataId(1L)).thenReturn(new ArrayList()); + when(_userDataDao.remove(1L)).thenReturn(true); + + boolean result = spy.deleteUserData(cmd); + Assert.assertEquals(true, result); + } + + @Test(expected = CloudRuntimeException.class) + public void testDeleteUserdataLinkedToTemplate() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + DeleteUserDataCmd cmd = Mockito.mock(DeleteUserDataCmd.class); + when(cmd.getAccountName()).thenReturn("testAccountName"); + when(cmd.getDomainId()).thenReturn(1L); + when(cmd.getProjectId()).thenReturn(2L); + when(cmd.getId()).thenReturn(1L); + + UserDataVO userData = Mockito.mock(UserDataVO.class); + Mockito.when(userData.getId()).thenReturn(1L); + when(_userDataDao.findById(1L)).thenReturn(userData); + when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(null); + + VMTemplateVO vmTemplateVO = Mockito.mock(VMTemplateVO.class); + List linkedTemplates = new ArrayList<>(); + linkedTemplates.add(vmTemplateVO); + when(_templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(linkedTemplates); + + spy.deleteUserData(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testDeleteUserdataUsedByVM() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + DeleteUserDataCmd cmd = Mockito.mock(DeleteUserDataCmd.class); + when(cmd.getAccountName()).thenReturn("testAccountName"); + when(cmd.getDomainId()).thenReturn(1L); + when(cmd.getProjectId()).thenReturn(2L); + when(cmd.getId()).thenReturn(1L); + + UserDataVO userData = Mockito.mock(UserDataVO.class); + Mockito.when(userData.getId()).thenReturn(1L); + when(_userDataDao.findById(1L)).thenReturn(userData); + when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(null); + + when(_templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(new ArrayList()); + + UserVmVO userVmVO = Mockito.mock(UserVmVO.class); + List vms = new ArrayList<>(); + vms.add(userVmVO); + when(_userVmDao.findByUserDataId(1L)).thenReturn(vms); + + spy.deleteUserData(cmd); + } + + @Test + public void testListUserDataById() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + ListUserDataCmd cmd = Mockito.mock(ListUserDataCmd.class); + when(cmd.getAccountName()).thenReturn("testAccountName"); + when(cmd.getDomainId()).thenReturn(1L); + when(cmd.getProjectId()).thenReturn(2L); + when(cmd.getId()).thenReturn(1L); + when(cmd.isRecursive()).thenReturn(false); + UserDataVO userData = Mockito.mock(UserDataVO.class); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + when(_userDataDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(userData); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + when(sb.create()).thenReturn(sc); + + List userDataList = new ArrayList(); + userDataList.add(userData); + Pair, Integer> result = new Pair(userDataList, 1); + when(_userDataDao.searchAndCount(nullable(SearchCriteria.class), nullable(Filter.class))).thenReturn(result); + + Pair, Integer> userdataResultList = spy.listUserDatas(cmd); + + Assert.assertEquals(userdataResultList.first().get(0), userDataList.get(0)); + } + + @Test + public void testListUserDataByName() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + ListUserDataCmd cmd = Mockito.mock(ListUserDataCmd.class); + when(cmd.getAccountName()).thenReturn("testAccountName"); + when(cmd.getDomainId()).thenReturn(1L); + when(cmd.getProjectId()).thenReturn(2L); + when(cmd.getName()).thenReturn("testSearchUserdataName"); + when(cmd.isRecursive()).thenReturn(false); + UserDataVO userData = Mockito.mock(UserDataVO.class); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + when(_userDataDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(userData); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + when(sb.create()).thenReturn(sc); + + List userDataList = new ArrayList(); + userDataList.add(userData); + Pair, Integer> result = new Pair(userDataList, 1); + when(_userDataDao.searchAndCount(nullable(SearchCriteria.class), nullable(Filter.class))).thenReturn(result); + + Pair, Integer> userdataResultList = spy.listUserDatas(cmd); + + Assert.assertEquals(userdataResultList.first().get(0), userDataList.get(0)); + } + + @Test + public void testListUserDataByKeyword() { + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(2L); + when(callContextMock.getCallingAccount()).thenReturn(account); + when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account); + + ListUserDataCmd cmd = Mockito.mock(ListUserDataCmd.class); + when(cmd.getAccountName()).thenReturn("testAccountName"); + when(cmd.getDomainId()).thenReturn(1L); + when(cmd.getProjectId()).thenReturn(2L); + when(cmd.getKeyword()).thenReturn("testSearchUserdataKeyword"); + when(cmd.isRecursive()).thenReturn(false); + UserDataVO userData = Mockito.mock(UserDataVO.class); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + when(_userDataDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(userData); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + when(sb.create()).thenReturn(sc); + + List userDataList = new ArrayList(); + userDataList.add(userData); + Pair, Integer> result = new Pair(userDataList, 1); + when(_userDataDao.searchAndCount(nullable(SearchCriteria.class), nullable(Filter.class))).thenReturn(result); + + Pair, Integer> userdataResultList = spy.listUserDatas(cmd); + + Assert.assertEquals(userdataResultList.first().get(0), userDataList.get(0)); + } + } diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index b5f460ee3c4..81e248959cf 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -216,7 +216,7 @@ public class VolumeApiServiceImplTest { VolumeVO volumeOfRunningVm = new VolumeVO("root", 1L, 1L, 1L, 1L, 1L, "root", "root", Storage.ProvisioningType.THIN, 1, null, null, "root", Volume.Type.ROOT); when(volumeDaoMock.findById(1L)).thenReturn(volumeOfRunningVm); - UserVmVO runningVm = new UserVmVO(1L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null); + UserVmVO runningVm = new UserVmVO(1L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null); runningVm.setState(State.Running); runningVm.setDataCenterId(1L); when(userVmDaoMock.findById(1L)).thenReturn(runningVm); @@ -226,13 +226,13 @@ public class VolumeApiServiceImplTest { volumeOfStoppedVm.setPoolId(1L); when(volumeDaoMock.findById(2L)).thenReturn(volumeOfStoppedVm); - UserVmVO stoppedVm = new UserVmVO(2L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null); + UserVmVO stoppedVm = new UserVmVO(2L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null); stoppedVm.setState(State.Stopped); stoppedVm.setDataCenterId(1L); when(userVmDaoMock.findById(2L)).thenReturn(stoppedVm); // volume of hyperV vm id=3 - UserVmVO hyperVVm = new UserVmVO(3L, "vm", "vm", 1, HypervisorType.Hyperv, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null); + UserVmVO hyperVVm = new UserVmVO(3L, "vm", "vm", 1, HypervisorType.Hyperv, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null); hyperVVm.setState(State.Stopped); hyperVVm.setDataCenterId(1L); when(userVmDaoMock.findById(3L)).thenReturn(hyperVVm); @@ -289,7 +289,7 @@ public class VolumeApiServiceImplTest { when(volumeDaoMock.findById(7L)).thenReturn(managedVolume1); // vm having root volume - UserVmVO vmHavingRootVolume = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null); + UserVmVO vmHavingRootVolume = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null); vmHavingRootVolume.setState(State.Stopped); vmHavingRootVolume.setDataCenterId(1L); when(userVmDaoMock.findById(4L)).thenReturn(vmHavingRootVolume); @@ -313,7 +313,7 @@ public class VolumeApiServiceImplTest { upVolume.setState(Volume.State.Uploaded); when(volumeDaoMock.findById(8L)).thenReturn(upVolume); - UserVmVO kvmVm = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.KVM, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null); + UserVmVO kvmVm = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.KVM, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null); kvmVm.setState(State.Running); kvmVm.setDataCenterId(1L); when(userVmDaoMock.findById(4L)).thenReturn(kvmVm); diff --git a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java index e6171bb468a..849718e7fc5 100755 --- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java +++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java @@ -57,6 +57,7 @@ import com.cloud.user.AccountManager; import com.cloud.user.AccountVO; import com.cloud.user.ResourceLimitService; import com.cloud.user.User; +import com.cloud.user.UserData; import com.cloud.user.UserVO; import com.cloud.user.dao.AccountDao; import com.cloud.utils.component.ComponentContext; @@ -67,6 +68,7 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.api.command.user.template.CreateTemplateCmd; import org.apache.cloudstack.api.command.user.template.DeleteTemplateCmd; +import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; @@ -89,12 +91,14 @@ import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.test.utils.SpringUtils; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -130,6 +134,7 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.eq; @RunWith(SpringJUnit4ClassRunner.class) +@PrepareForTest(CallContext.class) @ContextConfiguration(loader = AnnotationConfigContextLoader.class) public class TemplateManagerImplTest { @@ -184,6 +189,9 @@ public class TemplateManagerImplTest { @Inject HypervisorGuruManager _hvGuruMgr; + @Inject + AccountManager _accountMgr; + public class CustomThreadPoolExecutor extends ThreadPoolExecutor { AtomicInteger ai = new AtomicInteger(0); public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, @@ -504,6 +512,80 @@ public class TemplateManagerImplTest { assertTrue("Template in a region store should have cross zones set", template.isCrossZones()); } + @Test + public void testLinkUserDataToTemplate() { + LinkUserDataToTemplateCmd cmd = Mockito.mock(LinkUserDataToTemplateCmd.class); + when(cmd.getTemplateId()).thenReturn(1L); + when(cmd.getIsoId()).thenReturn(null); + when(cmd.getUserdataId()).thenReturn(2L); + when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(_tmpltDao.findById(anyLong())).thenReturn(template); + + VirtualMachineTemplate resultTemplate = templateManager.linkUserDataToTemplate(cmd); + + Assert.assertEquals(template, resultTemplate); + } + + @Test(expected = InvalidParameterValueException.class) + public void testLinkUserDataToTemplateByProvidingBothISOAndTemplateId() { + LinkUserDataToTemplateCmd cmd = Mockito.mock(LinkUserDataToTemplateCmd.class); + when(cmd.getTemplateId()).thenReturn(1L); + when(cmd.getIsoId()).thenReturn(1L); + when(cmd.getUserdataId()).thenReturn(2L); + when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(_tmpltDao.findById(1L)).thenReturn(template); + + templateManager.linkUserDataToTemplate(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testLinkUserDataToTemplateByNotProvidingBothISOAndTemplateId() { + LinkUserDataToTemplateCmd cmd = Mockito.mock(LinkUserDataToTemplateCmd.class); + when(cmd.getTemplateId()).thenReturn(null); + when(cmd.getIsoId()).thenReturn(null); + when(cmd.getUserdataId()).thenReturn(2L); + when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(_tmpltDao.findById(1L)).thenReturn(template); + + templateManager.linkUserDataToTemplate(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testLinkUserDataToTemplateWhenNoTemplate() { + LinkUserDataToTemplateCmd cmd = Mockito.mock(LinkUserDataToTemplateCmd.class); + when(cmd.getTemplateId()).thenReturn(1L); + when(cmd.getIsoId()).thenReturn(null); + when(cmd.getUserdataId()).thenReturn(2L); + when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); + + when(_tmpltDao.findById(anyLong())).thenReturn(null); + + templateManager.linkUserDataToTemplate(cmd); + } + + @Test + public void testUnLinkUserDataToTemplate() { + LinkUserDataToTemplateCmd cmd = Mockito.mock(LinkUserDataToTemplateCmd.class); + when(cmd.getTemplateId()).thenReturn(1L); + when(cmd.getIsoId()).thenReturn(null); + when(cmd.getUserdataId()).thenReturn(null); + when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(template.getId()).thenReturn(1L); + when(_tmpltDao.findById(1L)).thenReturn(template); + + VirtualMachineTemplate resultTemplate = templateManager.linkUserDataToTemplate(cmd); + + Assert.assertEquals(template, resultTemplate); + } + @Configuration @ComponentScan(basePackageClasses = {TemplateManagerImpl.class}, includeFilters = {@ComponentScan.Filter(value = TestConfiguration.Library.class, type = FilterType.CUSTOM)}, diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index c9460c3400f..c0a1d38315a 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -24,6 +24,8 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,7 +35,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.UserData; +import com.cloud.user.UserDataVO; +import com.cloud.user.dao.UserDataDao; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.BaseCmd.HTTPMethod; +import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; import org.apache.cloudstack.context.CallContext; @@ -159,6 +167,9 @@ public class UserVmManagerImplTest { @Mock VolumeApiService volumeApiService; + @Mock + UserDataDao userDataDao; + private long vmId = 1l; private static final long GiB_TO_BYTES = 1024 * 1024 * 1024; @@ -257,6 +268,7 @@ public class UserVmManagerImplTest { Mockito.when(_serviceOfferingDao.findById(Mockito.anyLong(), Mockito.anyLong())).thenReturn((ServiceOfferingVO) offering); Mockito.when(userVmVoMock.isDisplay()).thenReturn(true); Mockito.doNothing().when(userVmManagerImpl).updateDisplayVmFlag(false, vmId, userVmVoMock); + Mockito.when(updateVmCommand.getUserdataId()).thenReturn(null); userVmManagerImpl.updateVirtualMachine(updateVmCommand); verifyMethodsThatAreAlwaysExecuted(); @@ -272,6 +284,7 @@ public class UserVmManagerImplTest { Mockito.when(updateVmCommand.isCleanupDetails()).thenReturn(true); Mockito.lenient().doNothing().when(userVmManagerImpl).updateDisplayVmFlag(false, vmId, userVmVoMock); Mockito.doNothing().when(userVmDetailVO).removeDetails(vmId); + Mockito.when(updateVmCommand.getUserdataId()).thenReturn(null); userVmManagerImpl.updateVirtualMachine(updateVmCommand); verifyMethodsThatAreAlwaysExecuted(); @@ -322,6 +335,7 @@ public class UserVmManagerImplTest { if(!isDetailsEmpty) { details.put("", ""); } + Mockito.when(updateVmCommand.getUserdataId()).thenReturn(null); Mockito.when(updateVmCommand.getDetails()).thenReturn(details); Mockito.when(updateVmCommand.isCleanupDetails()).thenReturn(cleanUpDetails); configureDoNothingForDetailsMethod(); @@ -348,7 +362,7 @@ public class UserVmManagerImplTest { Mockito.verify(userVmManagerImpl).updateVirtualMachine(nullable(Long.class), nullable(String.class), nullable(String.class), nullable(Boolean.class), nullable(Boolean.class), nullable(Long.class), - nullable(String.class), nullable(Boolean.class), nullable(HTTPMethod.class), nullable(String.class), nullable(String.class), nullable(String.class), nullable(List.class), + nullable(String.class), nullable(Long.class), nullable(String.class), nullable(Boolean.class), nullable(HTTPMethod.class), nullable(String.class), nullable(String.class), nullable(String.class), nullable(List.class), nullable(Map.class)); } @@ -359,7 +373,7 @@ public class UserVmManagerImplTest { Mockito.doReturn(new ArrayList()).when(userVmManagerImpl).getSecurityGroupIdList(updateVmCommand); Mockito.lenient().doReturn(Mockito.mock(UserVm.class)).when(userVmManagerImpl).updateVirtualMachine(Mockito.anyLong(), Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyLong(), - Mockito.anyString(), Mockito.anyBoolean(), Mockito.any(HTTPMethod.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyListOf(Long.class), + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.any(HTTPMethod.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyListOf(Long.class), Mockito.anyMap()); } @@ -579,4 +593,231 @@ public class UserVmManagerImplTest { Mockito.when(newRootDiskOffering.getName()).thenReturn("OfferingName"); return newRootDiskOffering; } + + @Test (expected = CloudRuntimeException.class) + public void testUserDataDenyOverride() { + Long userDataId = 1L; + + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + when(template.getUserDataId()).thenReturn(2L); + when(template.getUserDataOverridePolicy()).thenReturn(UserData.UserDataOverridePolicy.DENYOVERRIDE); + + userVmManagerImpl.finalizeUserData(null, userDataId, template); + } + + @Test + public void testUserDataAllowOverride() { + String templateUserData = "testTemplateUserdata"; + Long userDataId = 1L; + + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + when(template.getUserDataId()).thenReturn(2L); + when(template.getUserDataOverridePolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); + + UserDataVO apiUserDataVO = Mockito.mock(UserDataVO.class); + doReturn(apiUserDataVO).when(userDataDao).findById(userDataId); + when(apiUserDataVO.getUserData()).thenReturn(templateUserData); + + String finalUserdata = userVmManagerImpl.finalizeUserData(null, userDataId, template); + + Assert.assertEquals(finalUserdata, templateUserData); + } + + @Test + public void testUserDataAppend() { + String userData = "testUserdata"; + String templateUserData = "testTemplateUserdata"; + Long userDataId = 1L; + + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + when(template.getUserDataId()).thenReturn(2L); + when(template.getUserDataOverridePolicy()).thenReturn(UserData.UserDataOverridePolicy.APPEND); + + UserDataVO templateUserDataVO = Mockito.mock(UserDataVO.class); + doReturn(templateUserDataVO).when(userDataDao).findById(2L); + when(templateUserDataVO.getUserData()).thenReturn(templateUserData); + + UserDataVO apiUserDataVO = Mockito.mock(UserDataVO.class); + doReturn(apiUserDataVO).when(userDataDao).findById(userDataId); + when(apiUserDataVO.getUserData()).thenReturn(userData); + + String finalUserdata = userVmManagerImpl.finalizeUserData(null, userDataId, template); + + Assert.assertEquals(finalUserdata, templateUserData+userData); + } + + @Test + public void testUserDataWithoutTemplate() { + String userData = "testUserdata"; + Long userDataId = 1L; + + UserDataVO apiUserDataVO = Mockito.mock(UserDataVO.class); + doReturn(apiUserDataVO).when(userDataDao).findById(userDataId); + when(apiUserDataVO.getUserData()).thenReturn(userData); + + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + when(template.getUserDataId()).thenReturn(null); + + String finalUserdata = userVmManagerImpl.finalizeUserData(null, userDataId, template); + + Assert.assertEquals(finalUserdata, userData); + } + + @Test + public void testUserDataAllowOverrideWithoutAPIuserdata() { + String templateUserData = "testTemplateUserdata"; + + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + when(template.getUserDataId()).thenReturn(2L); + when(template.getUserDataOverridePolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); + UserDataVO templateUserDataVO = Mockito.mock(UserDataVO.class); + doReturn(templateUserDataVO).when(userDataDao).findById(2L); + when(templateUserDataVO.getUserData()).thenReturn(templateUserData); + + String finalUserdata = userVmManagerImpl.finalizeUserData(null, null, template); + + Assert.assertEquals(finalUserdata, templateUserData); + } + + @Test + public void testUserDataAllowOverrideWithUserdataText() { + String userData = "testUserdata"; + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + when(template.getUserDataId()).thenReturn(null); + + String finalUserdata = userVmManagerImpl.finalizeUserData(userData, null, template); + + Assert.assertEquals(finalUserdata, userData); + } + + @Test(expected = InvalidParameterValueException.class) + @PrepareForTest(CallContext.class) + public void testResetVMUserDataVMStateNotStopped() { + CallContext callContextMock = Mockito.mock(CallContext.class); + Mockito.lenient().doReturn(accountMock).when(callContextMock).getCallingAccount(); + + ResetVMUserDataCmd cmd = Mockito.mock(ResetVMUserDataCmd.class); + when(cmd.getId()).thenReturn(1L); + when(userVmDao.findById(1L)).thenReturn(userVmVoMock); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(userVmVoMock.getTemplateId()).thenReturn(2L); + when(templateDao.findByIdIncludingRemoved(2L)).thenReturn(template); + + + when(userVmVoMock.getState()).thenReturn(VirtualMachine.State.Running); + + try { + userVmManagerImpl.resetVMUserData(cmd); + } catch (ResourceUnavailableException e) { + throw new RuntimeException(e); + } catch (InsufficientCapacityException e) { + throw new RuntimeException(e); + } + } + + @Test(expected = InvalidParameterValueException.class) + @PrepareForTest(CallContext.class) + public void testResetVMUserDataDontAcceptBothUserdataAndUserdataId() { + CallContext callContextMock = Mockito.mock(CallContext.class); + Mockito.lenient().doReturn(accountMock).when(callContextMock).getCallingAccount(); + + ResetVMUserDataCmd cmd = Mockito.mock(ResetVMUserDataCmd.class); + when(cmd.getId()).thenReturn(1L); + when(userVmDao.findById(1L)).thenReturn(userVmVoMock); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(userVmVoMock.getTemplateId()).thenReturn(2L); + when(templateDao.findByIdIncludingRemoved(2L)).thenReturn(template); + + + when(userVmVoMock.getState()).thenReturn(VirtualMachine.State.Stopped); + + when(cmd.getUserData()).thenReturn("testUserdata"); + when(cmd.getUserdataId()).thenReturn(1L); + + try { + userVmManagerImpl.resetVMUserData(cmd); + } catch (ResourceUnavailableException e) { + throw new RuntimeException(e); + } catch (InsufficientCapacityException e) { + throw new RuntimeException(e); + } + } + + @Test + @PrepareForTest(CallContext.class) + public void testResetVMUserDataSuccessResetWithUserdata() { + CallContext callContextMock = Mockito.mock(CallContext.class); + Mockito.lenient().doReturn(accountMock).when(callContextMock).getCallingAccount(); + + UserVmVO userVmVO = new UserVmVO(); + userVmVO.setTemplateId(2L); + userVmVO.setState(VirtualMachine.State.Stopped); + userVmVO.setUserDataId(100L); + userVmVO.setUserData("RandomUserdata"); + + ResetVMUserDataCmd cmd = Mockito.mock(ResetVMUserDataCmd.class); + when(cmd.getId()).thenReturn(1L); + when(userVmDao.findById(1L)).thenReturn(userVmVO); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(templateDao.findByIdIncludingRemoved(2L)).thenReturn(template); + when(template.getUserDataId()).thenReturn(null); + + when(cmd.getUserData()).thenReturn("testUserdata"); + when(cmd.getUserdataId()).thenReturn(null); + when(cmd.getHttpMethod()).thenReturn(HTTPMethod.GET); + + try { + doNothing().when(userVmManagerImpl).updateUserData(userVmVO); + userVmManagerImpl.resetVMUserData(cmd); + } catch (ResourceUnavailableException e) { + throw new RuntimeException(e); + } catch (InsufficientCapacityException e) { + throw new RuntimeException(e); + } + + Assert.assertEquals("testUserdata", userVmVO.getUserData()); + Assert.assertEquals(null, userVmVO.getUserDataId()); + } + + @Test + @PrepareForTest(CallContext.class) + public void testResetVMUserDataSuccessResetWithUserdataId() { + CallContext callContextMock = Mockito.mock(CallContext.class); + Mockito.lenient().doReturn(accountMock).when(callContextMock).getCallingAccount(); + + UserVmVO userVmVO = new UserVmVO(); + userVmVO.setTemplateId(2L); + userVmVO.setState(VirtualMachine.State.Stopped); + userVmVO.setUserDataId(100L); + userVmVO.setUserData("RandomUserdata"); + + ResetVMUserDataCmd cmd = Mockito.mock(ResetVMUserDataCmd.class); + when(cmd.getId()).thenReturn(1L); + when(userVmDao.findById(1L)).thenReturn(userVmVO); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(templateDao.findByIdIncludingRemoved(2L)).thenReturn(template); + when(template.getUserDataId()).thenReturn(null); + + when(cmd.getUserdataId()).thenReturn(1L); + UserDataVO apiUserDataVO = Mockito.mock(UserDataVO.class); + when(userDataDao.findById(1L)).thenReturn(apiUserDataVO); + when(apiUserDataVO.getUserData()).thenReturn("testUserdata"); + when(cmd.getHttpMethod()).thenReturn(HTTPMethod.GET); + + try { + doNothing().when(userVmManagerImpl).updateUserData(userVmVO); + userVmManagerImpl.resetVMUserData(cmd); + } catch (ResourceUnavailableException e) { + throw new RuntimeException(e); + } catch (InsufficientCapacityException e) { + throw new RuntimeException(e); + } + + Assert.assertEquals("testUserdata", userVmVO.getUserData()); + Assert.assertEquals(1L, (long)userVmVO.getUserDataId()); + } } diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerTest.java index 37cd6e51299..c75f2c47e73 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerTest.java @@ -517,7 +517,7 @@ public class UserVmManagerTest { AccountVO newAccount = new AccountVO("testaccount", 1, "networkdomain", (short)1, UUID.randomUUID().toString()); newAccount.setId(2L); - UserVmVO vm = new UserVmVO(10L, "test", "test", 1L, HypervisorType.Any, 1L, false, false, 1L, 1L, 1, 5L, "test", "test", 1L); + UserVmVO vm = new UserVmVO(10L, "test", "test", 1L, HypervisorType.Any, 1L, false, false, 1L, 1L, 1, 5L, "test", null, null, "test", 1L); vm.setState(VirtualMachine.State.Stopped); when(_vmDao.findById(anyLong())).thenReturn(vm); diff --git a/server/src/test/java/com/cloud/vm/dao/UserVmDaoImplTest.java b/server/src/test/java/com/cloud/vm/dao/UserVmDaoImplTest.java index 49de99ad803..b9c9061e0f4 100644 --- a/server/src/test/java/com/cloud/vm/dao/UserVmDaoImplTest.java +++ b/server/src/test/java/com/cloud/vm/dao/UserVmDaoImplTest.java @@ -48,7 +48,7 @@ public class UserVmDaoImplTest extends TestCase { // Persist the data. UserVmVO vo = new UserVmVO(vmId, instanceName, displayName, templateId, hypervisor, guestOsId, haEnabled, limitCpuUse, domainId, accountId, 1, serviceOfferingId, userdata, - name, diskOfferingId); + null, null, name, diskOfferingId); dao.persist(vo); vo = dao.findById(vmId); diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java index a731f7c7c10..1dd8df2bb75 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java @@ -913,7 +913,7 @@ public class MockNetworkModelImpl extends ManagerBase implements NetworkModel { } @Override - public List generateVmData(String userData, String serviceOffering, long datacenterId, String vmName, String vmHostName, long vmId, String vmUuid, String guestIpAddress, String publicKey, String password, Boolean isWindows, String hostname) { + public List generateVmData(String userData, String userDataDetails, String serviceOffering, long datacenterId, String vmName, String vmHostName, long vmId, String vmUuid, String guestIpAddress, String publicKey, String password, Boolean isWindows, String hostname) { return null; } diff --git a/server/src/test/java/org/apache/cloudstack/affinity/AffinityApiUnitTest.java b/server/src/test/java/org/apache/cloudstack/affinity/AffinityApiUnitTest.java index 0d366483300..990e6f377b5 100644 --- a/server/src/test/java/org/apache/cloudstack/affinity/AffinityApiUnitTest.java +++ b/server/src/test/java/org/apache/cloudstack/affinity/AffinityApiUnitTest.java @@ -205,7 +205,7 @@ public class AffinityApiUnitTest { @Test(expected = InvalidParameterValueException.class) public void updateAffinityGroupVMRunning() throws ResourceInUseException { - UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", "test", 1L); + UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", null, null, "test", 1L); vm.setState(VirtualMachine.State.Running); when(_vmDao.findById(10L)).thenReturn(vm); diff --git a/server/src/test/java/org/apache/cloudstack/affinity/AffinityGroupServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/affinity/AffinityGroupServiceImplTest.java index 8aa4aa8277e..c5caef8a6f7 100644 --- a/server/src/test/java/org/apache/cloudstack/affinity/AffinityGroupServiceImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/affinity/AffinityGroupServiceImplTest.java @@ -270,7 +270,7 @@ public class AffinityGroupServiceImplTest { @Test(expected = InvalidParameterValueException.class) public void updateAffinityGroupVMRunning() throws ResourceInUseException { when(_acctMgr.finalizeOwner((Account)anyObject(), anyString(), anyLong(), anyLong())).thenReturn(acct); - UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, DOMAIN_ID, 200L, 1, 5L, "", "test", 1L); + UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, DOMAIN_ID, 200L, 1, 5L, "", null, null, "test", 1L); vm.setState(VirtualMachine.State.Running); when(_vmDao.findById(10L)).thenReturn(vm); diff --git a/test/integration/smoke/test_register_userdata.py b/test/integration/smoke/test_register_userdata.py new file mode 100644 index 00000000000..8cf44c7620b --- /dev/null +++ b/test/integration/smoke/test_register_userdata.py @@ -0,0 +1,766 @@ +# 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. +#All tests inherit from cloudstackTestCase +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (ServiceOffering, + VirtualMachine, + Account, + UserData, + NetworkOffering, + Network, + Router, + EgressFireWallRule, + PublicIPAddress, + NATRule, + Template) +from marvin.lib.common import get_test_template, get_zone, list_virtual_machines +from marvin.lib.utils import (validateList, cleanup_resources) +from nose.plugins.attrib import attr +from marvin.codes import PASS,FAIL + + +from marvin.lib.common import (get_domain, get_template) + +class Services: + def __init__(self): + self.services = { + "virtual_machine": { + "displayname": "TesVM1", + "username": "root", + "password": "password", + "ssh_port": 22, + "privateport": 22, + "publicport": 22, + "protocol": 'TCP', + }, + "ostype": 'CentOS 5.5 (64-bit)', + "service_offering": { + "name": "Tiny Instance", + "displaytext": "Tiny Instance", + "cpunumber": 1, + "cpuspeed": 100, + "memory": 256, + }, + "egress": { + "name": 'web', + "protocol": 'TCP', + "startport": 80, + "endport": 80, + "cidrlist": '0.0.0.0/0', + }, + "natrule": { + "privateport": 22, + "publicport": 22, + "protocol": "TCP" + }, + } + +class TestRegisteredUserdata(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + super(TestRegisteredUserdata, cls) + cls.api_client = cls.testClient.getApiClient() + cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) + cls.services = Services().services + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.testdata = self.testClient.getParsedTestDataConfig() + + # Get Zone, Domain and Default Built-in template + self.domain = get_domain(self.apiclient) + self.zone = get_zone(self.apiclient, self.testClient.getZoneForTests()) + + #create a user account + self.account = Account.create( + self.apiclient, + self.testdata["account"], + domainid=self.domain.id + ) + + self.testdata["mode"] = self.zone.networktype + self.template = get_template(self.apiclient, self.zone.id, self.testdata["ostype"]) + + #create a service offering + small_service_offering = self.testdata["service_offerings"]["small"] + self.service_offering = ServiceOffering.create( + self.apiclient, + small_service_offering + ) + + self.no_isolate = NetworkOffering.create( + self.apiclient, + self.testdata["isolated_network_offering"] + ) + self.no_isolate.update(self.apiclient, state='Enabled') + self.isolated_network = Network.create( + self.apiclient, + self.testdata["network"], + networkofferingid=self.no_isolate.id, + zoneid=self.zone.id, + accountid="admin", + domainid=1 + ) + + #build cleanup list + self.cleanup = [ + self.service_offering, + self.isolated_network, + self.no_isolate, + self.account, + ] + + + def tearDown(self): + try: + cleanup_resources(self.apiclient, self.cleanup) + except Exception as e: + self.debug("Warning! Exception in tearDown: %s" % e) + + + @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False) + def test_CRUD_operations_userdata(self): + """Test register, list, update operations on userdata + 1. Register a userdata + 2. List the registered userdata + 3. Delete the registered userdata + """ + + self.userdata1 = UserData.register( + self.apiclient, + name="UserdataName", + userdata="VGVzdFVzZXJEYXRh", #TestUserData + account=self.account.name, + domainid=self.account.domainid + ) + + list_userdata = UserData.list(self.apiclient, id=self.userdata1.userdata.id, listall=True) + + self.assertNotEqual( + len(list_userdata), + 0, + "List userdata was empty" + ) + + userdata = list_userdata[0] + self.assertEqual( + userdata.id, + self.userdata1.userdata.id, + "userdata ids do not match" + ) + + UserData.delete( + self.apiclient, + id=self.userdata1.userdata.id + ) + + @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=True) + def test_deploy_vm_with_registered_userdata(self): + """Test deploy VM with registered userdata + 1. Register a userdata + 2. Deploy a VM by passing the userdata id + 3. Test the VM response + 4. SSH into VM and access the userdata + """ + + self.userdata2 = UserData.register( + self.apiclient, + name="testUserData2", + userdata="VGVzdFVzZXJEYXRh", #TestUserData + account=self.account.name, + domainid=self.account.domainid + ) + + self.virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + accountid="admin", + domainid=1, + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + networkids=[self.isolated_network.id], + userdataid=self.userdata2.userdata.id + ) + self.cleanup.append(self.virtual_machine) + self.cleanup.append(self.userdata2) + + networkid = self.virtual_machine.nic[0].networkid + src_nat_list = PublicIPAddress.list( + self.apiclient, + associatednetworkid=networkid, + account="admin", + domainid=1, + listall=True, + issourcenat=True, + ) + src_nat = src_nat_list[0] + + NATRule.create( + self.apiclient, + self.virtual_machine, + self.services["natrule"], + src_nat.id + ) + + # create egress rule to allow wget of my cloud-set-guest-password script + if self.zone.networktype.lower() == 'advanced': + EgressFireWallRule.create(self.api_client, + networkid=networkid, + protocol=self.testdata["egress_80"]["protocol"], + startport=self.testdata["egress_80"]["startport"], + endport=self.testdata["egress_80"]["endport"], + cidrlist=self.testdata["egress_80"]["cidrlist"]) + + list_vms = VirtualMachine.list(self.apiclient, id=self.virtual_machine.id) + + self.assertEqual( + isinstance(list_vms, list), + True, + "List VM response was not a valid list" + ) + self.assertNotEqual( + len(list_vms), + 0, + "List VM response was empty" + ) + + vm = list_vms[0] + self.assertEqual( + vm.id, + self.virtual_machine.id, + "Virtual Machine ids do not match" + ) + self.assertEqual( + vm.state, + "Running", + msg="VM is not in Running state" + ) + self.assertEqual( + vm.userdataid, + self.userdata2.userdata.id, + "Virtual Machine names do not match" + ) + + # Verify the retrieved ip address in listNICs API response + self.list_nics(vm.id) + vr_res = Router.list( + self.apiclient, + networkid=self.isolated_network.id, + listAll=True + ) + self.assertEqual(validateList(vr_res)[0], PASS, "List Routers returned invalid response") + vr_ip = vr_res[0].guestipaddress + ssh = self.virtual_machine.get_ssh_client(ipaddress=src_nat.ipaddress) + cmd = "curl http://%s/latest/user-data" % vr_ip + res = ssh.execute(cmd) + self.debug("Verifying userdata in the VR") + self.assertEqual( + str(res[0]), + "TestUserData", + "Failed to match userdata" + ) + + def list_nics(self, vm_id): + list_vm_res = VirtualMachine.list(self.apiclient, id=vm_id) + self.assertEqual(validateList(list_vm_res)[0], PASS, "List vms returned invalid response") + nics = list_vm_res[0].nic + for nic in nics: + if nic.type == "Shared": + nic_res = NIC.list( + self.apiclient, + virtualmachineid=vm_id, + nicid=nic.id + ) + nic_ip = nic_res[0].ipaddress + self.assertIsNotNone(nic_ip, "listNics API response does not have the ip address") + else: + continue + return + + @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=True) + def test_deploy_vm_with_registered_userdata_with_params(self): + """Test deploy VM with registered userdata with variables + 1. Register a userdata having variables + 2. Deploy a VM by passing the userdata id and custom userdata params map with values + 3. Test the VM response + 4. SSH into VM and access the userdata, check if values got rendered for the decalared variables in userdata + """ + self.userdata2 = UserData.register( + self.apiclient, + name="testUserData2", + userdata="IyMgdGVtcGxhdGU6IGppbmphCiNjbG91ZC1jb25maWcKcnVuY21kOgogICAgLSBlY2hvICdrZXkge3sgZHMubWV0YV9kYXRhLmtleTEgfX0nID4+IC9yb290L2luc3RhbmNlX21ldGFkYXRh", + # ## template: jinja + # #cloud-config + # runcmd: + # - echo 'key {{ ds.meta_data.key1 }}' >> /root/instance_metadata + + account=self.account.name, + domainid=self.account.domainid + ) + + self.virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + accountid="admin", + domainid=1, + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + networkids=[self.isolated_network.id], + userdataid=self.userdata2.userdata.id, + userdatadetails=[{"key1": "value1"}] + ) + self.cleanup.append(self.virtual_machine) + self.cleanup.append(self.userdata2) + + networkid = self.virtual_machine.nic[0].networkid + src_nat_list = PublicIPAddress.list( + self.apiclient, + associatednetworkid=networkid, + account="admin", + domainid=1, + listall=True, + issourcenat=True, + ) + src_nat = src_nat_list[0] + + NATRule.create( + self.apiclient, + self.virtual_machine, + self.services["natrule"], + src_nat.id + ) + + # create egress rule to allow wget of my cloud-set-guest-password script + if self.zone.networktype.lower() == 'advanced': + EgressFireWallRule.create(self.api_client, + networkid=networkid, + protocol=self.testdata["egress_80"]["protocol"], + startport=self.testdata["egress_80"]["startport"], + endport=self.testdata["egress_80"]["endport"], + cidrlist=self.testdata["egress_80"]["cidrlist"]) + + list_vms = VirtualMachine.list(self.apiclient, id=self.virtual_machine.id) + + self.assertEqual( + isinstance(list_vms, list), + True, + "List VM response was not a valid list" + ) + self.assertNotEqual( + len(list_vms), + 0, + "List VM response was empty" + ) + + vm = list_vms[0] + self.assertEqual( + vm.id, + self.virtual_machine.id, + "Virtual Machine ids do not match" + ) + self.assertEqual( + vm.state, + "Running", + msg="VM is not in Running state" + ) + self.assertEqual( + vm.userdataid, + self.userdata2.userdata.id, + "Userdata ids do not match" + ) + + # Verify the retrieved ip address in listNICs API response + self.list_nics(vm.id) + vr_res = Router.list( + self.apiclient, + networkid=self.isolated_network.id, + listAll=True + ) + self.assertEqual(validateList(vr_res)[0], PASS, "List Routers returned invalid response") + vr_ip = vr_res[0].guestipaddress + ssh = self.virtual_machine.get_ssh_client(ipaddress=src_nat.ipaddress) + cmd = "curl http://%s/latest/meta-data/key1" % vr_ip + res = ssh.execute(cmd) + self.debug("Verifying userdata in the VR") + self.assertEqual( + str(res[0]), + "value1", + "Failed to match userdata" + ) + + @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False) + def test_link_and_unlink_userdata_to_template(self): + """Test link and unlink of userdata to a template + 1. Register a userdata + 2. Link the registered userdata to a template + 3. Verify the template response and check userdata details in it + 4. Unlink the registered userdata from template + 5. Verify the template response + """ + + self.userdata3 = UserData.register( + self.apiclient, + name="testUserData2", + userdata="VGVzdFVzZXJEYXRh", #TestUserData + account=self.account.name, + domainid=self.account.domainid + ) + + self.template = Template.linkUserDataToTemplate( + self.apiclient, + templateid=self.template.id, + userdataid=self.userdata3.userdata.id + ) + + self.assertEqual( + self.userdata3.userdata.id, + self.template.userdataid, + "Match userdata id in template response" + ) + + self.assertEqual( + self.template.userdatapolicy, + "ALLOWOVERRIDE", + "Match default userdata override policy in template response" + ) + + self.debug("Verifying unlinking of userdata from template " + self.template.id) + + self.template = Template.linkUserDataToTemplate( + self.apiclient, + templateid=self.template.id + ) + + self.assertEqual( + self.template.userdataid, + None, + "Check userdata id in template response is None" + ) + + @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=True) + def test_deploy_vm_with_registered_userdata_with_override_policy_allow(self): + """Test deploy VM with registered userdata with variables + 1. Register two userdata, one to link to template and another to pass while deploying VM + 2. Link a userdata to template, default override policy is allow override + 3. Deploy a VM with that template and also by passing another userdata id + 4. Since the override policy is allow override, userdata id passed during VM deployment will be consider. + Verify the same by SSH into VM. + """ + + self.apiUserdata = UserData.register( + self.apiclient, + name="ApiUserdata", + userdata="QVBJdXNlcmRhdGE=", #APIuserdata + account=self.account.name, + domainid=self.account.domainid + ) + + self.templateUserdata = UserData.register( + self.apiclient, + name="TemplateUserdata", + userdata="VGVtcGxhdGVVc2VyRGF0YQ==", #TemplateUserData + account=self.account.name, + domainid=self.account.domainid + ) + + self.template = Template.linkUserDataToTemplate( + self.apiclient, + templateid=self.template.id, + userdataid=self.templateUserdata.userdata.id, + userdatapolicy="allowoverride" + ) + + self.virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + accountid="admin", + domainid=1, + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + networkids=[self.isolated_network.id], + userdataid=self.apiUserdata.userdata.id + ) + self.cleanup.append(self.virtual_machine) + self.cleanup.append(self.apiUserdata) + + self.template = Template.linkUserDataToTemplate( + self.apiclient, + templateid=self.template.id + ) + + networkid = self.virtual_machine.nic[0].networkid + src_nat_list = PublicIPAddress.list( + self.apiclient, + associatednetworkid=networkid, + account="admin", + domainid=1, + listall=True, + issourcenat=True, + ) + src_nat = src_nat_list[0] + + NATRule.create( + self.apiclient, + self.virtual_machine, + self.services["natrule"], + src_nat.id + ) + + # create egress rule to allow wget of my cloud-set-guest-password script + if self.zone.networktype.lower() == 'advanced': + EgressFireWallRule.create(self.api_client, + networkid=networkid, + protocol=self.testdata["egress_80"]["protocol"], + startport=self.testdata["egress_80"]["startport"], + endport=self.testdata["egress_80"]["endport"], + cidrlist=self.testdata["egress_80"]["cidrlist"]) + + list_vms = VirtualMachine.list(self.apiclient, id=self.virtual_machine.id) + + self.assertEqual( + isinstance(list_vms, list), + True, + "List VM response was not a valid list" + ) + self.assertNotEqual( + len(list_vms), + 0, + "List VM response was empty" + ) + + vm = list_vms[0] + self.assertEqual( + vm.id, + self.virtual_machine.id, + "Virtual Machine ids do not match" + ) + self.assertEqual( + vm.state, + "Running", + msg="VM is not in Running state" + ) + self.assertEqual( + vm.userdataid, + self.apiUserdata.userdata.id, + "Virtual Machine names do not match" + ) + + # Verify the retrieved ip address in listNICs API response + self.list_nics(vm.id) + vr_res = Router.list( + self.apiclient, + networkid=self.isolated_network.id, + listAll=True + ) + self.assertEqual(validateList(vr_res)[0], PASS, "List Routers returned invalid response") + vr_ip = vr_res[0].guestipaddress + ssh = self.virtual_machine.get_ssh_client(ipaddress=src_nat.ipaddress) + cmd = "curl http://%s/latest/user-data" % vr_ip + res = ssh.execute(cmd) + self.debug("Verifying userdata in the VR") + self.assertEqual( + str(res[0]), + "APIuserdata", + "Failed to match userdata" + ) + + @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=True) + def test_deploy_vm_with_registered_userdata_with_override_policy_append(self): + """Test deploy VM with registered userdata with variables + 1. Register two userdata, one to link to template and another to pass while deploying VM + 2. Link a userdata to template with override policy is append + 3. Deploy a VM with that template and also by passing another userdata id + 4. Since the override policy is append, userdata passed during VM deployment will be appended to template's + userdata and configured to VM. Verify the same by SSH into VM. + """ + + self.apiUserdata = UserData.register( + self.apiclient, + name="ApiUserdata", + userdata="QVBJdXNlcmRhdGE=", #APIuserdata + account=self.account.name, + domainid=self.account.domainid + ) + + self.templateUserdata = UserData.register( + self.apiclient, + name="TemplateUserdata", + userdata="VGVtcGxhdGVVc2VyRGF0YQ==", #TemplateUserData + account=self.account.name, + domainid=self.account.domainid + ) + + self.template = Template.linkUserDataToTemplate( + self.apiclient, + templateid=self.template.id, + userdataid=self.templateUserdata.userdata.id, + userdatapolicy="append" + ) + + self.virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + accountid="admin", + domainid=1, + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + networkids=[self.isolated_network.id], + userdataid=self.apiUserdata.userdata.id + ) + self.cleanup.append(self.virtual_machine) + self.cleanup.append(self.apiUserdata) + self.cleanup.append(self.templateUserdata) + + self.template = Template.linkUserDataToTemplate( + self.apiclient, + templateid=self.template.id + ) + + networkid = self.virtual_machine.nic[0].networkid + src_nat_list = PublicIPAddress.list( + self.apiclient, + associatednetworkid=networkid, + account="admin", + domainid=1, + listall=True, + issourcenat=True, + ) + src_nat = src_nat_list[0] + + NATRule.create( + self.apiclient, + self.virtual_machine, + self.services["natrule"], + src_nat.id + ) + + # create egress rule to allow wget of my cloud-set-guest-password script + if self.zone.networktype.lower() == 'advanced': + EgressFireWallRule.create(self.api_client, + networkid=networkid, + protocol=self.testdata["egress_80"]["protocol"], + startport=self.testdata["egress_80"]["startport"], + endport=self.testdata["egress_80"]["endport"], + cidrlist=self.testdata["egress_80"]["cidrlist"]) + + list_vms = VirtualMachine.list(self.apiclient, id=self.virtual_machine.id) + + self.assertEqual( + isinstance(list_vms, list), + True, + "List VM response was not a valid list" + ) + self.assertNotEqual( + len(list_vms), + 0, + "List VM response was empty" + ) + + vm = list_vms[0] + self.assertEqual( + vm.id, + self.virtual_machine.id, + "Virtual Machine ids do not match" + ) + self.assertEqual( + vm.state, + "Running", + msg="VM is not in Running state" + ) + + # Verify the retrieved ip address in listNICs API response + self.list_nics(vm.id) + vr_res = Router.list( + self.apiclient, + networkid=self.isolated_network.id, + listAll=True + ) + self.assertEqual(validateList(vr_res)[0], PASS, "List Routers returned invalid response") + vr_ip = vr_res[0].guestipaddress + ssh = self.virtual_machine.get_ssh_client(ipaddress=src_nat.ipaddress) + cmd = "curl http://%s/latest/user-data" % vr_ip + res = ssh.execute(cmd) + self.debug("Verifying userdata in the VR") + self.assertEqual( + str(res[0]), + "TemplateUserDataAPIuserdata", + "Failed to match userdata" + ) + + @attr(tags=['advanced', 'simulator', 'basic', 'sg', 'testnow'], required_hardware=True) + def test_deploy_vm_with_registered_userdata_with_override_policy_deny(self): + """Test deploy VM with registered userdata with variables + 1. Register two userdata, one to link to template and another to pass while deploying VM + 2. Link a userdata to template with override policy is deny override + 3. Deploy a VM with that template and also by passing another userdata id + 4. Since the override policy is deny override, userdata passed during VM deployment will not be accepted. + So expect an exception. + """ + + self.apiUserdata = UserData.register( + self.apiclient, + name="ApiUserdata", + userdata="QVBJdXNlcmRhdGE=", #APIuserdata + account=self.account.name, + domainid=self.account.domainid + ) + + self.templateUserdata = UserData.register( + self.apiclient, + name="TemplateUserdata", + userdata="VGVtcGxhdGVVc2VyRGF0YQ==", #TemplateUserData + account=self.account.name, + domainid=self.account.domainid + ) + + self.template = Template.linkUserDataToTemplate( + self.apiclient, + templateid=self.template.id, + userdataid=self.templateUserdata.userdata.id, + userdatapolicy="denyoverride" + ) + + with self.assertRaises(Exception) as e: + self.virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + accountid="admin", + domainid=1, + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + networkids=[self.isolated_network.id], + userdataid=self.apiUserdata.userdata.id + ) + self.cleanup.append(self.virtual_machine) + self.debug("Deploy VM with userdata passed during deployment failed as expected because template userdata override policy is deny. Exception here is : %s" % + e.exception) + + self.cleanup.append(self.apiUserdata) + self.cleanup.append(self.templateUserdata) + + self.template = Template.linkUserDataToTemplate( + self.apiclient, + templateid=self.template.id + ) + + diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 9096a3b9d6b..73c83e0e165 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -522,7 +522,7 @@ class VirtualMachine: method='GET', hypervisor=None, customcpunumber=None, customcpuspeed=None, custommemory=None, rootdisksize=None, rootdiskcontroller=None, vpcid=None, macaddress=None, datadisktemplate_diskoffering_list={}, - properties=None, nicnetworklist=None, bootmode=None, boottype=None, dynamicscalingenabled=None): + properties=None, nicnetworklist=None, bootmode=None, boottype=None, dynamicscalingenabled=None, userdataid=None, userdatadetails=None): """Create the instance""" cmd = deployVirtualMachine.deployVirtualMachineCmd() @@ -612,6 +612,12 @@ class VirtualMachine: if "userdata" in services: cmd.userdata = base64.urlsafe_b64encode(services["userdata"].encode()).decode() + if userdataid is not None: + cmd.userdataid = userdataid + + if userdatadetails is not None: + cmd.userdatadetails = userdatadetails + if "dhcpoptionsnetworklist" in services: cmd.dhcpoptionsnetworklist = services["dhcpoptionsnetworklist"] @@ -1622,6 +1628,18 @@ class Template: cmd.listall = True return (apiclient.listTemplates(cmd)) + @classmethod + def linkUserDataToTemplate(cls, apiclient, templateid, userdataid=None, userdatapolicy=None): + "Link userdata to template " + + cmd = linkUserDataToTemplate.linkUserDataToTemplateCmd() + cmd.templateid = templateid + if userdataid is not None: + cmd.userdataid = userdataid + if userdatapolicy is not None: + cmd.userdatapolicy = userdatapolicy + + return apiclient.linkUserDataToTemplate(cmd) class Iso: """Manage ISO life cycle""" @@ -4907,6 +4925,45 @@ class SSHKeyPair: cmd.listall = True return (apiclient.listSSHKeyPairs(cmd)) +class UserData: + """Manage Userdata""" + + def __init__(self, items, services): + self.__dict__.update(items) + + @classmethod + def register(cls, apiclient, name=None, account=None, + domainid=None, projectid=None, userdata=None, params=None): + """Registers Userdata""" + cmd = registerUserData.registerUserDataCmd() + cmd.name = name + cmd.userdata = userdata + if params is not None: + cmd.params = params + if account is not None: + cmd.account = account + if domainid is not None: + cmd.domainid = domainid + if projectid is not None: + cmd.projectid = projectid + + return (apiclient.registerUserData(cmd)) + + @classmethod + def delete(cls, apiclient, id): + """Delete Userdata""" + cmd = deleteUserData.deleteUserDataCmd() + cmd.id = id + apiclient.deleteUserData(cmd) + + @classmethod + def list(cls, apiclient, **kwargs): + """List all UserData""" + cmd = listUserData.listUserDataCmd() + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + if 'account' in list(kwargs.keys()) and 'domainid' in list(kwargs.keys()): + cmd.listall = True + return (apiclient.listUserData(cmd)) class Capacities: """Manage Capacities""" diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index db6f1e1274e..83ba35a2030 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -286,6 +286,7 @@ "label.action.update.os.preference.processing": "Updating OS Preference....", "label.action.update.resource.count": "Update Resource Count", "label.action.update.resource.count.processing": "Updating Resource Count....", +"label.action.userdata.reset": "Reset userdata", "label.action.vmsnapshot.create": "Take VM Snapshot", "label.action.vmsnapshot.delete": "Delete VM snapshot", "label.action.vmsnapshot.revert": "Revert to VM snapshot", @@ -1814,6 +1815,7 @@ "label.region": "Region", "label.region.details": "Region details", "label.register.template": "Register Template", +"label.register.user.data": "Register a userdata", "label.reinstall.vm": "Reinstall VM", "label.reject": "Reject", "label.related": "Related", @@ -1848,6 +1850,7 @@ "label.remove.static.route": "Remove static route", "label.remove.this.physical.network": "Remove this physical network", "label.remove.tier": "Remove tier", +"label.remove.user.data": "Remove userdata", "label.remove.vm.from.lb": "Remove VM from load balancer rule", "label.remove.vm.load.balancer": "Remove VM from load balancer", "label.remove.vmware.datacenter": "Remove VMware Datacenter", @@ -1874,6 +1877,7 @@ "label.reset.ssh.key.pair": "Reset SSH Key Pair", "label.reset.ssh.key.pair.on.vm": "Reset SSH Key Pair on VM", "label.reset.to.default": "Reset to default", +"label.reset.userdata.on.vm": "Reset Userdata on VM", "label.reset.vpn.connection": "Reset VPN connection", "label.resetvm": "Reset VM", "label.resource": "Resource", @@ -2075,6 +2079,17 @@ "label.srx.firewall": "Juniper SRX Firewall", "label.ssh.key.pair.details": "SSH Key Pair Details", "label.ssh.key.pairs": "SSH Key Pairs", +"label.userdataid": "Userdata ID", +"label.userdataname": "Userdata name", +"label.userdatadetails": "Userdata details", +"label.userdataparams": "Userdata parameters", +"label.userdatapolicy": "Userdata link policy", +"label.userdata.text": "Userdata Text", +"label.userdata.registered": "Userdata Registered", +"label.userdata.do.override": "Userdata override", +"label.userdata.do.append": "Userdata append", +"label.userdatapolicy.tooltip": "Userdata linked to the template can be overridden by userdata provided during VM deploy. Select the override policy as required.", +"label.user.data": "User Data", "label.ssh.port": "SSH Port", "label.sshkeypair": "New SSH Key Pair", "label.sshkeypairs": "SSH keypairs", @@ -2839,6 +2854,8 @@ "message.desc.primary.storage": "Each cluster must contain one or more primary storage servers, and we will add the first one now. Primary storage contains the disk volumes for all the VMs running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.", "message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this VM. Please note the root password will be changed by this operation if password is enabled.", "message.desc.secondary.storage": "Each zone must have at least one NFS or secondary storage server, and we will add the first one now. Secondary storage stores VM templates, ISO images, and VM disk volume snapshots. This server must be available to all hosts in the zone.

    Provide the IP address and exported path.", +"message.desc.register.user.data": "Please fill in the following data to register a user data.", +"message.desc.registered.user.data": "Registered a User Data.", "message.desc.zone": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", "message.detach.disk": "Are you sure you want to detach this disk?", "message.detach.iso.confirm": "Please confirm that you want to detach the ISO from this virtual instance.", @@ -2990,6 +3007,7 @@ "message.error.upload.template": "Template Upload Failed", "message.error.upload.template.description": "Only one template can be uploaded at a time", "message.error.url": "Please enter URL", +"message.error.userdata": "Please enter userdata", "message.error.username": "Enter your username", "message.error.valid.iops.range": "Please enter a valid IOPS range", "message.error.vcenter.datacenter": "Please enter vCenter Datacenter", @@ -3161,6 +3179,7 @@ "message.pending.projects.2": "To view, please go to the projects section, then select invitations from the drop-down.", "message.please.add.at.lease.one.traffic.range": "Please add at least one traffic range.", "message.please.confirm.remove.ssh.key.pair": "Please confirm that you want to remove this SSH Key Pair", +"message.please.confirm.remove.user.data": "Please confirm that you want to remove this userdata", "message.please.enter.valid.value": "Please enter a valid value", "message.please.enter.value": "Please enter values", "message.please.proceed": "Please proceed to the next step.", @@ -3324,6 +3343,7 @@ "message.success.register.iso": "Successfully registered ISO", "message.success.register.keypair": "Successfully registered SSH key pair", "message.success.register.template": "Successfully registered template", +"message.success.register.user.data": "Successfully registered Userdata", "message.success.release.ip": "Successfully released IP", "message.success.remove.egress.rule": "Successfully removed Egress rule", "message.success.remove.firewall.rule": "Successfully removed Firewall rule", @@ -3343,6 +3363,7 @@ "message.success.update.kubeversion": "Successfully updated Kubernetes supported version", "message.success.update.network": "Successfully updated Network", "message.success.update.template": "Successfully updated Template", +"message.success.update.iso": "Successfully updated ISO", "message.success.update.user": "Successfully updated user", "message.success.upgrade.kubernetes": "Successfully upgraded Kubernetes cluster", "message.success.upload": "Upload Successfully", diff --git a/ui/public/locales/pl.json b/ui/public/locales/pl.json index 595b0073b81..fbc94c523e2 100644 --- a/ui/public/locales/pl.json +++ b/ui/public/locales/pl.json @@ -2115,6 +2115,7 @@ "message.desc.primary.storage": "Each cluster must contain one or more primary storage servers, and we will add the first one now. Primary storage contains the disk volumes for all the VMs running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.", "message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this VM. Please note the root password will be changed by this operation if password is enabled.", "message.desc.secondary.storage": "Each zone must have at least one NFS or secondary storage server, and we will add the first one now. Secondary storage stores VM templates, ISO images, and VM disk volume snapshots. This server must be available to all hosts in the zone.

    Provide the IP address and exported path.", +"message.desc.reset.userdata": "Please select a userdata that you would like to add to this VM.", "message.desc.zone": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", "message.detach.disk": "Are you sure you want to detach this disk?", "message.detach.iso.confirm": "Please confirm that you want to detach the ISO from this virtual instance.", diff --git a/ui/src/components/view/AnnotationsTab.vue b/ui/src/components/view/AnnotationsTab.vue index 5223c407aff..e0cdb10a6e8 100644 --- a/ui/src/components/view/AnnotationsTab.vue +++ b/ui/src/components/view/AnnotationsTab.vue @@ -168,6 +168,7 @@ export default { case 'VMSnapshot': return 'VM_SNAPSHOT' case 'VMInstanceGroup': return 'INSTANCE_GROUP' case 'SSHKeyPair': return 'SSH_KEYPAIR' + case 'UserData': return 'USER_DATA' case 'KubernetesCluster': return 'KUBERNETES_CLUSTER' case 'Network': return 'NETWORK' case 'Vpc': return 'VPC' diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 14508baaa5c..10b759f0bb3 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -45,6 +45,9 @@
    {{ $toLocaleDate(resource[item]) }}
    +
    +
    {{ decodeUserData(resource.userdata) }}
    +
    {{ resource[item] }}
    @@ -119,6 +122,10 @@ export default { } }, methods: { + decodeUserData (userdata) { + const decodedData = Buffer.from(userdata, 'base64') + return decodedData.toString('utf-8') + }, fetchProjectAdmins () { if (!this.resource.owner) { return false diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index e94fd1e1d35..588582d5adb 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -541,6 +541,14 @@ {{ resource.zone || resource.zonename || resource.zoneid }} +
    +
    {{ $t('label.userdata') }}
    +
    + + {{ resource.userdataname || resource.userdataid }} + {{ resource.userdataname || resource.userdataid }} +
    +
    {{ $t('label.owners') }}
    diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index f060b16ea7b..9f567e66345 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -468,7 +468,7 @@ export default { }, methods: { quickViewEnabled () { - return new RegExp(['/vm', '/kubernetes', '/ssh', '/vmgroup', '/affinitygroup', + return new RegExp(['/vm', '/kubernetes', '/ssh', '/userdata', '/vmgroup', '/affinitygroup', '/volume', '/snapshot', '/vmsnapshot', '/backup', '/guestnetwork', '/vpc', '/vpncustomergateway', '/template', '/iso', @@ -478,7 +478,7 @@ export default { .test(this.$route.path) }, enableGroupAction () { - return ['vm', 'alert', 'vmgroup', 'ssh', 'affinitygroup', 'volume', 'snapshot', + return ['vm', 'alert', 'vmgroup', 'ssh', 'userdata', 'affinitygroup', 'volume', 'snapshot', 'vmsnapshot', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', 'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering', 'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment' diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 835923d9f07..867c2294bc1 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -66,7 +66,7 @@ export default { return fields }, searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'], - details: ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename'], + details: ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy'], tabs: [{ component: () => import('@/views/compute/InstanceTab.vue') }], @@ -369,6 +369,17 @@ export default { } } }, + { + api: 'resetUserDataForVirtualMachine', + icon: 'solution', + label: 'label.reset.userdata.on.vm', + message: 'message.desc.reset.userdata', + docHelp: 'adminguide/virtual_machines.html#resetting-userdata', + dataView: true, + show: (record) => { return ['Stopped'].includes(record.state) }, + popup: true, + component: () => import('@/views/compute/ResetUserData') + }, { api: 'assignVirtualMachine', icon: 'user-add', @@ -637,6 +648,77 @@ export default { } ] }, + { + name: 'userdata', + title: 'label.user.data', + icon: 'solution', + docHelp: 'adminguide/virtual_machines.html#user-data-and-meta-data', + permission: ['listUserData'], + columns: () => { + var fields = ['name', 'id'] + if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) { + fields.push('account') + } + return fields + }, + resourceType: 'UserData', + details: ['id', 'name', 'userdata', 'account', 'domain', 'params'], + related: [{ + name: 'vm', + title: 'label.instances', + param: 'userdata' + }], + tabs: [ + { + name: 'details', + component: () => import('@/components/view/DetailsTab.vue') + }, + { + name: 'comments', + component: () => import('@/components/view/AnnotationsTab.vue') + } + ], + actions: [ + { + api: 'registerUserData', + icon: 'plus', + label: 'label.register.user.data', + docHelp: 'adminguide/virtual_machines.html#creating-the-ssh-keypair', + listView: true, + popup: true, + component: () => import('@/views/compute/RegisterUserData.vue') + }, + { + api: 'deleteUserData', + icon: 'delete', + label: 'label.remove.user.data', + message: 'message.please.confirm.remove.user.data', + dataView: true, + args: ['id', 'account', 'domainid'], + mapping: { + id: { + value: (record, params) => { return record.id } + }, + account: { + value: (record, params) => { return record.account } + }, + domainid: { + value: (record, params) => { return record.domainid } + } + }, + groupAction: true, + popup: true, + groupMap: (selection, values, record) => { + return selection.map(x => { + const data = record.filter(y => { return y.id === x }) + return { + id: x, account: data[0].account, domainid: data[0].domainid + } + }) + } + } + ] + }, { name: 'affinitygroup', title: 'label.affinity.groups', diff --git a/ui/src/config/section/image.js b/ui/src/config/section/image.js index a93dc20032e..abdb9a8e8fb 100644 --- a/ui/src/config/section/image.js +++ b/ui/src/config/section/image.js @@ -46,7 +46,7 @@ export default { details: () => { var fields = ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'isready', 'passwordenabled', 'directdownload', 'deployasis', 'ispublic', 'isfeatured', 'isextractable', 'isdynamicallyscalable', 'crosszones', 'type', - 'account', 'domain', 'created'] + 'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy'] if (['Admin'].includes(store.getters.userInfo.roletype)) { fields.push('templatetype', 'url') } @@ -194,7 +194,7 @@ export default { } return fields }, - details: ['name', 'id', 'displaytext', 'checksum', 'ostypename', 'size', 'bootable', 'isready', 'directdownload', 'isextractable', 'ispublic', 'isfeatured', 'crosszones', 'account', 'domain', 'created'], + details: ['name', 'id', 'displaytext', 'checksum', 'ostypename', 'size', 'bootable', 'isready', 'directdownload', 'isextractable', 'ispublic', 'isfeatured', 'crosszones', 'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy'], searchFilters: ['name', 'zoneid', 'tags'], related: [{ name: 'vm', diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 3c330e1278d..d5b80ad67b5 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -543,9 +543,116 @@ - - + +
    + Userdata "{{ $t(this.template.userdataname) }}" is linked with template "{{ $t(this.template.name) }}" with override policy "{{ $t(this.template.userdatapolicy) }}" +

    +
    +
    + Enter the values for the variables in userdata +
    + + + + + +
    +
    +
    + Userdata "{{ $t(this.iso.userdataname) }}" is linked with ISO "{{ $t(this.iso.name) }}" with override policy "{{ $t(this.iso.userdatapolicy) }}" +

    +
    +
    + Enter the values for the variables in userdata +
    + + + + + +
    +


    +
    + + {{ $t('label.userdata.do.override') }} + + + + {{ $t('label.userdata.do.append') }} + + + + + +
    +
    0 && this.zone.securitygroupsenabled) || (this.zone && this.zone.networktype === 'Basic') }, isUserAllowedToListSshKeys () { return Boolean('listSSHKeyPairs' in this.$store.getters.apis) }, + isUserAllowedToListUserDatas () { + return Boolean('listUserData' in this.$store.getters.apis) + }, dynamicScalingVmConfigValue () { return this.options.dynamicScalingVmConfig?.[0]?.value === 'true' }, @@ -1203,6 +1373,8 @@ export default { template (oldValue, newValue) { if (oldValue && newValue && oldValue.id !== newValue.id) { this.dynamicscalingenabled = this.isDynamicallyScalable() + this.doUserdataOverride = false + this.doUserdataAppend = false } }, created () { @@ -1442,6 +1614,8 @@ export default { if (template) { var size = template.size / (1024 * 1024 * 1024) || 0 // bytes to GB this.dataPreFill.minrootdisksize = Math.ceil(size) + this.updateTemplateLinkedUserData(this.template.userdataid) + this.userdataDefaultOverridePolicy = this.template.userdatapolicy } } else if (name === 'isoid') { this.templateConfigurations = [] @@ -1455,6 +1629,8 @@ export default { isoid: value, templateid: null }) + this.updateTemplateLinkedUserData(this.iso.userdataid) + this.userdataDefaultOverridePolicy = this.iso.userdatapolicy } else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) { this.vm[name] = value this.form.setFieldsValue({ @@ -1520,6 +1696,56 @@ export default { keypair: name }) }, + updateUserData (id) { + if (id === '0') { + this.userdataid = undefined + return + } + this.userdataid = id + this.userDataParams = [] + api('listUserData', { id: id }).then(json => { + const resp = json?.listuserdataresponse?.userdata || [] + if (resp) { + var params = resp[0].params + if (params) { + var dataParams = params.split(',') + } + var that = this + dataParams.forEach(function (val, index) { + that.userDataParams.push({ + id: index, + key: val + }) + }) + } + }) + }, + updateTemplateLinkedUserData (id) { + if (id === '0') { + return + } + this.templateUserDataParams = [] + + api('listUserData', { id: id }).then(json => { + const resp = json?.listuserdataresponse?.userdata || [] + if (resp) { + var params = resp[0].params + if (params) { + var dataParams = params.split(',') + } + var that = this + that.templateUserDataParams = [] + if (dataParams) { + dataParams.forEach(function (val, index) { + that.templateUserDataParams.push({ + id: index, + key: val + }) + }) + } + } + }) + }, escapePropertyKey (key) { return key.split('.').join('\\002E') }, @@ -1721,6 +1947,8 @@ export default { } // step 7: select ssh key pair deployVmData.keypair = values.keypair + deployVmData.userdataid = this.userdataid + if (values.name) { deployVmData.name = values.name deployVmData.displayname = values.name @@ -1748,6 +1976,20 @@ export default { deployVmData = Object.fromEntries( Object.entries(deployVmData).filter(([key, value]) => value !== undefined)) + var idx = 0 + if (this.templateUserDataValues) { + for (const [key, value] of Object.entries(this.templateUserDataValues)) { + deployVmData['userdatadetails[' + idx + '].' + `${key}`] = value + idx++ + } + } + if (this.userDataValues) { + for (const [key, value] of Object.entries(this.userDataValues)) { + deployVmData['userdatadetails[' + idx + '].' + `${key}`] = value + idx++ + } + } + api('deployVirtualMachine', {}, 'POST', deployVmData).then(response => { const jobId = response.deployvirtualmachineresponse.jobid if (jobId) { @@ -2027,6 +2269,10 @@ export default { this.fetchAllIsos() } }, + onUserdataTabChange (key, type) { + this[type] = key + this.userDataParams = [] + }, sanitizeReverse (value) { const reversedValue = value .replace(/&/g, '&') diff --git a/ui/src/views/compute/RegisterUserData.vue b/ui/src/views/compute/RegisterUserData.vue new file mode 100644 index 00000000000..0084d22407c --- /dev/null +++ b/ui/src/views/compute/RegisterUserData.vue @@ -0,0 +1,236 @@ +// 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. + + + + + + diff --git a/ui/src/views/compute/ResetUserData.vue b/ui/src/views/compute/ResetUserData.vue new file mode 100644 index 00000000000..9e2f9cb4076 --- /dev/null +++ b/ui/src/views/compute/ResetUserData.vue @@ -0,0 +1,389 @@ +// 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. + + + + + + diff --git a/ui/src/views/compute/wizard/UserDataSelection.vue b/ui/src/views/compute/wizard/UserDataSelection.vue new file mode 100644 index 00000000000..4f7025a98f7 --- /dev/null +++ b/ui/src/views/compute/wizard/UserDataSelection.vue @@ -0,0 +1,202 @@ +// 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. + + + + + + diff --git a/ui/src/views/image/RegisterOrUploadIso.vue b/ui/src/views/image/RegisterOrUploadIso.vue index eaff52fb473..fd3b65faf46 100644 --- a/ui/src/views/image/RegisterOrUploadIso.vue +++ b/ui/src/views/image/RegisterOrUploadIso.vue @@ -24,7 +24,7 @@ {{ $t('message.upload.file.processing') }} - + + + + + + + {{ opt.name || opt.description }} + + + + + + + + + + {{ opt.id || opt.description }} + + + + + + + + + + + {{ opt.name || opt.description }} + + + + + + + + + + {{ opt.id || opt.description }} + + + + + @@ -396,6 +433,7 @@ import { api } from '@/api' import store from '@/store' import { axios } from '../../utils/request' import ResourceIcon from '@/components/view/ResourceIcon' +import TooltipLabel from '@/components/widgets/TooltipLabel' export default { name: 'RegisterOrUploadTemplate', @@ -410,7 +448,8 @@ export default { } }, components: { - ResourceIcon + ResourceIcon, + TooltipLabel }, data () { return { @@ -427,6 +466,10 @@ export default { format: {}, osTypes: {}, defaultOsType: '', + userdata: {}, + userdataid: null, + userdatapolicy: null, + userdatapolicylist: {}, defaultOsId: null, xenServerProvider: false, hyperKVMShow: false, @@ -447,6 +490,7 @@ export default { beforeCreate () { this.form = this.$form.createForm(this) this.apiParams = this.$getApiParams('registerTemplate') + this.linkUserDataParams = this.$getApiParams('linkUserDataToTemplate') }, created () { this.$set(this.zones, 'loading', false) @@ -474,6 +518,8 @@ export default { fetchData () { this.fetchZone() this.fetchOsTypes() + this.fetchUserData() + this.fetchUserdataPolicy() if (Object.prototype.hasOwnProperty.call(store.getters.apis, 'listConfigurations')) { this.fetchXenServerProvider() } @@ -590,6 +636,20 @@ export default { this.osTypes.loading = false }) }, + fetchUserData () { + const params = {} + params.listAll = true + + this.userdata.opts = [] + this.userdata.loading = true + + api('listUserData', params).then(json => { + const listUserdata = json.listuserdataresponse.userdata + this.userdata.opts = listUserdata + }).finally(() => { + this.userdata.loading = false + }) + }, fetchXenServerProvider () { const params = {} params.name = 'xenserver.pvdriver.version' @@ -782,6 +842,23 @@ export default { } this.$set(this.format, 'opts', format) }, + fetchUserdataPolicy () { + const userdataPolicy = [] + userdataPolicy.push({ + id: 'allowoverride', + description: 'allowoverride' + }) + userdataPolicy.push({ + id: 'append', + description: 'append' + }) + userdataPolicy.push({ + id: 'denyoverride', + description: 'denyoverride' + }) + this.userdatapolicylist.opts = userdataPolicy + }, + handlerSelectZone (value) { if (!Array.isArray(value)) { value = [value] @@ -888,6 +965,9 @@ export default { if (this.currentForm === 'Create') { this.loading = true api('registerTemplate', params).then(json => { + if (values.userdataid !== null) { + this.linkUserdataToTemplate(values.userdataid, json.registertemplateresponse.template[0].id, values.userdatapolicy) + } this.$notification.success({ message: this.$t('label.register.template'), description: `${this.$t('message.success.register.template')} ${params.name}` @@ -911,6 +991,9 @@ export default { api('getUploadParamsForTemplate', params).then(json => { this.uploadParams = (json.postuploadtemplateresponse && json.postuploadtemplateresponse.getuploadparams) ? json.postuploadtemplateresponse.getuploadparams : '' this.handleUpload() + if (values.userdataid !== null) { + this.linkUserdataToTemplate(values.userdataid, json.postuploadtemplateresponse.template[0].id, values.userdatapolicy) + } }).catch(error => { this.$notifyError(error) }).finally(() => { @@ -936,6 +1019,22 @@ export default { closeAction () { this.$emit('close-action') }, + linkUserdataToTemplate (userdataid, templateid, userdatapolicy) { + this.loading = true + const params = {} + params.userdataid = userdataid + params.templateid = templateid + if (userdatapolicy) { + params.userdatapolicy = userdatapolicy + } + api('linkUserDataToTemplate', params).then(json => { + this.closeAction() + }).catch(error => { + this.$notifyError(error) + }).finally(() => { + this.loading = false + }) + }, resetSelect () { this.form.setFieldsValue({ hypervisor: undefined, diff --git a/ui/src/views/image/UpdateISO.vue b/ui/src/views/image/UpdateISO.vue new file mode 100644 index 00000000000..b566c6826cc --- /dev/null +++ b/ui/src/views/image/UpdateISO.vue @@ -0,0 +1,307 @@ +// 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. + + + + + + diff --git a/ui/src/views/image/UpdateTemplate.vue b/ui/src/views/image/UpdateTemplate.vue index 0d9239cd321..587c71e9807 100644 --- a/ui/src/views/image/UpdateTemplate.vue +++ b/ui/src/views/image/UpdateTemplate.vue @@ -121,6 +121,41 @@ + + + + + + {{ opt.name || opt.description }} + + + + + + + + + + {{ opt.id || opt.description }} + + + + + @@ -180,13 +215,18 @@ export default { keyboardType: {}, osTypes: {}, loading: false, - selectedTemplateType: '' + selectedTemplateType: '', + userdata: {}, + userdataid: null, + userdatapolicy: null, + userdatapolicylist: {} } }, beforeCreate () { this.form = this.$form.createForm(this) this.apiParams = this.$getApiParams('updateTemplate') this.isAdmin = ['Admin'].includes(this.$store.getters.userInfo.roletype) + this.linkUserDataParams = this.$getApiParams('linkUserDataToTemplate') }, created () { this.$set(this.rootDisk, 'loading', false) @@ -197,7 +237,7 @@ export default { this.$set(this.keyboardType, 'opts', []) this.$set(this.osTypes, 'loading', false) this.$set(this.osTypes, 'opts', []) - const resourceFields = ['name', 'displaytext', 'passwordenabled', 'ostypeid', 'isdynamicallyscalable'] + const resourceFields = ['name', 'displaytext', 'passwordenabled', 'ostypeid', 'isdynamicallyscalable', 'userdataid', 'userdatapolicy'] if (this.isAdmin) { resourceFields.push('templatetype') } @@ -205,6 +245,17 @@ export default { var fieldValue = this.resource[field] if (fieldValue) { this.form.getFieldDecorator(field, { initialValue: fieldValue }) + switch (field) { + case 'userdataid': + this.userdataid = fieldValue + break + case 'userdatapolicy': + this.userdatapolicy = fieldValue + break + default: + this.form[field] = fieldValue + break + } } } const resourceDetailsFields = [] @@ -227,6 +278,8 @@ export default { this.fetchRootDiskControllerTypes(this.resource.hypervisor) this.fetchNicAdapterTypes() this.fetchKeyboardTypes() + this.fetchUserdata() + this.fetchUserdataPolicy() }, isValidValueForKey (obj, key) { return key in obj && obj[key] != null && obj[key] !== undefined && obj[key] !== '' @@ -347,6 +400,53 @@ export default { this.$set(this.keyboardType, 'opts', keyboardType) }, + fetchUserdataPolicy () { + const userdataPolicy = [] + userdataPolicy.push({ + id: '', + description: '' + }) + userdataPolicy.push({ + id: 'allowoverride', + description: 'allowoverride' + }) + userdataPolicy.push({ + id: 'append', + description: 'append' + }) + userdataPolicy.push({ + id: 'denyoverride', + description: 'denyoverride' + }) + this.userdatapolicylist.opts = userdataPolicy + }, + fetchUserdata () { + const params = {} + params.listAll = true + + this.userdata.opts = [] + this.userdata.loading = true + + api('listUserData', params).then(json => { + const userdataIdAndName = [] + const userdataOpts = json.listuserdataresponse.userdata + userdataIdAndName.push({ + id: '', + name: '' + }) + + Object.values(userdataOpts).forEach(userdata => { + userdataIdAndName.push({ + id: userdata.id, + name: userdata.name + }) + }) + + this.userdata.opts = userdataIdAndName + }).finally(() => { + this.userdata.loading = false + }) + }, handleSubmit (e) { e.preventDefault() if (this.loading) return @@ -368,6 +468,9 @@ export default { params[key] = values[key] } api('updateTemplate', params).then(json => { + if (values.userdataid !== null) { + this.linkUserdataToTemplate(values.userdataid, json.updatetemplateresponse.template.id, values.userdatapolicy) + } this.$message.success(`${this.$t('message.success.update.template')}: ${this.resource.name}`) this.$emit('refresh-data') this.closeAction() @@ -380,6 +483,24 @@ export default { }, closeAction () { this.$emit('close-action') + }, + linkUserdataToTemplate (userdataid, templateid, userdatapolicy) { + this.loading = true + const params = {} + if (userdataid && userdataid.length > 0) { + params.userdataid = userdataid + } + params.templateid = templateid + if (userdatapolicy) { + params.userdatapolicy = userdatapolicy + } + api('linkUserDataToTemplate', params).then(json => { + this.closeAction() + }).catch(error => { + this.$notifyError(error) + }).finally(() => { + this.loading = false + }) } } }