From 713a236843c0c51f248d1bf05d198f935e6bf13d Mon Sep 17 00:00:00 2001 From: Harikrishna Date: Wed, 5 Oct 2022 17:34:59 +0530 Subject: [PATCH] UserData as first class resource (#6202) This PR introduces a new feature to make userdata as a first class resource much like existing SSH keys. Detailed feature specification document: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Userdata+as+a+first+class+resource --- .../main/java/com/cloud/event/EventTypes.java | 5 + .../java/com/cloud/network/NetworkModel.java | 6 +- .../com/cloud/server/ManagementService.java | 31 + .../cloud/template/TemplateApiService.java | 5 + .../template/VirtualMachineTemplate.java | 6 + .../main/java/com/cloud/user/UserData.java | 32 + .../main/java/com/cloud/uservm/UserVm.java | 8 + .../main/java/com/cloud/vm/UserVmService.java | 63 +- .../annotation/AnnotationService.java | 2 +- .../apache/cloudstack/api/ApiConstants.java | 6 + .../cloudstack/api/ResponseGenerator.java | 8 +- .../admin/vm/ResetVMUserDataCmdAdmin.java | 31 + .../user/userdata/DeleteUserDataCmd.java | 120 +++ .../userdata/LinkUserDataToTemplateCmd.java | 130 +++ .../user/userdata/ListUserDataCmd.java | 85 ++ .../user/userdata/RegisterUserDataCmd.java | 148 ++++ .../api/command/user/vm/DeployVMCmd.java | 26 + .../command/user/vm/ResetVMUserDataCmd.java | 173 ++++ .../api/command/user/vm/UpdateVMCmd.java | 32 +- .../api/response/TemplateResponse.java | 44 + .../api/response/UserDataResponse.java | 128 +++ .../api/response/UserVmResponse.java | 45 + .../command/test/ResetVMUserDataCmdTest.java | 138 ++++ .../user/userdata/DeleteUserDataCmdTest.java | 98 +++ .../LinkUserDataToTemplateCmdTest.java | 109 +++ .../user/userdata/ListUserDataCmdTest.java | 89 ++ .../userdata/RegisterUserDataCmdTest.java | 113 +++ .../subsystem/api/storage/TemplateInfo.java | 5 + .../cloud/vm/VirtualMachineManagerImpl.java | 2 +- .../java/com/cloud/storage/VMTemplateVO.java | 27 + .../com/cloud/storage/dao/VMTemplateDao.java | 2 + .../cloud/storage/dao/VMTemplateDaoImpl.java | 17 +- .../main/java/com/cloud/user/UserDataVO.java | 120 +++ .../java/com/cloud/user/dao/UserDataDao.java | 28 + .../com/cloud/user/dao/UserDataDaoImpl.java | 66 ++ .../src/main/java/com/cloud/vm/UserVmVO.java | 30 +- .../main/java/com/cloud/vm/dao/UserVmDao.java | 16 +- .../java/com/cloud/vm/dao/UserVmDaoImpl.java | 19 +- ...spring-engine-schema-core-daos-context.xml | 1 + .../META-INF/db/schema-41710to41800.sql | 324 ++++++++ .../configdrive/ConfigDriveBuilder.java | 35 +- .../configdrive/ConfigDriveBuilderTest.java | 72 +- .../storage/image/store/TemplateObject.java | 11 + .../com/cloud/hypervisor/guru/VMwareGuru.java | 2 +- ...esClusterResourceModifierActionWorker.java | 4 +- .../KubernetesClusterStartWorker.java | 8 +- .../management/ServiceVirtualMachine.java | 2 +- .../management/ManagementServerMock.java | 2 +- .../java/com/cloud/api/ApiResponseHelper.java | 18 +- .../api/query/dao/TemplateJoinDaoImpl.java | 23 + .../api/query/dao/UserVmJoinDaoImpl.java | 7 + .../cloud/api/query/vo/TemplateJoinVO.java | 34 + .../com/cloud/api/query/vo/UserVmJoinVO.java | 35 + .../com/cloud/network/NetworkModelImpl.java | 18 +- .../network/as/AutoScaleManagerImpl.java | 6 +- .../element/ConfigDriveNetworkElement.java | 29 +- .../network/router/CommandSetupHelper.java | 24 +- .../cloud/server/ManagementServerImpl.java | 240 +++++- .../cloud/template/TemplateManagerImpl.java | 39 + .../main/java/com/cloud/vm/UserVmManager.java | 2 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 223 ++++- .../annotation/AnnotationManagerImpl.java | 6 + .../cloud/network/MockNetworkModelImpl.java | 2 +- .../ConfigDriveNetworkElementTest.java | 2 +- .../network/lb/AssignLoadBalancerTest.java | 6 +- .../router/CommandSetupHelperTest.java | 82 ++ .../server/ManagementServerImplTest.java | 323 +++++++- .../storage/VolumeApiServiceImplTest.java | 10 +- .../template/TemplateManagerImplTest.java | 82 ++ .../com/cloud/vm/UserVmManagerImplTest.java | 245 +++++- .../java/com/cloud/vm/UserVmManagerTest.java | 2 +- .../com/cloud/vm/dao/UserVmDaoImplTest.java | 2 +- .../com/cloud/vpc/MockNetworkModelImpl.java | 2 +- .../affinity/AffinityApiUnitTest.java | 2 +- .../AffinityGroupServiceImplTest.java | 2 +- .../smoke/test_register_userdata.py | 766 ++++++++++++++++++ tools/marvin/marvin/lib/base.py | 59 +- ui/public/locales/en.json | 20 + ui/public/locales/pl.json | 1 + ui/src/components/view/AnnotationsTab.vue | 1 + ui/src/components/view/DetailsTab.vue | 7 + ui/src/components/view/InfoCard.vue | 8 + ui/src/components/view/ListView.vue | 4 +- ui/src/config/section/compute.js | 86 +- ui/src/config/section/image.js | 9 +- ui/src/core/lazy_lib/icons_use.js | 4 +- ui/src/views/compute/DeployVM.vue | 255 +++++- ui/src/views/compute/RegisterUserData.vue | 255 ++++++ ui/src/views/compute/ResetUserData.vue | 394 +++++++++ .../compute/wizard/UserDataSelection.vue | 202 +++++ ui/src/views/image/RegisterOrUploadIso.vue | 104 ++- .../views/image/RegisterOrUploadTemplate.vue | 104 ++- ui/src/views/image/UpdateISO.vue | 307 +++++++ ui/src/views/image/UpdateTemplate.vue | 131 ++- 94 files changed, 6480 insertions(+), 177 deletions(-) create mode 100644 api/src/main/java/com/cloud/user/UserData.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ResetVMUserDataCmdAdmin.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/userdata/DeleteUserDataCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/userdata/LinkUserDataToTemplateCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/UserDataResponse.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/command/test/ResetVMUserDataCmdTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/command/user/userdata/DeleteUserDataCmdTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/command/user/userdata/LinkUserDataToTemplateCmdTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmdTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmdTest.java create mode 100644 engine/schema/src/main/java/com/cloud/user/UserDataVO.java create mode 100644 engine/schema/src/main/java/com/cloud/user/dao/UserDataDao.java create mode 100644 engine/schema/src/main/java/com/cloud/user/dao/UserDataDaoImpl.java create mode 100644 server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java create mode 100644 test/integration/smoke/test_register_userdata.py create mode 100644 ui/src/views/compute/RegisterUserData.vue create mode 100644 ui/src/views/compute/ResetUserData.vue create mode 100644 ui/src/views/compute/wizard/UserDataSelection.vue create mode 100644 ui/src/views/image/UpdateISO.vue diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 289276fe663..ac7a612a202 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -101,6 +101,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"; @@ -235,6 +237,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 60f1aff3c62..fa44eac4a4a 100644 --- a/api/src/main/java/com/cloud/network/NetworkModel.java +++ b/api/src/main/java/com/cloud/network/NetworkModel.java @@ -18,6 +18,7 @@ package com.cloud.network; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -85,6 +86,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); @@ -325,7 +329,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 5c385cc18d9..527bb4807c3 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; @@ -56,6 +57,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; @@ -333,6 +337,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 9098f919ca4..1f8cef0365b 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; @@ -142,4 +143,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 4ec0496b890..6830d42de7b 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; @@ -146,6 +149,12 @@ public interface UserVmService { * * * + * @param sshKeyPair + * - name of the ssh key pair used to login to the virtual + * machine + * @param cpuSpeed + * @param memory + * @param cpuNumber * @param zone * - availability zone for the virtual machine * @param serviceOffering @@ -181,9 +190,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 sshKeyPair - * - name of the ssh key pair used to login to the virtual - * machine + * @param userDataId + * @param userDataDetails * @param requestedIps * TODO * @param defaultIp @@ -191,19 +199,16 @@ public interface UserVmService { * @param displayVm * - Boolean flag whether to the display the vm to the end user or not * @param affinityGroupIdList - * @param cpuSpeed - * @param memory - * @param cpuNumber * @param customId * @param dhcpOptionMap * - Maps the dhcp option code and the dhcp value to the network uuid - * @return UserVm object if successful. * @param dataDiskTemplateToDiskOfferingMap * - Datadisk template to Disk offering Map * an optional parameter that creates additional data disks for the virtual machine * For each of the templates in the map, a data disk will be created from the corresponding * disk offering obtained from the map * + * @return UserVm object if successful. * @throws InsufficientCapacityException * if there is insufficient capacity to deploy the VM. * @throws ConcurrentOperationException @@ -214,11 +219,11 @@ public interface UserVmService { * available. */ 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, List sshKeyPairs, Map requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard, - List affinityGroupIdList, Map customParameter, String customId, Map> dhcpOptionMap, - Map dataDiskTemplateToDiskOfferingMap, - Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId) throws InsufficientCapacityException, + Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, + String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard, + List affinityGroupIdList, Map customParameter, String customId, Map> dhcpOptionMap, + Map dataDiskTemplateToDiskOfferingMap, + Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; /** @@ -227,6 +232,7 @@ public interface UserVmService { * * * + * @param type * @param zone * - availability zone for the virtual machine * @param serviceOffering @@ -264,6 +270,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 requestedIps * TODO * @param defaultIps @@ -279,7 +287,6 @@ public interface UserVmService { * an optional parameter that creates additional data disks for the virtual machine * For each of the templates in the map, a data disk will be created from the corresponding * disk offering obtained from the map - * @param type * @return UserVm object if successful. * * @throws InsufficientCapacityException @@ -292,10 +299,10 @@ public interface UserVmService { * available. */ 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, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, - List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, - Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, String vmType) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; + List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, + HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, + List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, + Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, String vmType) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; /** * Creates a User VM in Advanced Zone (Security Group feature is disabled) @@ -303,6 +310,12 @@ public interface UserVmService { * * * + * @param sshKeyPair + * - name of the ssh key pair used to login to the virtual + * machine + * @param cpuSpeed + * @param memory + * @param cpuNumber * @param zone * - availability zone for the virtual machine * @param serviceOffering @@ -337,9 +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 sshKeyPair - * - name of the ssh key pair used to login to the virtual - * machine + * @param userDataId + * @param userDataDetails * @param requestedIps * TODO * @param defaultIps @@ -347,9 +359,6 @@ public interface UserVmService { * @param displayVm * - Boolean flag whether to the display the vm to the end user or not * @param affinityGroupIdList - * @param cpuSpeed - * @param memory - * @param cpuNumber * @param customId * @param dhcpOptionMap * - Map that maps the DhcpOption code and their value on the Network uuid @@ -370,10 +379,10 @@ public interface UserVmService { * available. */ 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, - List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, - Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, - Map templateOvfPropertiesMap, boolean dynamicScalingEnabled, String vmType, Long overrideDiskOfferingId) + String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, + Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, + Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, + Map templateOvfPropertiesMap, boolean dynamicScalingEnabled, String vmType, Long overrideDiskOfferingId) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; 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 2abdb328702..103ead7842f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -415,6 +415,12 @@ public class ApiConstants { public static final String USAGE_INTERFACE = "usageinterface"; public static final String USED_SUBNETS = "usedsubnets"; 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 ecf203a3472..a90c222e351 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -118,6 +118,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; @@ -201,8 +202,8 @@ import com.cloud.projects.ProjectAccount; import com.cloud.projects.ProjectInvitation; import com.cloud.region.ha.GlobalLoadBalancerRule; import com.cloud.resource.RollingMaintenanceManager; -import com.cloud.server.ResourceIcon; import com.cloud.server.ResourceTag; +import com.cloud.server.ResourceIcon; import com.cloud.storage.GuestOS; import com.cloud.storage.GuestOSHypervisor; import com.cloud.storage.ImageStore; @@ -216,9 +217,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.Pair; 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; @@ -491,6 +493,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 d974783f629..8a89a7d81cb 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; @@ -153,6 +154,12 @@ 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; @@ -420,6 +427,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 66a847ad540..d7ebe90ebde 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 @@ -336,6 +336,18 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "Base64 string containing the user data", since = "4.18.0.0") private String userData; + @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()))); @@ -964,4 +976,37 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co public void setUserData(String userData) { this.userData = userData; } + + 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 84cc1bab42d..e42d98a8bce 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -3101,7 +3101,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"); - List vmData = _networkModel.generateVmData(userVm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), + List 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 8f66da052e9..44e4dc920ab 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,7 @@ import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; +import com.cloud.user.UserData; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import com.cloud.hypervisor.Hypervisor.HypervisorType; @@ -157,6 +158,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; @@ -644,4 +652,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 e1d3dc4b3e8..ce0bd2d5717 100644 --- a/engine/schema/src/main/java/com/cloud/vm/UserVmVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/UserVmVO.java @@ -43,6 +43,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; @@ -74,9 +80,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) { + boolean limitCpuUse, long domainId, long accountId, long userId, long serviceOfferingId, String userData, Long userDataId, String userDataDetails, String name) { super(id, serviceOfferingId, name, instanceName, Type.User, templateId, hypervisorType, guestOsId, domainId, accountId, userId, haEnabled, limitCpuUse); this.userData = userData; + this.userDataId = userDataId; + this.userDataDetails = userDataDetails; this.displayName = displayName; this.details = new HashMap(); } @@ -99,6 +107,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; @@ -146,4 +164,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 f4fa5c1d405..abc8d80fbb2 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 @@ -43,14 +43,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); @@ -97,4 +100,7 @@ public interface UserVmDao extends GenericDao { List> countVmsBySize(long dcId, int limit); int getActiveAccounts(final long dcId); + + 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 af8c0a0988a..8a1039e83be 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 @@ -81,6 +81,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 @@ -219,6 +221,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"; } @@ -255,13 +261,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) { @@ -763,4 +771,11 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use return customSearch(sc, null).size(); } + + @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 d4676f3d58e..b670621de9f 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 @@ -183,6 +183,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41710to41800.sql b/engine/schema/src/main/resources/META-INF/db/schema-41710to41800.sql index ef40520991e..ce553cf4453 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41710to41800.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41710to41800.sql @@ -251,3 +251,327 @@ DELETE role_perm FROM role_permissions role_perm INNER JOIN roles ON role_perm.role_id = roles.id WHERE roles.role_type != 'Admin' AND roles.is_default = 1 AND role_perm.rule = 'migrateVolume'; + +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`, + `service_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`, + `service_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_details`.`value` AS `keypair_names`, + `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 ((`volumes`.`disk_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.KeyPairNames')))) + 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 a76bbd947a6..c7c63f80494 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.lang3.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 c1bce019172..9de07558e54 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.lang3.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 0151a7cdedb..b688197bfb9 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 70e068e76be..eb66bc01309 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 @@ -677,7 +677,7 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co vmInternalName, id, vmInternalName, templateId, guestOsId, serviceOfferingId)); UserVmVO vmInstanceVO = new UserVmVO(id, vmInternalName, vmInternalName, templateId, HypervisorType.VMware, guestOsId, false, false, domainId, accountId, userId, - serviceOfferingId, null, vmInternalName); + serviceOfferingId, null, null, null, vmInternalName); 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 81db87a7c7d..6949c0c0b1c 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 @@ -387,13 +387,13 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu List securityGroupIds = new ArrayList<>(); securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); nodeVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, securityGroupIds, owner, - hostName, hostName, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, keypairs, + hostName, hostName, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, null, null, keypairs, null, addrs, null, null, null, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE); } else { nodeVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner, hostName, hostName, null, null, null, - Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, keypairs, + Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, keypairs, null, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null); } if (LOGGER.isInfoEnabled()) { 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 5cb9c08a202..679f2e8f59b 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 @@ -221,13 +221,13 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif List securityGroupIds = new ArrayList<>(); securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); controlVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, securityGroupIds, owner, - hostName, hostName, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, keypairs, + hostName, hostName, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, null, null, keypairs, requestedIps, addrs, null, null, null, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE); } else { controlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner, hostName, hostName, null, null, null, - Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, keypairs, + Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, keypairs, requestedIps, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null); } if (LOGGER.isInfoEnabled()) { @@ -295,13 +295,13 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif List securityGroupIds = new ArrayList<>(); securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); additionalControlVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, securityGroupIds, owner, - hostName, hostName, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, keypairs, + hostName, hostName, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, null, null, keypairs, null, addrs, null, null, null, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE); } else { additionalControlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner, hostName, hostName, null, null, null, - Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, keypairs, + Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, keypairs, null, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null); } 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 b9a446b69a0..ea5e24aa161 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); + super(id, instanceName, name, templateId, hypervisorType, guestOSId, false, false, domainId, accountId, userId, serviceOfferingId, null, null, null, name); } } 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 859330071e9..afbb7355fd6 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); + 1, small.getId(), null, null, null, name); 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 52502f39cf1..af746ef6f74 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -37,6 +37,8 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import com.cloud.utils.security.CertificateHelper; +import com.cloud.user.UserData; import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.network.vpc.VpcVO; import org.apache.cloudstack.acl.ControlledEntity; @@ -156,6 +158,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; @@ -370,7 +373,6 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.Dhcp; import com.cloud.utils.net.Ip; import com.cloud.utils.net.NetUtils; -import com.cloud.utils.security.CertificateHelper; import com.cloud.vm.ConsoleProxyVO; import com.cloud.vm.InstanceGroup; import com.cloud.vm.Nic; @@ -4573,6 +4575,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 3a71b3e2b3d..8195592640d 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 d27af1de02a..6f9cc614232 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 @@ -398,6 +398,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) { @@ -2572,6 +2572,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); @@ -2640,6 +2642,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 2757525eca4..0c21437da08 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -1330,17 +1330,17 @@ 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, null); } 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,null, null, true, null, null, null, null, null, null, null, true, 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, null, null); } 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, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, null, null, addrs, true, null, null, null, null, null, null, null, true, null, 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 d95ad648d39..7bbf39a6655 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 fe0f88c3388..e3446c29b64 100644 --- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java +++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java @@ -214,7 +214,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); @@ -1185,9 +1185,9 @@ public class CommandSetupHelper { } } - 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())); @@ -1203,6 +1203,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); @@ -1237,6 +1239,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 9f1b5fe78fa..55bd8c119d8 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; @@ -43,6 +45,11 @@ import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; import javax.naming.ConfigurationException; +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.agent.api.Answer; import com.cloud.agent.api.Command; import com.cloud.agent.api.PatchSystemVmAnswer; @@ -68,6 +75,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; @@ -506,6 +514,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; @@ -518,6 +530,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; @@ -786,6 +799,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()); protected StateMachine2 _stateMachine; @@ -796,6 +812,10 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe 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 VirtualMachine.Type []systemVmTypes = { VirtualMachine.Type.SecondaryStorageVm, VirtualMachine.Type.ConsoleProxy}; + 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 @@ -829,7 +849,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe @Inject private UserDao _userDao; @Inject - private UserVmDao _userVmDao; + protected UserVmDao _userVmDao; @Inject private ConfigurationDao _configDao; @Inject @@ -927,8 +947,6 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe @Inject private VpcDao _vpcDao; @Inject - private AnnotationDao annotationDao; - @Inject private DomainVlanMapDao _domainVlanMapDao; @Inject private NicDao nicDao; @@ -936,6 +954,12 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe 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")); @@ -3455,6 +3479,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); @@ -3671,6 +3696,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; } @@ -4329,9 +4360,9 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe final String keyword = cmd.getKeyword(); final Account caller = getCaller(); - final List permittedAccounts = new ArrayList(); + final List permittedAccounts = new ArrayList<>(); - final Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); + 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(); @@ -4363,7 +4394,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } final Pair, Integer> result = _sshKeyPairDao.searchAndCount(sc, searchFilter); - return new Pair, Integer>(result.first(), result.second()); + return new Pair<>(result.first(), result.second()); } @Override @@ -4382,6 +4413,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.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 @@ -4440,6 +4645,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 */ @@ -4463,6 +4677,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 45f8c9d27dd..f441229a4bf 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -34,6 +34,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.user.UserData; import com.cloud.storage.VolumeApiService; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ApiConstants; @@ -56,6 +57,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; @@ -2289,4 +2291,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 e1397c4fbce..9829124f2e5 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -75,6 +75,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; @@ -327,8 +328,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; @@ -569,6 +572,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private StatsCollector statsCollector; + @Inject + private UserDataDao userDataDao; @Inject protected SnapshotHelper snapshotHelper; @@ -899,6 +904,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 { @@ -2437,6 +2485,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()); } @@ -2719,7 +2772,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(); @@ -2728,12 +2780,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(); @@ -2795,7 +2854,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()); } @@ -2893,7 +2952,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) { @@ -2940,6 +2999,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(); } @@ -3035,7 +3102,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); @@ -3048,7 +3115,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())); @@ -3456,10 +3523,10 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override @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, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, - Map customParametes, String customId, Map> dhcpOptionMap, - Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, + Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, + String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, + Map customParametes, String customId, Map> dhcpOptionMap, + Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); @@ -3507,7 +3574,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, - userData, sshKeyPairs, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParametes, customId, dhcpOptionMap, + userData, userDataId, userDataDetails, sshKeyPairs, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParametes, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null, overrideDiskOfferingId); } @@ -3515,10 +3582,10 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override @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, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, - List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, - Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, String vmType) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { + List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, + HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, + List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, + Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, String vmType) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); List networkList = new ArrayList(); @@ -3617,17 +3684,17 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, - userData, sshKeyPairs, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap, + userData, userDataId, userDataDetails, sshKeyPairs, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, vmType, overrideDiskOfferingId); } @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, - List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayvm, String keyboard, List affinityGroupIdList, - Map customParametrs, String customId, Map> dhcpOptionsMap, Map dataDiskTemplateToDiskOfferingMap, - Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, Long overrideDiskOfferingId) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, + String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, + Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayvm, String keyboard, List affinityGroupIdList, + Map customParametrs, String customId, Map> dhcpOptionsMap, Map dataDiskTemplateToDiskOfferingMap, + Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, Long overrideDiskOfferingId) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); @@ -3677,7 +3744,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, - sshKeyPairs, hypervisor, caller, requestedIps, defaultIps, displayvm, keyboard, affinityGroupIdList, customParametrs, customId, dhcpOptionsMap, + userDataId, userDataDetails, sshKeyPairs, hypervisor, caller, requestedIps, defaultIps, displayvm, keyboard, affinityGroupIdList, customParametrs, customId, dhcpOptionsMap, dataDiskTemplateToDiskOfferingMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, overrideDiskOfferingId); } @@ -3794,7 +3861,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, - List sshKeyPairs, HypervisorType hypervisor, Account caller, Map requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, + Long userDataId, String userDataDetails, List sshKeyPairs, 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 vmType, Long overrideDiskOfferingId) throws InsufficientCapacityException, ResourceUnavailableException, @@ -3889,7 +3956,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir DiskOfferingVO diskOffering = _diskOfferingDao.findById(diskOfferingId); volumesSize += verifyAndGetDiskSize(diskOffering, diskSize); } - UserVm vm = getCheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, sshKeyPairs, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, rootDiskOfferingId, volumesSize); + UserVm vm = getCheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPairs, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, rootDiskOfferingId, volumesSize); _securityGroupMgr.addInstanceToGroups(vm.getId(), securityGroupIdList); @@ -3900,13 +3967,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir CallContext.current().putContextParameter(VirtualMachine.class, vm.getUuid()); 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, List sshKeyPairs, 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 rootDiskOfferingId, 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, List sshKeyPairs, 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 rootDiskOfferingId, 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, sshKeyPairs, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, rootDiskOfferingId, volumesSize); + return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPairs, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, rootDiskOfferingId, volumesSize); } catch (ResourceAllocationException | CloudRuntimeException e) { throw e; } catch (Exception e) { @@ -3915,11 +3982,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } else { - return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, sshKeyPairs, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, rootDiskOfferingId, volumesSize); + return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPairs, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, rootDiskOfferingId, 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, List sshKeyPairs, 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 rootDiskOfferingId, 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, List sshKeyPairs, 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 rootDiskOfferingId, 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)) { @@ -4195,7 +4262,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir dynamicScalingEnabled = dynamicScalingEnabled && checkIfDynamicScalingCanBeEnabled(null, offering, template, zone.getId()); - UserVmVO vm = commitUserVm(zone, template, hostName, displayName, owner, diskOfferingId, diskSize, userData, caller, isDisplayVm, keyboard, accountId, userId, offering, + UserVmVO vm = commitUserVm(zone, template, hostName, displayName, owner, diskOfferingId, diskSize, userData, userDataId, userDataDetails, caller, isDisplayVm, keyboard, accountId, userId, offering, isIso, sshPublicKeys, networkNicMap, id, instanceName, uuidName, hypervisorType, customParameters, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, rootDiskOfferingId, keypairnames); @@ -4332,7 +4399,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 Boolean isDisplayVm, final String keyboard, final long accountId, final long userId, final ServiceOffering offering, final boolean isIso, final String sshPublicKeys, final LinkedHashMap> networkNicMap, final long id, final String instanceName, final String uuidName, final HypervisorType hypervisorType, final Map customParameters, final Map> extraDhcpOptionMap, final Map dataDiskTemplateToDiskOfferingMap, @@ -4341,7 +4408,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); + offering.getLimitCpuUse(), owner.getDomainId(), owner.getId(), userId, offering.getId(), userData, userDataId, userDataDetails, hostName); vm.setUuid(uuidName); vm.setDynamicallyScalable(dynamicScalingEnabled); @@ -4555,13 +4622,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 accountId, final long userId, final ServiceOfferingVO offering, final boolean isIso, final String sshPublicKeys, final LinkedHashMap> networkNicMap, - final long id, final String instanceName, final String uuidName, final HypervisorType hypervisorType, final Map customParameters, final Map> 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 vmType, final Long rootDiskOfferingId, String sshkeypairs) throws InsufficientCapacityException { + Map userVmOVFPropertiesMap, final boolean dynamicScalingEnabled, String vmType, final Long rootDiskOfferingId, String sshkeypairs) throws InsufficientCapacityException { return commitUserVm(false, zone, null, null, template, hostName, displayName, owner, - diskOfferingId, diskSize, userData, caller, isDisplayVm, keyboard, + diskOfferingId, diskSize, userData, userDataId, userDataDetails, isDisplayVm, keyboard, accountId, userId, offering, isIso, sshPublicKeys, networkNicMap, id, instanceName, uuidName, hypervisorType, customParameters, extraDhcpOptionMap, dataDiskTemplateToDiskOfferingMap, @@ -4934,7 +5001,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"; @@ -5685,6 +5752,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 { @@ -5775,6 +5909,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(); @@ -5792,7 +5934,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(); List sshKeyPairNames = cmd.getSSHKeyPairNames(); Boolean displayVm = cmd.isDisplayVm(); String keyboard = cmd.getKeyboard(); @@ -5803,14 +5944,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, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(), + size , group , cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, overrideDiskOfferingId); } } else { if (zone.isSecurityGroupEnabled()) { vm = createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, networkIds, getSecurityGroupIdList(cmd), owner, name, - displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, + displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, overrideDiskOfferingId, null); @@ -5819,7 +5960,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, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), + cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null, overrideDiskOfferingId); } } @@ -7964,7 +8105,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, isDisplayVm, keyboard, accountId, userId, serviceOffering, template.getFormat().equals(ImageFormat.ISO), sshPublicKeys, null, id, instanceName, uuidName, hypervisorType, customParameters, null, null, null, powerState, dynamicScalingEnabled, null, serviceOffering.getDiskOfferingId(), 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 1a60b66d9d8..5c43dbd30c4 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; @@ -152,6 +153,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); @@ -165,6 +168,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); @@ -507,6 +511,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 6f437b8be56..cc919e0267e 100644 --- a/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java +++ b/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java @@ -911,7 +911,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/lb/AssignLoadBalancerTest.java b/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java index 12d4912e296..060f2e1a5fd 100644 --- a/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java +++ b/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java @@ -157,7 +157,7 @@ public class AssignLoadBalancerTest { vmIds.add(2L); LoadBalancerVO lbVO = new LoadBalancerVO("1", "L1", "Lbrule", 1, 22, 22, "rb", 204, 0, 0, "tcp", null); - UserVmVO vm = new UserVmVO(2L, "test", "test", 101L, Hypervisor.HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", "test"); + UserVmVO vm = new UserVmVO(2L, "test", "test", 101L, Hypervisor.HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", 1L, null, "test"); LoadBalancerDao lbDao = Mockito.mock(LoadBalancerDao.class); LoadBalancerVMMapDao lb2VmMapDao = Mockito.mock(LoadBalancerVMMapDao.class); @@ -200,7 +200,7 @@ public class AssignLoadBalancerTest { vmIds.add(2L); LoadBalancerVO lbVO = new LoadBalancerVO("1", "L1", "Lbrule", 1, 22, 22, "rb", 204, 0, 0, "tcp", null); - UserVmVO vm = new UserVmVO(2L, "test", "test", 101L, Hypervisor.HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", "test"); + UserVmVO vm = new UserVmVO(2L, "test", "test", 101L, Hypervisor.HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", 1L, null, "test"); LoadBalancerDao lbDao = Mockito.mock(LoadBalancerDao.class); LoadBalancerVMMapDao lb2VmMapDao = Mockito.mock(LoadBalancerVMMapDao.class); @@ -245,7 +245,7 @@ public class AssignLoadBalancerTest { vmIds.add(2L); LoadBalancerVO lbVO = new LoadBalancerVO("1", "L1", "Lbrule", 1, 22, 22, "rb", 204, 0, 0, "tcp", null); - UserVmVO vm = new UserVmVO(2L, "test", "test", 101L, Hypervisor.HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", "test"); + UserVmVO vm = new UserVmVO(2L, "test", "test", 101L, Hypervisor.HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", 1L, null, "test"); LoadBalancerDao lbDao = Mockito.mock(LoadBalancerDao.class); LoadBalancerVMMapDao lb2VmMapDao = Mockito.mock(LoadBalancerVMMapDao.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 2d6969fe966..3c169cda55d 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -16,21 +16,46 @@ // 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.address.ListPublicIpAddressesCmd; 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.dc.Vlan.VlanType; @@ -44,7 +69,11 @@ 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 @@ -61,7 +90,6 @@ public class ManagementServerImplTest { @Mock SSHKeyPairDao sshKeyPairDao; - ManagementServerImpl ms = new ManagementServerImpl(); @Mock SSHKeyPair sshKeyPair; @@ -69,15 +97,42 @@ public class ManagementServerImplTest { @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) @@ -221,4 +276,264 @@ public class ManagementServerImplTest { Mockito.verify(sc, Mockito.times(1)).setParameters("sourceNetworkId", 10L); Mockito.verify(sc, Mockito.never()).setParameters("forsystemvms", false); } + + @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 29374a3fe3b..acc9d5886f8 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -220,7 +220,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"); + UserVmVO runningVm = new UserVmVO(1L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm"); runningVm.setState(State.Running); runningVm.setDataCenterId(1L); when(userVmDaoMock.findById(1L)).thenReturn(runningVm); @@ -230,13 +230,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"); + UserVmVO stoppedVm = new UserVmVO(2L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm"); 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"); + UserVmVO hyperVVm = new UserVmVO(3L, "vm", "vm", 1, HypervisorType.Hyperv, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm"); hyperVVm.setState(State.Stopped); hyperVVm.setDataCenterId(1L); when(userVmDaoMock.findById(3L)).thenReturn(hyperVVm); @@ -290,7 +290,7 @@ public class VolumeApiServiceImplTest { managedVolume1.setDataCenterId(1L); // vm having root volume - UserVmVO vmHavingRootVolume = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, "vm"); + UserVmVO vmHavingRootVolume = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm"); vmHavingRootVolume.setState(State.Stopped); vmHavingRootVolume.setDataCenterId(1L); when(userVmDaoMock.findById(4L)).thenReturn(vmHavingRootVolume); @@ -314,7 +314,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"); + UserVmVO kvmVm = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.KVM, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm"); 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 0a00d283292..5dc50bc7339 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; @@ -132,6 +136,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 { @@ -186,6 +191,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, @@ -506,6 +514,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 22b7f223e6e..d8219611253 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; @@ -155,6 +163,9 @@ public class UserVmManagerImplTest { @Mock VolumeApiService volumeApiService; + @Mock + UserDataDao userDataDao; + private long vmId = 1l; private static final long GiB_TO_BYTES = 1024 * 1024 * 1024; @@ -254,6 +265,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(); @@ -269,6 +281,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(); @@ -319,6 +332,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(); @@ -345,7 +359,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)); } @@ -356,7 +370,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()); } @@ -573,4 +587,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 640b35f131c..7cc2c8a6be1 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerTest.java @@ -516,7 +516,7 @@ public class UserVmManagerTest { AccountVO newAccount = new AccountVO("testaccount", 1, "networkdomain", Account.Type.ADMIN, 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"); + UserVmVO vm = new UserVmVO(10L, "test", "test", 1L, HypervisorType.Any, 1L, false, false, 1L, 1L, 1, 5L, "test", null, null, "test"); 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 7dbfba304e8..e25a069533f 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); + null, null, name); 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 ccb78e3b027..291a2e20f01 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java @@ -926,7 +926,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 f43fcc55c0d..2e56484d39b 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"); + UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", null, null, "test"); 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 bd2e45f013e..30e1e45c3f6 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"); + UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, DOMAIN_ID, 200L, 1, 5L, "", null, null, "test"); 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 c6a595810b0..00e834ffc4f 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"] @@ -1665,6 +1671,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""" @@ -4998,6 +5016,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 e0e56986178..065528c210c 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -165,6 +165,7 @@ "label.action.unmanage.virtualmachine": "Unmanage VM", "label.action.update.offering.access": "Update offering access", "label.action.update.resource.count": "Update 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", @@ -1386,6 +1387,7 @@ "label.refresh": "Refresh", "label.region": "Region", "label.register.template": "Register template", +"label.register.user.data": "Register a userdata", "label.reinstall.vm": "Reinstall VM", "label.reject": "Reject", "label.related": "Related", @@ -1408,6 +1410,7 @@ "label.remove.project.user": "Remove user from project", "label.remove.rule": "Remove rule", "label.remove.ssh.key.pair": "Remove SSH Key pair", +"label.remove.user.data": "Remove userdata", "label.remove.vm.from.lb": "Remove VM from load balancer rule", "label.remove.vmware.datacenter": "Remove VMware Datacenter", "label.remove.vpc": "Remove VPC", @@ -1432,6 +1435,7 @@ "label.reset.config.value": "Reset to default value", "label.reset.ssh.key.pair": "Reset SSH key pair", "label.reset.to.default": "Reset to default", +"label.reset.userdata.on.vm": "Reset Userdata on VM", "label.reset.vpn.connection": "Reset VPN connection", "label.resource": "Resource", "label.resource.limit.exceeded": "Resource limit exceeded", @@ -1590,6 +1594,17 @@ "label.srx": "SRX", "label.srx.firewall": "Juniper SRX firewall", "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 key pairs", @@ -2172,6 +2187,8 @@ "message.desc.primary.storage": "Each cluster must contain one or more primary storage servers. 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. 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.", @@ -2300,6 +2317,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.", @@ -2407,6 +2425,7 @@ "message.ovf.configurations": "OVF configurations available for the selected appliance. Please select the desired value. Incompatible compute offerings will get disabled.", "message.path.description": "NFS: exported path from the server. VMFS: /datacenter name/datastore name. SharedMountPoint: path where primary storage is mounted, such as /mnt/primary.", "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.wait.while.zone.is.being.created": "Please wait while your zone is being created; this may take a while...", @@ -2541,6 +2560,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", diff --git a/ui/public/locales/pl.json b/ui/public/locales/pl.json index 1136ac50765..b77b896b1f1 100644 --- a/ui/public/locales/pl.json +++ b/ui/public/locales/pl.json @@ -1381,6 +1381,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 dc0282ead66..daad48c5b81 100644 --- a/ui/src/components/view/AnnotationsTab.vue +++ b/ui/src/components/view/AnnotationsTab.vue @@ -173,6 +173,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 a86026912fb..e58ea100fd7 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -51,6 +51,9 @@
    {{ $toLocaleDate(dataResource[item]) }}
    +
    +
    {{ decodeUserData(dataResource.userdata)}}
    +
    {{ dataResource[item]? $t('message.egress.rules.allow') : $t('message.egress.rules.deny') }}
    @@ -166,6 +169,10 @@ export default { } }, methods: { + decodeUserData (userdata) { + const decodedData = Buffer.from(userdata, 'base64') + return decodedData.toString('utf-8') + }, fetchProjectAdmins () { if (!this.dataResource.owner) { return false diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 0bc6df661aa..1d88c46c885 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -533,6 +533,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 981152fe216..690e4f0a858 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -511,7 +511,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', @@ -521,7 +521,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 5249185651d..c568a002989 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -77,7 +77,9 @@ export default { }, searchFilters: ['name', 'zoneid', 'domainid', 'account', 'groupid', 'tags'], details: () => { - var fields = ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename'] + var fields = ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', + 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', + 'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy'] const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true) if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) { return fields @@ -365,6 +367,17 @@ export default { popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ResetSshKeyPair'))) }, + { + api: 'resetUserDataForVirtualMachine', + icon: 'solution-outlined', + 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: shallowRef(defineAsyncComponent(() => import('@/views/compute/ResetUserData'))) + }, { api: 'assignVirtualMachine', icon: 'user-add-outlined', @@ -633,6 +646,77 @@ export default { } ] }, + { + name: 'userdata', + title: 'label.user.data', + icon: 'solution-outlined', + 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: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { + name: 'comments', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/AnnotationsTab.vue'))) + } + ], + actions: [ + { + api: 'registerUserData', + icon: 'plus-outlined', + label: 'label.register.user.data', + docHelp: 'adminguide/virtual_machines.html#creating-the-ssh-keypair', + listView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/compute/RegisterUserData.vue'))) + }, + { + api: 'deleteUserData', + icon: 'delete-outlined', + 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 e428c0578c0..8d485993097 100644 --- a/ui/src/config/section/image.js +++ b/ui/src/config/section/image.js @@ -47,7 +47,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') } @@ -101,7 +101,7 @@ export default { { api: 'updateTemplate', icon: 'edit-outlined', - label: 'label.edit', + label: 'label.action.edit.template', dataView: true, show: (record, store) => { return (['Admin'].includes(store.userInfo.roletype) || // If admin or owner or belongs to current project @@ -195,7 +195,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', @@ -250,7 +250,8 @@ export default { !(record.account === 'system' && record.domainid === 1) && record.isready }, - args: ['name', 'displaytext', 'bootable', 'ostypeid'] + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/image/UpdateISO.vue'))) }, { api: 'updateIsoPermissions', diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js index e02578d99ab..895379a415c 100644 --- a/ui/src/core/lazy_lib/icons_use.js +++ b/ui/src/core/lazy_lib/icons_use.js @@ -156,7 +156,8 @@ import { UsergroupDeleteOutlined, UserOutlined, UploadOutlined, - WifiOutlined + WifiOutlined, + SolutionOutlined } from '@ant-design/icons-vue' import renderIcon from '@/utils/renderIcon' @@ -304,5 +305,6 @@ export default { app.component('UploadOutlined', UploadOutlined) app.component('WifiOutlined', WifiOutlined) app.component('renderIcon', renderIcon) + app.component('SolutionOutlined', SolutionOutlined) } } diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 0f0e96acf38..e604a438b2e 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -551,7 +551,7 @@ ref="bootintosetup"> - + @@ -563,10 +563,120 @@ @change="val => { dynamicscalingenabled = val }"/> - - - + + +
    + + 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' }, @@ -1273,6 +1443,8 @@ export default { template (oldValue, newValue) { if (oldValue && newValue && oldValue.id !== newValue.id) { this.dynamicscalingenabled = this.isDynamicallyScalable() + this.doUserdataOverride = false + this.doUserdataAppend = false } }, created () { @@ -1586,6 +1758,8 @@ export default { this.defaultBootType = this.template?.details?.UEFI ? 'UEFI' : '' this.fetchBootModes(this.defaultBootType) this.defaultBootMode = this.template?.details?.UEFI + this.updateTemplateLinkedUserData(this.template.userdataid) + this.userdataDefaultOverridePolicy = this.template.userdatapolicy } } else if (name === 'isoid') { this.templateConfigurations = [] @@ -1597,6 +1771,8 @@ export default { this.resetFromTemplateConfiguration() this.form.isoid = value this.form.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[name] = value @@ -1644,6 +1820,56 @@ export default { this.form.keypairs = names this.sshKeyPairs = names.map((sshKeyPair) => { return sshKeyPair.name }) }, + updateUserData (id) { + if (id === '0') { + this.form.userdataid = undefined + return + } + this.form.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') }, @@ -1830,6 +2056,7 @@ export default { } // step 7: select ssh key pair deployVmData.keypairs = this.sshKeyPairs.join(',') + deployVmData.userdataid = values.userdataid if (values.name) { deployVmData.name = values.name @@ -1858,6 +2085,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) { @@ -2149,6 +2390,10 @@ export default { this.fetchAllIsos() } }, + onUserdataTabChange (key, type) { + this[type] = key + this.userDataParams = [] + }, fetchTemplateNics (template) { var nics = [] this.nicToNetworkSelection = [] diff --git a/ui/src/views/compute/RegisterUserData.vue b/ui/src/views/compute/RegisterUserData.vue new file mode 100644 index 00000000000..36e469f676c --- /dev/null +++ b/ui/src/views/compute/RegisterUserData.vue @@ -0,0 +1,255 @@ +// 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..b7287751032 --- /dev/null +++ b/ui/src/views/compute/ResetUserData.vue @@ -0,0 +1,394 @@ +// 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..4dfc14e21ca --- /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 b243a58f35b..55588871112 100644 --- a/ui/src/views/image/RegisterOrUploadIso.vue +++ b/ui/src/views/image/RegisterOrUploadIso.vue @@ -121,6 +121,47 @@ + + + + + + {{ opt.name || opt.description }} + + + + + + + + + + {{ opt.id || opt.description }} + + + + + + @@ -153,6 +194,7 @@ import store from '@/store' import { axios } from '../../utils/request' import { mixinForm } from '@/utils/mixin' import ResourceIcon from '@/components/view/ResourceIcon' +import TooltipLabel from '@/components/widgets/TooltipLabel' export default { name: 'RegisterIso', @@ -168,7 +210,8 @@ export default { } }, components: { - ResourceIcon + ResourceIcon, + TooltipLabel }, data () { return { @@ -177,6 +220,10 @@ export default { osTypes: [], zoneLoading: false, osTypeLoading: false, + userdata: {}, + userdataid: null, + userdatapolicy: null, + userdatapolicylist: {}, loading: false, allowed: false, uploadParams: null, @@ -186,6 +233,7 @@ export default { }, beforeCreate () { this.apiParams = this.$getApiParams('registerIso') + this.linkUserDataParams = this.$getApiParams('linkUserDataToTemplate') }, created () { this.initForm() @@ -220,6 +268,8 @@ export default { fetchData () { this.fetchZoneData() this.fetchOsType() + this.fetchUserData() + this.fetchUserdataPolicy() }, fetchZoneData () { const params = {} @@ -250,6 +300,36 @@ export default { this.form.ostypeid = this.osTypes[0].id }) }, + 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 + }) + }, + 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 + }, handleRemove (file) { const index = this.fileList.indexOf(file) const newFileList = this.fileList.slice() @@ -336,6 +416,9 @@ export default { if (this.currentForm === 'Create') { this.loading = true api('registerIso', params).then(json => { + if (this.userdataid !== null) { + this.linkUserdataToTemplate(this.userdataid, json.registerisoresponse.iso[0].id, this.userdatapolicy) + } this.$notification.success({ message: this.$t('label.action.register.iso'), description: `${this.$t('message.success.register.iso')} ${params.name}` @@ -356,6 +439,9 @@ export default { api('getUploadParamsForIso', params).then(json => { this.uploadParams = (json.postuploadisoresponse && json.postuploadisoresponse.getuploadparams) ? json.postuploadisoresponse.getuploadparams : '' const response = this.handleUpload() + if (this.userdataid !== null) { + this.linkUserdataToTemplate(this.userdataid, json.postuploadisoresponse.iso[0].id) + } if (response === 'upload successful') { this.$notification.success({ message: this.$t('message.success.upload'), @@ -375,6 +461,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 + }) } } } diff --git a/ui/src/views/image/RegisterOrUploadTemplate.vue b/ui/src/views/image/RegisterOrUploadTemplate.vue index eaeffe5bcdb..4fe043d8469 100644 --- a/ui/src/views/image/RegisterOrUploadTemplate.vue +++ b/ui/src/views/image/RegisterOrUploadTemplate.vue @@ -253,6 +253,46 @@
    + + + + + + {{ opt.name || opt.description }} + + + + + + + + + + {{ opt.id || opt.description }} + + + + + @@ -318,6 +358,7 @@ import store from '@/store' import { axios } from '../../utils/request' import { mixinForm } from '@/utils/mixin' import ResourceIcon from '@/components/view/ResourceIcon' +import TooltipLabel from '@/components/widgets/TooltipLabel' export default { name: 'RegisterOrUploadTemplate', @@ -333,7 +374,8 @@ export default { } }, components: { - ResourceIcon + ResourceIcon, + TooltipLabel }, data () { return { @@ -349,6 +391,10 @@ export default { format: {}, osTypes: {}, defaultOsType: '', + userdata: {}, + userdataid: null, + userdatapolicy: null, + userdatapolicylist: {}, defaultOsId: null, hyperKVMShow: false, hyperXenServerShow: false, @@ -366,6 +412,7 @@ export default { }, beforeCreate () { this.apiParams = this.$getApiParams('registerTemplate') + this.linkUserDataParams = this.$getApiParams('linkUserDataToTemplate') }, created () { this.initForm() @@ -405,6 +452,8 @@ export default { fetchData () { this.fetchZone() this.fetchOsTypes() + this.fetchUserData() + this.fetchUserdataPolicy() if (Object.prototype.hasOwnProperty.call(store.getters.apis, 'listConfigurations')) { if (this.allowed && this.hyperXenServerShow) { this.fetchXenServerProvider() @@ -520,6 +569,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' @@ -712,6 +775,23 @@ export default { } 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] @@ -830,6 +910,9 @@ export default { if (this.currentForm === 'Create') { this.loading = true api('registerTemplate', params).then(json => { + if (this.userdataid !== null) { + this.linkUserdataToTemplate(this.userdataid, json.registertemplateresponse.template[0].id, this.userdatapolicy) + } this.$notification.success({ message: this.$t('label.register.template'), description: `${this.$t('message.success.register.template')} ${params.name}` @@ -853,6 +936,9 @@ export default { api('getUploadParamsForTemplate', params).then(json => { this.uploadParams = (json.postuploadtemplateresponse && json.postuploadtemplateresponse.getuploadparams) ? json.postuploadtemplateresponse.getuploadparams : '' this.handleUpload() + if (this.userdataid !== null) { + this.linkUserdataToTemplate(this.userdataid, json.postuploadtemplateresponse.template[0].id) + } }).catch(error => { this.$notifyError(error) }).finally(() => { @@ -881,6 +967,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 (arrSelectReset) { arrSelectReset.forEach(name => { this.form[name] = undefined diff --git a/ui/src/views/image/UpdateISO.vue b/ui/src/views/image/UpdateISO.vue new file mode 100644 index 00000000000..2fa797996d4 --- /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 8ecbee60b30..280ae6a69d2 100644 --- a/ui/src/views/image/UpdateTemplate.vue +++ b/ui/src/views/image/UpdateTemplate.vue @@ -104,6 +104,46 @@ + + + + + + {{ opt.name || opt.description }} + + + + + + + + + + {{ opt.id || opt.description }} + + + + +