User data as first class resource (#193)

* UserData as first clas resource

* Few fixes

* Added userdata id in deploy VM flow

* Fixed userdata append scenario between template userdata and user provided userdata

* UI: added a new section for userdata

* Added userdata details to the deployVM cmd flow

* Write userdata details into metadata VR

* Updated template response and views

* UI: added userdata id to deloyvm wizard

* Added userdata list to deploy VM form

* Added userdata params to registeruserdata UI form

* Small fixes and added userdata to updateVM flow

* Fixed unit tests and imports

* Userdata Navigation in template view

* Added userdata denyoverride flag and userdata params section

* Added ToolTips and fixes deploy VM, register userdata form

* added userdata policy list to register template form

* Allow override append of userdata in deploy VM flow

* update userdata linking to template

* Few UI fixes in deploy VM and edit template form

* fixes in deploy VM form to support deny userdata policy

* Added unit tests for userdata

* Added unit tests for linking userdata to template

* Remove unused imports

* Move test file to proper files

* Fix unused imports

* Fix Userdata delete flow

* Few improvements in the code

* Adding marvin tests for userdata

* Fixed marvin tests for registerd userdata

* Added few more marvin tests for userdata

* Few code fixes

* Few more code fixes

* Added userdata details to register and upload iso forms

* Added userdata selection in deploy VM form for ISOs

* Add comments section to userdata

* Added new API to reset UserData of a VM along with UpdateVM API

* Added new UnitTests for resetVMUserData

* Added resetVMUserdata in UI for stopped VM

* Added blank values for userdata in edit template form

* Added ISO id to the linkuserdatatotemplate API

* Added validation to userdata params so that it wont contain any VR metadafile names

* Removed required param for iso id in linkuserdatatotemplate cmd

* Added length to userdata param

* remove delete cascade on user_vm and vm_template tables for userdata id foreign key reference.

* Fix custom userdata params for config drive by adding userdata file name and value metadata json

* Fix marvin test case

* added comments to marvin test cases

* Fix document link in UI

* Added a check while deleting the userdata, to see if no VMs are using the userdata

* Added unit tests

* Removed labels added during merge

* added success message for userdata registration

* Added Schema changes to 4160 to 4161 upgrade path

* Fixed imports and some errors

* Fix service offering uuid in mysql view

* UI changes wrt to 4.16 branch mostly related to vue2

* Some UI fixes

* Register userdata and update template form fixes

* Fixed compute.js related to userdata

* UI fixes and user_vm_view wrt sshkey

* Fixed update template form

* Fix deploy VM and userdata reset forms

* Fixed Register and upload template and ISO forms

* Fixed getting params for userdata from template in instance creation form

* Removed CloudZonesNetworkElement.java as part of rebase, which is actually removed in https://github.com/shapeblue/cloudstack-apple/pull/191

* Fixed userdata selection in deploy vm and reset userdata vm forms

* Fixed method calls after rebase
diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java
index 5880804..4baa3a3 100644
--- a/api/src/main/java/com/cloud/event/EventTypes.java
+++ b/api/src/main/java/com/cloud/event/EventTypes.java
@@ -100,6 +100,8 @@
     public static final String EVENT_VM_DYNAMIC_SCALE = "VM.DYNAMIC.SCALE";
     public static final String EVENT_VM_RESETPASSWORD = "VM.RESETPASSWORD";
     public static final String EVENT_VM_RESETSSHKEY = "VM.RESETSSHKEY";
+
+    public static final String EVENT_VM_RESETUSERDATA = "VM.RESETUSERDATA";
     public static final String EVENT_VM_MIGRATE = "VM.MIGRATE";
     public static final String EVENT_VM_MOVE = "VM.MOVE";
     public static final String EVENT_VM_RESTORE = "VM.RESTORE";
@@ -229,6 +231,9 @@
     //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 f2d1a13..be59ac4 100644
--- a/api/src/main/java/com/cloud/network/NetworkModel.java
+++ b/api/src/main/java/com/cloud/network/NetworkModel.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableMap;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -82,6 +83,9 @@
             .put(HYPERVISOR_HOST_NAME_FILE, HYPERVISOR_HOST_NAME_FILE)
             .build();
 
+    List<String> 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<Integer> MACIdentifier = new ConfigKey<Integer>("Advanced",Integer.class, "mac.identifier", "0",
             "This value will be used while generating the mac addresses for isolated and shared networks. The hexadecimal equivalent value will be present at the 2nd octet of the mac address. Default value is null which means this feature is disabled.Its scope is global.", true, ConfigKey.Scope.Global);
 
@@ -318,7 +322,7 @@
 
     boolean getNetworkEgressDefaultPolicy(Long networkId);
 
-    List<String[]> generateVmData(String userData, String serviceOffering, long datacenterId,
+    List<String[]> 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 868e606..050b93b 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.List;
 import java.util.Map;
 
+import com.cloud.user.UserData;
 import org.apache.cloudstack.api.command.admin.cluster.ListClustersCmd;
 import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd;
 import org.apache.cloudstack.api.command.admin.config.UpdateHypervisorCapabilitiesCmd;
@@ -55,6 +56,9 @@
 import org.apache.cloudstack.api.command.user.ssh.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 @@
     String generateRandomPassword();
 
     /**
+     * Search registered userdatas for the logged in user.
+     *
+     * @param cmd
+     *            The api command class.
+     * @return The list of userdatas found.
+     */
+    Pair<List<? extends UserData>, 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.
      *
      * @param cmd
diff --git a/api/src/main/java/com/cloud/template/TemplateApiService.java b/api/src/main/java/com/cloud/template/TemplateApiService.java
index ea818a5..5b494c3 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.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 @@
 
     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 @@
     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 95d1ebf..9a89d4d 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 @@
 import java.util.Date;
 import java.util.Map;
 
+import com.cloud.user.UserData;
 import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.api.Identity;
 import org.apache.cloudstack.api.InternalIdentity;
@@ -140,4 +141,9 @@
     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 0000000..fa0c504
--- /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 16a2037..e30f5e0 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 @@
 
     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 d6252c0..36ba95b 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.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 @@
      */
     UserVm resetVMSSHKey(ResetVMSSHKeyCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException;
 
+    UserVm resetVMUserData(ResetVMUserDataCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException;
+
     UserVm startVirtualMachine(StartVMCmd cmd) throws StorageUnavailableException, ExecutionException, ConcurrentOperationException, ResourceUnavailableException,
         InsufficientCapacityException, ResourceAllocationException;
 
@@ -181,6 +184,8 @@
      *            base64 encoded before adding it to the request. Currently only
      *            HTTP GET is supported. Using HTTP GET (via querystring), you
      *            can send up to 2KB of data after base64 encoding
+     * @param userDataId
+     * @param userDataDetails
      * @param sshKeyPair
      *            - name of the ssh key pair used to login to the virtual
      *            machine
@@ -215,7 +220,7 @@
      */
     UserVm createBasicSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List<Long> securityGroupIdList,
         Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod,
-        String userData, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard,
+        String userData, Long userDataId, String userDataDetails, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard,
         List<Long> affinityGroupIdList, Map<String, String> customParameter, String customId, Map<String, Map<Integer, String>> dhcpOptionMap,
         Map<Long, DiskOffering> dataDiskTemplateToDiskOfferingMap,
         Map<String, String> userVmOVFProperties, boolean dynamicScalingEnabled) throws InsufficientCapacityException,
@@ -264,6 +269,8 @@
      *            base64 encoded before adding it to the request. Currently only
      *            HTTP GET is supported. Using HTTP GET (via querystring), you
      *            can send up to 2KB of data after base64 encoding
+     * @param userDataId
+     * @param userDataDetails
      * @param sshKeyPair
      *            - name of the ssh key pair used to login to the virtual
      *            machine
@@ -297,7 +304,7 @@
      */
     UserVm createAdvancedSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List<Long> networkIdList,
         List<Long> securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor,
-        HTTPMethod httpmethod, String userData, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard,
+        HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard,
         List<Long> affinityGroupIdList, Map<String, String> customParameters, String customId, Map<String, Map<Integer, String>> dhcpOptionMap,
         Map<Long, DiskOffering> dataDiskTemplateToDiskOfferingMap,
         Map<String, String> userVmOVFProperties, boolean dynamicScalingEnabled) throws InsufficientCapacityException,
@@ -343,6 +350,8 @@
      *            base64 encoded before adding it to the request. Currently only
      *            HTTP GET is supported. Using HTTP GET (via querystring), you
      *            can send up to 2KB of data after base64 encoding
+     * @param userDataId
+     * @param userDataDetails
      * @param sshKeyPair
      *            - name of the ssh key pair used to login to the virtual
      *            machine
@@ -377,7 +386,7 @@
      */
     UserVm createAdvancedVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List<Long> networkIdList, Account owner,
         String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData,
-        String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List<Long> affinityGroupIdList,
+        Long userDataId, String userDataDetails, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List<Long> affinityGroupIdList,
         Map<String, String> customParameters, String customId, Map<String, Map<Integer, String>> dhcpOptionMap, Map<Long, DiskOffering> dataDiskTemplateToDiskOfferingMap,
         Map<String, String> templateOvfPropertiesMap, boolean dynamicScalingEnabled, String type)
 
diff --git a/api/src/main/java/org/apache/cloudstack/annotation/AnnotationService.java b/api/src/main/java/org/apache/cloudstack/annotation/AnnotationService.java
index 0aca007..6e47347 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 @@
 
     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 4f80e47..83fcad5 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -394,6 +394,12 @@
     public static final String URL = "url";
     public static final String USAGE_INTERFACE = "usageinterface";
     public static final String USER_DATA = "userdata";
+
+    public static final String USER_DATA_NAME = "userdataname";
+    public static final String USER_DATA_ID = "userdataid";
+    public static final String USER_DATA_POLICY = "userdatapolicy";
+    public static final String USER_DATA_DETAILS = "userdatadetails";
+    public static final String USER_DATA_PARAMS = "userdataparams";
     public static final String USER_FILTER = "userfilter";
     public static final String USER_ID = "userid";
     public static final String USER_SOURCE = "usersource";
diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java
index 6e149b6..d718eca 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java
@@ -23,7 +23,6 @@
 import java.util.Set;
 
 import com.cloud.server.ResourceIcon;
-import com.cloud.utils.Pair;
 import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
 import org.apache.cloudstack.api.response.ResourceIconResponse;
 import org.apache.cloudstack.api.response.DirectDownloadCertificateHostStatusResponse;
@@ -33,6 +32,7 @@
 import org.apache.cloudstack.direct.download.DirectDownloadCertificate;
 import org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMap;
 import org.apache.cloudstack.direct.download.DirectDownloadManager;
+import org.apache.cloudstack.api.response.UserDataResponse;
 import org.apache.cloudstack.management.ManagementServerHost;
 import org.apache.cloudstack.affinity.AffinityGroup;
 import org.apache.cloudstack.affinity.AffinityGroupResponse;
@@ -211,8 +211,10 @@
 import com.cloud.user.SSHKeyPair;
 import com.cloud.user.User;
 import com.cloud.user.UserAccount;
+import com.cloud.user.UserData;
 import com.cloud.uservm.UserVm;
 import com.cloud.utils.net.Ip;
+import com.cloud.utils.Pair;
 import com.cloud.vm.InstanceGroup;
 import com.cloud.vm.Nic;
 import com.cloud.vm.NicSecondaryIp;
@@ -483,6 +485,8 @@
 
     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 0000000..83e3481
--- /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 0000000..24b2f5b
--- /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 0000000..8ae5a10
--- /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 0000000..21d4e3b
--- /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<List<? extends UserData>, Integer> resultList = _mgr.listUserDatas(this);
+        List<UserDataResponse> responses = new ArrayList<>();
+        for (UserData result : resultList.first()) {
+            UserDataResponse r = _responseGenerator.createUserDataResponse(result);
+            r.setObjectName(ApiConstants.USER_DATA);
+            responses.add(r);
+        }
+
+        ListResponse<UserDataResponse> 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 0000000..bb2e0a3
--- /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<String> 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 fce7c91..c5cb996 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.SecurityGroupResponse;
 import org.apache.cloudstack.api.response.ServiceOfferingResponse;
 import org.apache.cloudstack.api.response.TemplateResponse;
+import org.apache.cloudstack.api.response.UserDataResponse;
 import org.apache.cloudstack.api.response.UserVmResponse;
 import org.apache.cloudstack.api.response.ZoneResponse;
 import org.apache.cloudstack.context.CallContext;
@@ -154,6 +155,13 @@
             length = 1048576)
     private String userData;
 
+    @Parameter(name = ApiConstants.USER_DATA_ID, type = CommandType.UUID, entityType = UserDataResponse.class, description = "the ID of the Userdata", since = "4.18")
+    private Long userdataId;
+
+    @Parameter(name = ApiConstants.USER_DATA_DETAILS, type = CommandType.MAP, description = "used to specify the parameters values for the variables in userdata.", since = "4.18")
+    private Map userdataDetails;
+
+    @Deprecated
     @Parameter(name = ApiConstants.SSH_KEYPAIR, type = CommandType.STRING, description = "name of the ssh key pair used to login to the virtual machine")
     private String sshKeyPairName;
 
@@ -413,6 +421,25 @@
         return userData;
     }
 
+    public Long getUserdataId() {
+        return userdataId;
+    }
+
+    public Map<String, String> getUserdataDetails() {
+        Map<String, String> userdataDetailsMap = new HashMap<String, String>();
+        if (userdataDetails != null && userdataDetails.size() != 0) {
+            Collection parameterCollection = userdataDetails.values();
+            Iterator iter = parameterCollection.iterator();
+            while (iter.hasNext()) {
+                HashMap<String, String> value = (HashMap<String, String>)iter.next();
+                for (Map.Entry<String,String> 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 0000000..ad0592c
--- /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<String, String> getUserdataDetails() {
+        Map<String, String> userdataDetailsMap = new HashMap<String, String>();
+        if (userdataDetails != null && userdataDetails.size() != 0) {
+            Collection parameterCollection = userdataDetails.values();
+            Iterator iter = parameterCollection.iterator();
+            while (iter.hasNext()) {
+                HashMap<String, String> value = (HashMap<String, String>)iter.next();
+                for (Map.Entry<String,String> 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 cc0d7af..3e0ef75 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 @@
 
 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.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 @@
                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 @@
         return userData;
     }
 
+    public Long getUserdataId() {
+        return userdataId;
+    }
+
+    public Map<String, String> getUserdataDetails() {
+        Map<String, String> userdataDetailsMap = new HashMap<String, String>();
+        if (userdataDetails != null && userdataDetails.size() != 0) {
+            Collection parameterCollection = userdataDetails.values();
+            Iterator iter = parameterCollection.iterator();
+            while (iter.hasNext()) {
+                HashMap<String, String> value = (HashMap<String, String>)iter.next();
+                for (Map.Entry<String,String> 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 892b5b8..bd09d09 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 @@
     @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 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 0000000..bbe27f8
--- /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 3483c17..17b577b 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java
@@ -332,6 +332,18 @@
     @Param(description = "Base64 string representation of the resource icon", since = "4.16.0.0")
     ResourceIconResponse resourceIconResponse;
 
+    @SerializedName(ApiConstants.USER_DATA_ID) @Param(description="the id of userdata used for the VM", since = "4.18.0")
+    private String userDataId;
+
+    @SerializedName(ApiConstants.USER_DATA_NAME) @Param(description="the name of userdata used for the VM", since = "4.18.0")
+    private String userDataName;
+
+    @SerializedName(ApiConstants.USER_DATA_POLICY) @Param(description="the userdata override policy with the userdata provided while deploying VM", since = "4.18.0")
+    private String userDataPolicy;
+
+    @SerializedName(ApiConstants.USER_DATA_DETAILS) @Param(description="list of variables and values for the variables declared in userdata", since = "4.18.0")
+    private String userDataDetails;
+
     public UserVmResponse() {
         securityGroupList = new LinkedHashSet<SecurityGroupResponse>();
         nics = new TreeSet<>(Comparator.comparingInt(x -> Integer.parseInt(x.getDeviceId())));
@@ -952,4 +964,37 @@
     public void setBytesSent(Long bytesSent) {
         this.bytesSent = bytesSent;
     }
+
+    public String getUserDataId() {
+        return userDataId;
+    }
+
+    public void setUserDataId(String userDataId) {
+        this.userDataId = userDataId;
+    }
+
+    public String getUserDataName() {
+        return userDataName;
+    }
+
+    public void setUserDataName(String userDataName) {
+        this.userDataName = userDataName;
+    }
+
+    public String getUserDataPolicy() {
+        return userDataPolicy;
+    }
+
+    public void setUserDataPolicy(String userDataPolicy) {
+        this.userDataPolicy = userDataPolicy;
+    }
+
+    public String getUserDataDetails() {
+        return userDataDetails;
+    }
+
+    public void setUserDataDetails(String userDataDetails) {
+        this.userDataDetails = userDataDetails;
+    }
+
 }
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/ResetVMUserDataCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/ResetVMUserDataCmdTest.java
new file mode 100644
index 0000000..e975138
--- /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<UserVmResponse> 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<String, String> values1 = new HashMap<>();
+        values1.put("key1", "value1");
+        values1.put("key2", "value2");
+
+        Map<String, String> values2 = new HashMap<>();
+        values1.put("key3", "value3");
+        values1.put("key4", "value4");
+
+        Map<Integer, Map<String, String>> userdataDetails = new HashMap<>();
+        userdataDetails.put(0, values1);
+        userdataDetails.put(1, values2);
+
+        ReflectionTestUtils.setField(cmd, "userdataDetails", userdataDetails);
+
+        Map<String, String> 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 0000000..fbaf46f
--- /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 0000000..c5581ed
--- /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 0000000..fd3b1a7
--- /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<UserData> userDataList = new ArrayList<UserData>();
+        userDataList.add(userData);
+        Pair<List<? extends UserData>, Integer> result = new Pair<List<? extends UserData>, 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<UserDataResponse> actualResponse = (ListResponse<UserDataResponse>)cmd.getResponseObject();
+        Assert.assertEquals(userDataResponse, actualResponse.getResponses().get(0));
+    }
+
+    @Test
+    public void testEmptyList() {
+        List<UserData> userDataList = new ArrayList<UserData>();
+        Pair<List<? extends UserData>, Integer> result = new Pair<List<? extends UserData>, Integer>(userDataList, 0);
+
+        Mockito.when(_mgr.listUserDatas(cmd)).thenReturn(result);
+
+        cmd.execute();
+
+        ListResponse<UserDataResponse> actualResponse = (ListResponse<UserDataResponse>)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 0000000..901ec84
--- /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 cc8e111..1d49e19 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 @@
     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 1136449..890197f 100755
--- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
+++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
@@ -3221,7 +3221,7 @@
                 if (_networkModel.isSharedNetworkWithoutServices(network.getId())) {
                     final String serviceOffering = _serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId()).getDisplayText();
                     boolean isWindows = _guestOSCategoryDao.findById(_guestOSDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows");
-                    vmData = _networkModel.generateVmData(userVm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(),
+                    vmData = _networkModel.generateVmData(userVm.getUserData(), userVm.getUserDataDetails(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(),
                             vm.getUuid(), defaultNic.getMacAddress(), userVm.getDetail("SSH.PublicKey"), (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows,
                             VirtualMachineManager.getHypervisorHostname(destination.getHost() != null ? destination.getHost().getName() : ""));
                     String vmName = vm.getInstanceName();
diff --git a/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java b/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java
index 61df40e..2aeb831 100644
--- a/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java
+++ b/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java
@@ -31,6 +31,8 @@
 import javax.persistence.TemporalType;
 import javax.persistence.Transient;
 
+import com.cloud.user.UserData;
+
 import com.cloud.hypervisor.Hypervisor.HypervisorType;
 import com.cloud.storage.Storage.ImageFormat;
 import com.cloud.storage.Storage.TemplateType;
@@ -155,6 +157,13 @@
     @Column(name = "deploy_as_is")
     private boolean deployAsIs;
 
+    @Column(name = "user_data_id")
+    private Long userDataId;
+
+    @Column(name = "user_data_link_policy")
+    @Enumerated(value = EnumType.STRING)
+    UserData.UserDataOverridePolicy userDataLinkPolicy;
+
     @Override
     public String getUniqueName() {
         return uniqueName;
@@ -648,4 +657,23 @@
     public 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 63221e7..b1d7f21 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 @@
     List<VMTemplateVO> listByParentTemplatetId(long parentTemplatetId);
 
     VMTemplateVO findLatestTemplateByName(String name);
+
+    List<VMTemplateVO> 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 74d210b..08b98f9 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 @@
     protected SearchBuilder<VMTemplateVO> ParentTemplateIdSearch;
     private SearchBuilder<VMTemplateVO> InactiveUnremovedTmpltSearch;
     private SearchBuilder<VMTemplateVO> LatestTemplateByHypervisorTypeSearch;
-
+    private SearchBuilder<VMTemplateVO> userDataSearch;
     @Inject
     ResourceTagDao _tagsDao;
 
@@ -422,6 +422,11 @@
         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,13 +634,21 @@
     }
 
     @Override
-     public List<VMTemplateVO> listUnRemovedTemplatesByStates(VirtualMachineTemplate.State ...states) {
+    public List<VMTemplateVO> listUnRemovedTemplatesByStates(VirtualMachineTemplate.State ...states) {
         SearchCriteria<VMTemplateVO> sc = InactiveUnremovedTmpltSearch.create();
         sc.setParameters("state", (Object[]) states);
         return listBy(sc);
     }
 
     @Override
+    public List<VMTemplateVO> findTemplatesLinkedToUserdata(long userdataId) {
+        SearchCriteria<VMTemplateVO> sc = userDataSearch.create();
+        sc.setParameters("userDataId", userdataId);
+        sc.setParameters("state", VirtualMachineTemplate.State.Active.toString());
+        return listBy(sc);
+    }
+
+    @Override
     @DB
     public boolean remove(Long id) {
         TransactionLegacy txn = TransactionLegacy.currentTxn();
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 0000000..f54b1a8
--- /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 0000000..f012d41
--- /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<UserDataVO, Long> {
+
+    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 0000000..416c441
--- /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<UserDataVO, Long> implements UserDataDao  {
+
+    private final SearchBuilder<UserDataVO> userdataSearch;
+    private final SearchBuilder<UserDataVO> 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<UserDataVO> 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<UserDataVO> 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 6733a87..6be995b 100644
--- a/engine/schema/src/main/java/com/cloud/vm/UserVmVO.java
+++ b/engine/schema/src/main/java/com/cloud/vm/UserVmVO.java
@@ -44,6 +44,12 @@
     @Basic(fetch = FetchType.LAZY)
     private String userData;
 
+    @Column(name = "user_data_id", nullable = true)
+    private Long userDataId = null;
+
+    @Column(name = "user_data_details", updatable = true, length = 4096)
+    private String userDataDetails;
+
     @Column(name = "display_name", updatable = true, nullable = true)
     private String displayName;
 
@@ -75,9 +81,11 @@
     }
 
     public UserVmVO(long id, String instanceName, String displayName, long templateId, HypervisorType hypervisorType, long guestOsId, boolean haEnabled,
-                    boolean limitCpuUse, long domainId, long accountId, long userId, long serviceOfferingId, String userData, String name, Long diskOfferingId) {
+                    boolean limitCpuUse, long domainId, long accountId, long userId, long serviceOfferingId, String userData, Long userDataId, String userDataDetails, String name, Long diskOfferingId) {
         super(id, serviceOfferingId, name, instanceName, Type.User, templateId, hypervisorType, guestOsId, domainId, accountId, userId, haEnabled, limitCpuUse, diskOfferingId);
         this.userData = userData;
+        this.userDataId = userDataId;
+        this.userDataDetails = userDataDetails;
         this.displayName = displayName;
         this.details = new HashMap<String, String>();
     }
@@ -101,6 +109,16 @@
     }
 
     @Override
+    public void setUserDataId(Long userDataId) {
+        this.userDataId = userDataId;
+    }
+
+    @Override
+    public Long getUserDataId() {
+        return userDataId;
+    }
+
+    @Override
     public String getDisplayName() {
         return displayName;
     }
@@ -147,4 +165,14 @@
     public 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 55d8fa0..2684908 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java
@@ -42,14 +42,17 @@
 
     /**
      * 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<UserVmVO> findDestroyedVms(Date date);
 
@@ -86,4 +89,7 @@
     List<UserVmVO> listByIsoId(Long isoId);
 
     List<Pair<Pair<String, VirtualMachine.Type>, Pair<Long, String>>> getVmsDetailByNames(Set<String> vmNames, String detail);
+
+    List<UserVmVO> 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 a903c33..7501bf4 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java
@@ -78,6 +78,8 @@
 
     protected SearchBuilder<UserVmVO> UserVmSearch;
     protected SearchBuilder<UserVmVO> UserVmByIsoSearch;
+
+    protected SearchBuilder<UserVmVO> listByUserdataId;
     protected Attribute _updateTimeAttr;
     // ResourceTagsDaoImpl _tagsDao = ComponentLocator.inject(ResourceTagsDaoImpl.class);
     @Inject
@@ -205,6 +207,10 @@
         UserVmByIsoSearch.and("isoId", UserVmByIsoSearch.entity().getIsoId(), SearchCriteria.Op.EQ);
         UserVmByIsoSearch.done();
 
+        listByUserdataId = createSearchBuilder();
+        listByUserdataId.and("userDataId", listByUserdataId.entity().getUserDataId(), SearchCriteria.Op.EQ);
+        listByUserdataId.done();
+
         _updateTimeAttr = _allAttributes.get("updateTime");
         assert _updateTimeAttr != null : "Couldn't get this updateTime attribute";
     }
@@ -228,13 +234,15 @@
     }
 
     @Override
-    public void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, boolean displayVm,
-            boolean isDynamicallyScalable, String customId, String hostName, String instanceName) {
+    public void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, Long userDataId, String userDataDetails, boolean displayVm,
+                         boolean isDynamicallyScalable, String customId, String hostName, String instanceName) {
         UserVmVO vo = createForUpdate();
         vo.setDisplayName(displayName);
         vo.setHaEnabled(enable);
         vo.setGuestOSId(osTypeId);
         vo.setUserData(userData);
+        vo.setUserDataId(userDataId);
+        vo.setUserDataDetails(userDataDetails);
         vo.setDisplayVm(displayVm);
         vo.setDynamicallyScalable(isDynamicallyScalable);
         if (hostName != null) {
@@ -697,4 +705,11 @@
 
         return vmsDetailByNames;
     }
+
+    @Override
+    public List<UserVmVO> findByUserDataId(long userdataId) {
+        SearchCriteria<UserVmVO> 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 0cbf364..860dcba 100644
--- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
+++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
@@ -180,6 +180,7 @@
   <bean id="resourceTagsDaoImpl" class="com.cloud.tags.dao.ResourceTagsDaoImpl" />
   <bean id="routerNetworkDaoImpl" class="com.cloud.network.dao.RouterNetworkDaoImpl" />
   <bean id="sSHKeyPairDaoImpl" class="com.cloud.user.dao.SSHKeyPairDaoImpl" />
+  <bean id="userDataDaoImpl" class="com.cloud.user.dao.UserDataDaoImpl" />
   <bean id="secondaryStorageVmDaoImpl" class="com.cloud.vm.dao.SecondaryStorageVmDaoImpl" />
   <bean id="securityGroupDaoImpl" class="com.cloud.network.security.dao.SecurityGroupDaoImpl" />
   <bean id="securityGroupJoinDaoImpl" class="com.cloud.api.query.dao.SecurityGroupJoinDaoImpl" />
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41600to41610.sql b/engine/schema/src/main/resources/META-INF/db/schema-41600to41610.sql
index c4d81eb..4d0f3f2 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41600to41610.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41600to41610.sql
@@ -399,3 +399,329 @@
   `amount` bigint unsigned NOT NULL,
   PRIMARY KEY (`id`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE `cloud`.`user_data` (
+  `id` bigint unsigned NOT NULL auto_increment COMMENT 'id',
+  `uuid` varchar(40) NOT NULL COMMENT 'UUID of the user data',
+  `name` varchar(256) NOT NULL COMMENT 'name of the user data',
+  `account_id` bigint unsigned NOT NULL COMMENT 'owner, foreign key to account table',
+  `domain_id` bigint unsigned NOT NULL COMMENT 'domain, foreign key to domain table',
+  `user_data` mediumtext COMMENT 'value of the userdata',
+  `params` mediumtext COMMENT 'value of the comma-separated list of parameters',
+  PRIMARY KEY (`id`),
+  CONSTRAINT `fk_userdata__account_id` FOREIGN KEY(`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `fk_userdata__domain_id` FOREIGN KEY(`domain_id`) REFERENCES `domain` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `uc_userdata__uuid` UNIQUE (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+ALTER TABLE `cloud`.`user_vm` ADD COLUMN `user_data_id` bigint unsigned DEFAULT NULL COMMENT 'id of the user data' AFTER `user_data`;
+ALTER TABLE `cloud`.`user_vm` ADD COLUMN `user_data_details` mediumtext DEFAULT NULL COMMENT 'value of the comma-separated list of parameters' AFTER `user_data_id`;
+ALTER TABLE `cloud`.`user_vm` ADD CONSTRAINT `fk_user_vm__user_data_id` FOREIGN KEY `fk_user_vm__user_data_id`(`user_data_id`) REFERENCES `user_data`(`id`);
+
+ALTER TABLE `cloud`.`vm_template` ADD COLUMN `user_data_id` bigint unsigned DEFAULT NULL COMMENT 'id of the user data';
+ALTER TABLE `cloud`.`vm_template` ADD COLUMN `user_data_link_policy` varchar(255) DEFAULT NULL COMMENT 'user data link policy with template';
+ALTER TABLE `cloud`.`vm_template` ADD CONSTRAINT `fk_vm_template__user_data_id` FOREIGN KEY `fk_vm_template__user_data_id`(`user_data_id`) REFERENCES `user_data`(`id`);
+
+-- Added userdata details to template
+DROP VIEW IF EXISTS `cloud`.`template_view`;
+CREATE VIEW `cloud`.`template_view` AS
+     SELECT
+         `vm_template`.`id` AS `id`,
+         `vm_template`.`uuid` AS `uuid`,
+         `vm_template`.`unique_name` AS `unique_name`,
+         `vm_template`.`name` AS `name`,
+         `vm_template`.`public` AS `public`,
+         `vm_template`.`featured` AS `featured`,
+         `vm_template`.`type` AS `type`,
+         `vm_template`.`hvm` AS `hvm`,
+         `vm_template`.`bits` AS `bits`,
+         `vm_template`.`url` AS `url`,
+         `vm_template`.`format` AS `format`,
+         `vm_template`.`created` AS `created`,
+         `vm_template`.`checksum` AS `checksum`,
+         `vm_template`.`display_text` AS `display_text`,
+         `vm_template`.`enable_password` AS `enable_password`,
+         `vm_template`.`dynamically_scalable` AS `dynamically_scalable`,
+         `vm_template`.`state` AS `template_state`,
+         `vm_template`.`guest_os_id` AS `guest_os_id`,
+         `guest_os`.`uuid` AS `guest_os_uuid`,
+         `guest_os`.`display_name` AS `guest_os_name`,
+         `vm_template`.`bootable` AS `bootable`,
+         `vm_template`.`prepopulate` AS `prepopulate`,
+         `vm_template`.`cross_zones` AS `cross_zones`,
+         `vm_template`.`hypervisor_type` AS `hypervisor_type`,
+         `vm_template`.`extractable` AS `extractable`,
+         `vm_template`.`template_tag` AS `template_tag`,
+         `vm_template`.`sort_key` AS `sort_key`,
+         `vm_template`.`removed` AS `removed`,
+         `vm_template`.`enable_sshkey` AS `enable_sshkey`,
+         `parent_template`.`id` AS `parent_template_id`,
+         `parent_template`.`uuid` AS `parent_template_uuid`,
+         `source_template`.`id` AS `source_template_id`,
+         `source_template`.`uuid` AS `source_template_uuid`,
+         `account`.`id` AS `account_id`,
+         `account`.`uuid` AS `account_uuid`,
+         `account`.`account_name` AS `account_name`,
+         `account`.`type` AS `account_type`,
+         `domain`.`id` AS `domain_id`,
+         `domain`.`uuid` AS `domain_uuid`,
+         `domain`.`name` AS `domain_name`,
+         `domain`.`path` AS `domain_path`,
+         `projects`.`id` AS `project_id`,
+         `projects`.`uuid` AS `project_uuid`,
+         `projects`.`name` AS `project_name`,
+         `data_center`.`id` AS `data_center_id`,
+         `data_center`.`uuid` AS `data_center_uuid`,
+         `data_center`.`name` AS `data_center_name`,
+         `launch_permission`.`account_id` AS `lp_account_id`,
+         `template_store_ref`.`store_id` AS `store_id`,
+         `image_store`.`scope` AS `store_scope`,
+         `template_store_ref`.`state` AS `state`,
+         `template_store_ref`.`download_state` AS `download_state`,
+         `template_store_ref`.`download_pct` AS `download_pct`,
+         `template_store_ref`.`error_str` AS `error_str`,
+         `template_store_ref`.`size` AS `size`,
+         `template_store_ref`.physical_size AS `physical_size`,
+         `template_store_ref`.`destroyed` AS `destroyed`,
+         `template_store_ref`.`created` AS `created_on_store`,
+         `vm_template_details`.`name` AS `detail_name`,
+         `vm_template_details`.`value` AS `detail_value`,
+         `resource_tags`.`id` AS `tag_id`,
+         `resource_tags`.`uuid` AS `tag_uuid`,
+         `resource_tags`.`key` AS `tag_key`,
+         `resource_tags`.`value` AS `tag_value`,
+         `resource_tags`.`domain_id` AS `tag_domain_id`,
+         `domain`.`uuid` AS `tag_domain_uuid`,
+         `domain`.`name` AS `tag_domain_name`,
+         `resource_tags`.`account_id` AS `tag_account_id`,
+         `account`.`account_name` AS `tag_account_name`,
+         `resource_tags`.`resource_id` AS `tag_resource_id`,
+         `resource_tags`.`resource_uuid` AS `tag_resource_uuid`,
+         `resource_tags`.`resource_type` AS `tag_resource_type`,
+         `resource_tags`.`customer` AS `tag_customer`,
+          CONCAT(`vm_template`.`id`,
+                 '_',
+                 IFNULL(`data_center`.`id`, 0)) AS `temp_zone_pair`,
+          `vm_template`.`direct_download` AS `direct_download`,
+          `vm_template`.`deploy_as_is` AS `deploy_as_is`,
+         `user_data`.`id` AS `user_data_id`,
+         `user_data`.`uuid` AS `user_data_uuid`,
+         `user_data`.`name` AS `user_data_name`,
+         `user_data`.`params` AS `user_data_params`,
+         `vm_template`.`user_data_link_policy` AS `user_data_policy`
+     FROM
+         (((((((((((((`vm_template`
+         JOIN `guest_os` ON ((`guest_os`.`id` = `vm_template`.`guest_os_id`)))
+         JOIN `account` ON ((`account`.`id` = `vm_template`.`account_id`)))
+         JOIN `domain` ON ((`domain`.`id` = `account`.`domain_id`)))
+         LEFT JOIN `projects` ON ((`projects`.`project_account_id` = `account`.`id`)))
+         LEFT JOIN `vm_template_details` ON ((`vm_template_details`.`template_id` = `vm_template`.`id`)))
+         LEFT JOIN `vm_template` `source_template` ON ((`source_template`.`id` = `vm_template`.`source_template_id`)))
+         LEFT JOIN `template_store_ref` ON (((`template_store_ref`.`template_id` = `vm_template`.`id`)
+             AND (`template_store_ref`.`store_role` = 'Image')
+             AND (`template_store_ref`.`destroyed` = 0))))
+         LEFT JOIN `vm_template` `parent_template` ON ((`parent_template`.`id` = `vm_template`.`parent_template_id`)))
+         LEFT JOIN `image_store` ON ((ISNULL(`image_store`.`removed`)
+             AND (`template_store_ref`.`store_id` IS NOT NULL)
+             AND (`image_store`.`id` = `template_store_ref`.`store_id`))))
+         LEFT JOIN `template_zone_ref` ON (((`template_zone_ref`.`template_id` = `vm_template`.`id`)
+             AND ISNULL(`template_store_ref`.`store_id`)
+             AND ISNULL(`template_zone_ref`.`removed`))))
+         LEFT JOIN `data_center` ON (((`image_store`.`data_center_id` = `data_center`.`id`)
+             OR (`template_zone_ref`.`zone_id` = `data_center`.`id`))))
+         LEFT JOIN `launch_permission` ON ((`launch_permission`.`template_id` = `vm_template`.`id`)))
+         LEFT JOIN `user_data` ON ((`user_data`.`id` = `vm_template`.`user_data_id`))
+         LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_template`.`id`)
+             AND ((`resource_tags`.`resource_type` = 'Template')
+             OR (`resource_tags`.`resource_type` = 'ISO')))));
+
+DROP VIEW IF EXISTS `cloud`.`user_vm_view`;
+CREATE
+    VIEW `user_vm_view` AS
+SELECT
+    `vm_instance`.`id` AS `id`,
+    `vm_instance`.`name` AS `name`,
+    `user_vm`.`display_name` AS `display_name`,
+    `user_vm`.`user_data` AS `user_data`,
+    `account`.`id` AS `account_id`,
+    `account`.`uuid` AS `account_uuid`,
+    `account`.`account_name` AS `account_name`,
+    `account`.`type` AS `account_type`,
+    `domain`.`id` AS `domain_id`,
+    `domain`.`uuid` AS `domain_uuid`,
+    `domain`.`name` AS `domain_name`,
+    `domain`.`path` AS `domain_path`,
+    `projects`.`id` AS `project_id`,
+    `projects`.`uuid` AS `project_uuid`,
+    `projects`.`name` AS `project_name`,
+    `instance_group`.`id` AS `instance_group_id`,
+    `instance_group`.`uuid` AS `instance_group_uuid`,
+    `instance_group`.`name` AS `instance_group_name`,
+    `vm_instance`.`uuid` AS `uuid`,
+    `vm_instance`.`user_id` AS `user_id`,
+    `vm_instance`.`last_host_id` AS `last_host_id`,
+    `vm_instance`.`vm_type` AS `type`,
+    `vm_instance`.`limit_cpu_use` AS `limit_cpu_use`,
+    `vm_instance`.`created` AS `created`,
+    `vm_instance`.`state` AS `state`,
+    `vm_instance`.`update_time` AS `update_time`,
+    `vm_instance`.`removed` AS `removed`,
+    `vm_instance`.`ha_enabled` AS `ha_enabled`,
+    `vm_instance`.`hypervisor_type` AS `hypervisor_type`,
+    `vm_instance`.`instance_name` AS `instance_name`,
+    `vm_instance`.`guest_os_id` AS `guest_os_id`,
+    `vm_instance`.`display_vm` AS `display_vm`,
+    `guest_os`.`uuid` AS `guest_os_uuid`,
+    `vm_instance`.`pod_id` AS `pod_id`,
+    `host_pod_ref`.`uuid` AS `pod_uuid`,
+    `vm_instance`.`private_ip_address` AS `private_ip_address`,
+    `vm_instance`.`private_mac_address` AS `private_mac_address`,
+    `vm_instance`.`vm_type` AS `vm_type`,
+    `data_center`.`id` AS `data_center_id`,
+    `data_center`.`uuid` AS `data_center_uuid`,
+    `data_center`.`name` AS `data_center_name`,
+    `data_center`.`is_security_group_enabled` AS `security_group_enabled`,
+    `data_center`.`networktype` AS `data_center_type`,
+    `host`.`id` AS `host_id`,
+    `host`.`uuid` AS `host_uuid`,
+    `host`.`name` AS `host_name`,
+    `host`.`cluster_id` AS `cluster_id`,
+    `vm_template`.`id` AS `template_id`,
+    `vm_template`.`uuid` AS `template_uuid`,
+    `vm_template`.`name` AS `template_name`,
+    `vm_template`.`display_text` AS `template_display_text`,
+    `vm_template`.`enable_password` AS `password_enabled`,
+    `iso`.`id` AS `iso_id`,
+    `iso`.`uuid` AS `iso_uuid`,
+    `iso`.`name` AS `iso_name`,
+    `iso`.`display_text` AS `iso_display_text`,
+    `service_offering`.`id` AS `service_offering_id`,
+    `svc_disk_offering`.`uuid` AS `service_offering_uuid`,
+    `disk_offering`.`uuid` AS `disk_offering_uuid`,
+    `disk_offering`.`id` AS `disk_offering_id`,
+    (CASE
+         WHEN ISNULL(`service_offering`.`cpu`) THEN `custom_cpu`.`value`
+         ELSE `service_offering`.`cpu`
+        END) AS `cpu`,
+    (CASE
+         WHEN ISNULL(`service_offering`.`speed`) THEN `custom_speed`.`value`
+         ELSE `service_offering`.`speed`
+        END) AS `speed`,
+    (CASE
+         WHEN ISNULL(`service_offering`.`ram_size`) THEN `custom_ram_size`.`value`
+         ELSE `service_offering`.`ram_size`
+        END) AS `ram_size`,
+    `backup_offering`.`uuid` AS `backup_offering_uuid`,
+    `backup_offering`.`id` AS `backup_offering_id`,
+    `svc_disk_offering`.`name` AS `service_offering_name`,
+    `disk_offering`.`name` AS `disk_offering_name`,
+    `backup_offering`.`name` AS `backup_offering_name`,
+    `storage_pool`.`id` AS `pool_id`,
+    `storage_pool`.`uuid` AS `pool_uuid`,
+    `storage_pool`.`pool_type` AS `pool_type`,
+    `volumes`.`id` AS `volume_id`,
+    `volumes`.`uuid` AS `volume_uuid`,
+    `volumes`.`device_id` AS `volume_device_id`,
+    `volumes`.`volume_type` AS `volume_type`,
+    `security_group`.`id` AS `security_group_id`,
+    `security_group`.`uuid` AS `security_group_uuid`,
+    `security_group`.`name` AS `security_group_name`,
+    `security_group`.`description` AS `security_group_description`,
+    `nics`.`id` AS `nic_id`,
+    `nics`.`uuid` AS `nic_uuid`,
+    `nics`.`device_id` AS `nic_device_id`,
+    `nics`.`network_id` AS `network_id`,
+    `nics`.`ip4_address` AS `ip_address`,
+    `nics`.`ip6_address` AS `ip6_address`,
+    `nics`.`ip6_gateway` AS `ip6_gateway`,
+    `nics`.`ip6_cidr` AS `ip6_cidr`,
+    `nics`.`default_nic` AS `is_default_nic`,
+    `nics`.`gateway` AS `gateway`,
+    `nics`.`netmask` AS `netmask`,
+    `nics`.`mac_address` AS `mac_address`,
+    `nics`.`broadcast_uri` AS `broadcast_uri`,
+    `nics`.`isolation_uri` AS `isolation_uri`,
+    `vpc`.`id` AS `vpc_id`,
+    `vpc`.`uuid` AS `vpc_uuid`,
+    `networks`.`uuid` AS `network_uuid`,
+    `networks`.`name` AS `network_name`,
+    `networks`.`traffic_type` AS `traffic_type`,
+    `networks`.`guest_type` AS `guest_type`,
+    `user_ip_address`.`id` AS `public_ip_id`,
+    `user_ip_address`.`uuid` AS `public_ip_uuid`,
+    `user_ip_address`.`public_ip_address` AS `public_ip_address`,
+    `ssh_keypairs`.`keypair_name` AS `keypair_name`,
+    `resource_tags`.`id` AS `tag_id`,
+    `resource_tags`.`uuid` AS `tag_uuid`,
+    `resource_tags`.`key` AS `tag_key`,
+    `resource_tags`.`value` AS `tag_value`,
+    `resource_tags`.`domain_id` AS `tag_domain_id`,
+    `domain`.`uuid` AS `tag_domain_uuid`,
+    `domain`.`name` AS `tag_domain_name`,
+    `resource_tags`.`account_id` AS `tag_account_id`,
+    `account`.`account_name` AS `tag_account_name`,
+    `resource_tags`.`resource_id` AS `tag_resource_id`,
+    `resource_tags`.`resource_uuid` AS `tag_resource_uuid`,
+    `resource_tags`.`resource_type` AS `tag_resource_type`,
+    `resource_tags`.`customer` AS `tag_customer`,
+    `async_job`.`id` AS `job_id`,
+    `async_job`.`uuid` AS `job_uuid`,
+    `async_job`.`job_status` AS `job_status`,
+    `async_job`.`account_id` AS `job_account_id`,
+    `affinity_group`.`id` AS `affinity_group_id`,
+    `affinity_group`.`uuid` AS `affinity_group_uuid`,
+    `affinity_group`.`name` AS `affinity_group_name`,
+    `affinity_group`.`description` AS `affinity_group_description`,
+    `vm_instance`.`dynamically_scalable` AS `dynamically_scalable`,
+    `user_data`.`id` AS `user_data_id`,
+    `user_data`.`uuid` AS `user_data_uuid`,
+    `user_data`.`name` AS `user_data_name`,
+    `user_vm`.`user_data_details` AS `user_data_details`,
+    `vm_template`.`user_data_link_policy` AS `user_data_policy`
+FROM
+    ((((((((((((((((((((((((((((((((((`user_vm`
+        JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`)
+            AND ISNULL(`vm_instance`.`removed`))))
+        JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`)))
+        JOIN `domain` ON ((`vm_instance`.`domain_id` = `domain`.`id`)))
+        LEFT JOIN `guest_os` ON ((`vm_instance`.`guest_os_id` = `guest_os`.`id`)))
+        LEFT JOIN `host_pod_ref` ON ((`vm_instance`.`pod_id` = `host_pod_ref`.`id`)))
+        LEFT JOIN `projects` ON ((`projects`.`project_account_id` = `account`.`id`)))
+        LEFT JOIN `instance_group_vm_map` ON ((`vm_instance`.`id` = `instance_group_vm_map`.`instance_id`)))
+        LEFT JOIN `instance_group` ON ((`instance_group_vm_map`.`group_id` = `instance_group`.`id`)))
+        LEFT JOIN `data_center` ON ((`vm_instance`.`data_center_id` = `data_center`.`id`)))
+        LEFT JOIN `host` ON ((`vm_instance`.`host_id` = `host`.`id`)))
+        LEFT JOIN `vm_template` ON ((`vm_instance`.`vm_template_id` = `vm_template`.`id`)))
+        LEFT JOIN `vm_template` `iso` ON ((`iso`.`id` = `user_vm`.`iso_id`)))
+        LEFT JOIN `volumes` ON ((`vm_instance`.`id` = `volumes`.`instance_id`)))
+        LEFT JOIN `service_offering` ON ((`vm_instance`.`service_offering_id` = `service_offering`.`id`)))
+        LEFT JOIN `disk_offering` `svc_disk_offering` ON ((`vm_instance`.`service_offering_id` = `svc_disk_offering`.`id`)))
+        LEFT JOIN `disk_offering` ON ((`volumes`.`disk_offering_id` = `disk_offering`.`id`)))
+        LEFT JOIN `backup_offering` ON ((`vm_instance`.`backup_offering_id` = `backup_offering`.`id`)))
+        LEFT JOIN `storage_pool` ON ((`volumes`.`pool_id` = `storage_pool`.`id`)))
+        LEFT JOIN `security_group_vm_map` ON ((`vm_instance`.`id` = `security_group_vm_map`.`instance_id`)))
+        LEFT JOIN `security_group` ON ((`security_group_vm_map`.`security_group_id` = `security_group`.`id`)))
+        LEFT JOIN `user_data` ON ((`user_data`.`id` = `user_vm`.`user_data_id`)))
+        LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`)
+            AND ISNULL(`nics`.`removed`))))
+        LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`)))
+        LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`)
+            AND ISNULL(`vpc`.`removed`))))
+        LEFT JOIN `user_ip_address` ON ((`user_ip_address`.`vm_id` = `vm_instance`.`id`)))
+        LEFT JOIN `user_vm_details` `ssh_details` ON (((`ssh_details`.`vm_id` = `vm_instance`.`id`)
+            AND (`ssh_details`.`name` = 'SSH.PublicKey'))))
+        LEFT JOIN `ssh_keypairs` ON (((`ssh_keypairs`.`public_key` = `ssh_details`.`value`)
+            AND (`ssh_keypairs`.`account_id` = `account`.`id`))))
+        LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_instance`.`id`)
+            AND (`resource_tags`.`resource_type` = 'UserVm'))))
+        LEFT JOIN `async_job` ON (((`async_job`.`instance_id` = `vm_instance`.`id`)
+            AND (`async_job`.`instance_type` = 'VirtualMachine')
+            AND (`async_job`.`job_status` = 0))))
+        LEFT JOIN `affinity_group_vm_map` ON ((`vm_instance`.`id` = `affinity_group_vm_map`.`instance_id`)))
+        LEFT JOIN `affinity_group` ON ((`affinity_group_vm_map`.`affinity_group_id` = `affinity_group`.`id`)))
+        LEFT JOIN `user_vm_details` `custom_cpu` ON (((`custom_cpu`.`vm_id` = `vm_instance`.`id`)
+            AND (`custom_cpu`.`name` = 'CpuNumber'))))
+        LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`)
+            AND (`custom_speed`.`name` = 'CpuSpeed'))))
+        LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`)
+        AND (`custom_ram_size`.`name` = 'memory'))));
diff --git a/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java b/engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java
index 0675b43..f643a34 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.Path;
 import java.nio.file.Paths;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.collections.MapUtils;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.log4j.Logger;
@@ -91,7 +94,7 @@
      *  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<String[]> vmData, String isoFileName, String driveLabel) {
+    public static String buildConfigDrive(List<String[]> vmData, String isoFileName, String driveLabel, Map<String, String> customUserdataParams) {
         if (vmData == null) {
             throw new CloudRuntimeException("No VM metadata provided");
         }
@@ -105,7 +108,7 @@
             File openStackFolder = new File(tempDirName + ConfigDrive.openStackConfigDriveName);
 
             writeVendorAndNetworkEmptyJsonFile(openStackFolder);
-            writeVmMetadata(vmData, tempDirName, openStackFolder);
+            writeVmMetadata(vmData, tempDirName, openStackFolder, customUserdataParams);
 
             linkUserData(tempDirName);
 
@@ -187,10 +190,10 @@
     }
 
     /**
-     * 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<String[]> vmData, String tempDirName, File openStackFolder) {
-        JsonObject metaData = createJsonObjectWithVmData(vmData, tempDirName);
+    static void writeVmMetadata(List<String[]> vmData, String tempDirName, File openStackFolder, Map<String, String> customUserdataParams) {
+        JsonObject metaData = createJsonObjectWithVmData(vmData, tempDirName, customUserdataParams);
         writeFile(openStackFolder, "meta_data.json", metaData.toString());
     }
 
@@ -220,7 +223,7 @@
      *  <li> [2]: config data file content
      * </ul>
      */
-    static JsonObject createJsonObjectWithVmData(List<String[]> vmData, String tempDirName) {
+    static JsonObject createJsonObjectWithVmData(List<String[]> vmData, String tempDirName, Map<String, String> customUserdataParams) {
         JsonObject metaData = new JsonObject();
         for (String[] item : vmData) {
             String dataType = item[CONFIGDATA_DIR];
@@ -228,12 +231,12 @@
             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<String, String> customUserdataParams) {
         if (StringUtils.isBlank(dataType)) {
             return;
         }
@@ -258,6 +261,22 @@
 
         //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<String, String> customUserdataParams) {
+        if (!NetworkModel.METATDATA_DIR.equals(dataType)) {
+            return;
+        }
+        if (StringUtils.isEmpty(content)) {
+            return;
+        }
+        if (MapUtils.isNotEmpty(customUserdataParams)) {
+            Set<String> 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 50cab35..cefa368 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.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang.StringUtils;
@@ -124,7 +127,7 @@
 
     @Test(expected = CloudRuntimeException.class)
     public void buildConfigDriveTestNoVmData() {
-        ConfigDriveBuilder.buildConfigDrive(null, "teste", "C:");
+        ConfigDriveBuilder.buildConfigDrive(null, "teste", "C:", null);
     }
 
     @SuppressWarnings("unchecked")
@@ -140,9 +143,9 @@
 
         //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 @@
         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 @@
         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 @@
         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<String[]> 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 @@
         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<String[]> 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<String, String> 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<String, String> 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 8f857a2..57dd5cd 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 javax.inject.Inject;
 
+import com.cloud.user.UserData;
 import org.apache.log4j.Logger;
 
 import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore;
@@ -328,6 +329,16 @@
     }
 
     @Override
+    public Long getUserDataId() {
+        return imageVO.getUserDataId();
+    }
+
+    @Override
+    public UserData.UserDataOverridePolicy getUserDataOverridePolicy() {
+        return imageVO.getUserDataOverridePolicy();
+    }
+
+    @Override
     public DataTO getTO() {
         DataTO to = null;
         if (dataStore == 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 d1bce21..8f6414d 100644
--- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java
+++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java
@@ -656,7 +656,7 @@
         } else {
             long id = userVmDao.getNextInSequence(Long.class, "id");
             UserVmVO vmInstanceVO = new UserVmVO(id, vmInternalName, vmInternalName, templateId, HypervisorType.VMware, guestOsId, false, false, domainId, accountId, userId,
-                    serviceOfferingId, null, vmInternalName, null);
+                    serviceOfferingId, null, null, null, vmInternalName, null);
             vmInstanceVO.setDataCenterId(zoneId);
             return userVmDao.persist(vmInstanceVO);
         }
diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java
index 67fb093..dd88c6d 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java
@@ -379,7 +379,7 @@
         String base64UserData = Base64.encodeBase64String(k8sNodeConfig.getBytes(StringUtils.getPreferredCharset()));
         nodeVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner,
                 hostName, hostName, null, null, null,
-                Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, kubernetesCluster.getKeyPair(),
+                Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, kubernetesCluster.getKeyPair(),
                 null, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE);
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info(String.format("Created node VM : %s, %s in the Kubernetes cluster : %s", hostName, nodeVm.getUuid(), kubernetesCluster.getName()));
diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java
index f9d42f6..bee00ff 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java
@@ -216,7 +216,7 @@
         String base64UserData = Base64.encodeBase64String(k8sControlNodeConfig.getBytes(StringUtils.getPreferredCharset()));
         controlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner,
                 hostName, hostName, null, null, null,
-                Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, kubernetesCluster.getKeyPair(),
+                Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, kubernetesCluster.getKeyPair(),
                 requestedIps, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE);
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info(String.format("Created control VM ID: %s, %s in the Kubernetes cluster : %s", controlVm.getUuid(), hostName, kubernetesCluster.getName()));
@@ -276,7 +276,7 @@
         String base64UserData = Base64.encodeBase64String(k8sControlNodeConfig.getBytes(StringUtils.getPreferredCharset()));
         additionalControlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, clusterTemplate, networkIds, owner,
                 hostName, hostName, null, null, null,
-                Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, kubernetesCluster.getKeyPair(),
+                Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, kubernetesCluster.getKeyPair(),
                 null, addrs, null, null, null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE);
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info(String.format("Created control VM ID : %s, %s in the Kubernetes cluster : %s", additionalControlVm.getUuid(), hostName, kubernetesCluster.getName()));
diff --git a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ServiceVirtualMachine.java b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/ServiceVirtualMachine.java
index 676afc9..17384c2 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 @@
 public class ServiceVirtualMachine extends UserVmVO {
     public ServiceVirtualMachine(long id, String instanceName, String name, long templateId, long serviceOfferingId, HypervisorType hypervisorType, long guestOSId,
                                  long dataCenterId, long domainId, long accountId, long userId, boolean haEnabled) {
-        super(id, instanceName, name, templateId, hypervisorType, guestOSId, false, false, domainId, accountId, userId, serviceOfferingId, null, name, null);
+        super(id, instanceName, name, templateId, hypervisorType, guestOSId, false, false, domainId, accountId, userId, serviceOfferingId, null, null, null, name, null);
     }
 }
diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java
index 64a036a..631341e 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 @@
         long id = _userVmDao.getNextInSequence(Long.class, "id");
         UserVmVO vm =
             new UserVmVO(id, name, name, tmpl.getId(), HypervisorType.XenServer, tmpl.getGuestOSId(), false, false, _zone.getDomainId(), Account.ACCOUNT_ID_SYSTEM,
-                    1, small.getId(), null, name, null);
+                    1, small.getId(), null, null, null, name, null);
         vm.setState(com.cloud.vm.VirtualMachine.State.Running);
         vm.setHostId(_hostId);
         vm.setDataCenterId(network.getDataCenterId());
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index 7949340..8c58ab6 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -37,6 +37,7 @@
 import javax.inject.Inject;
 
 import com.cloud.utils.security.CertificateHelper;
+import com.cloud.user.UserData;
 import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.acl.ControlledEntity.ACLType;
 import org.apache.cloudstack.affinity.AffinityGroup;
@@ -149,6 +150,7 @@
 import org.apache.cloudstack.api.response.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;
@@ -4484,6 +4486,20 @@
     }
 
     @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 fe00ef0..6a856e7 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 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 @@
     private TemplateDeployAsIsDetailsDao templateDeployAsIsDetailsDao;
     @Inject
     private AnnotationDao annotationDao;
+    @Inject
+    private UserDataDao userDataDao;
 
     private final SearchBuilder<TemplateJoinVO> tmpltIdPairSearch;
 
@@ -290,6 +293,12 @@
             templateResponse.setChildTemplates(childTemplatesSet);
         }
 
+        if (template.getUserDataId() != null) {
+            templateResponse.setUserDataId(template.getUserDataUUid());
+            templateResponse.setUserDataName(template.getUserDataName());
+            templateResponse.setUserDataParams(template.getUserDataParams());
+            templateResponse.setUserDataPolicy(template.getUserDataPolicy());
+        }
         templateResponse.setObjectName("template");
         return templateResponse;
     }
@@ -338,6 +347,13 @@
             response.setDetails(details);
         }
 
+        if (result.getUserDataId() != null) {
+            response.setUserDataId(result.getUserDataUUid());
+            response.setUserDataName(result.getUserDataName());
+            response.setUserDataParams(result.getUserDataParams());
+            response.setUserDataPolicy(result.getUserDataPolicy());
+        }
+
         // update tag information
         long tag_id = result.getTagId();
         if (tag_id > 0) {
@@ -451,6 +467,13 @@
             isoResponse.setSize(isoSize);
         }
 
+        if (iso.getUserDataId() != null) {
+            isoResponse.setUserDataId(iso.getUserDataUUid());
+            isoResponse.setUserDataName(iso.getUserDataName());
+            isoResponse.setUserDataParams(iso.getUserDataParams());
+            isoResponse.setUserDataPolicy(iso.getUserDataPolicy());
+        }
+
         // update tag information
         long tag_id = iso.getTagId();
         if (tag_id > 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 03fdb54..28fb84d 100644
--- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
@@ -379,6 +379,13 @@
             userVmResponse.setDynamicallyScalable(userVm.isDynamicallyScalable());
         }
 
+        if (userVm.getUserDataId() != null) {
+            userVmResponse.setUserDataId(userVm.getUserDataUUid());
+            userVmResponse.setUserDataName(userVm.getUserDataName());
+            userVmResponse.setUserDataDetails(userVm.getUserDataDetails());
+            userVmResponse.setUserDataPolicy(userVm.getUserDataPolicy());
+        }
+
         addVmRxTxDataToResponse(userVm, userVmResponse);
 
         return userVmResponse;
diff --git a/server/src/main/java/com/cloud/api/query/vo/TemplateJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/TemplateJoinVO.java
index 91bb763..fe48cb6 100644
--- a/server/src/main/java/com/cloud/api/query/vo/TemplateJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/TemplateJoinVO.java
@@ -234,6 +234,21 @@
     @Column(name = "deploy_as_is")
     private boolean deployAsIs;
 
+    @Column(name = "user_data_id")
+    private Long userDataId;
+
+    @Column(name = "user_data_uuid")
+    private String userDataUuid;
+
+    @Column(name = "user_data_name")
+    private String userDataName;
+
+    @Column(name = "user_data_policy")
+    private String userDataPolicy;
+
+    @Column(name = "user_data_params")
+    private String userDataParams;
+
     public TemplateJoinVO() {
     }
 
@@ -505,4 +520,23 @@
         return parentTemplateUuid;
     }
 
+    public Long getUserDataId() {
+        return userDataId;
+    }
+
+    public String getUserDataUUid() {
+        return userDataUuid;
+    }
+
+    public String getUserDataName() {
+        return userDataName;
+    }
+
+    public String getUserDataPolicy() {
+        return userDataPolicy;
+    }
+
+    public String getUserDataParams() {
+        return userDataParams;
+    }
 }
diff --git a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
index 2ce7e6c..c3ad84f 100644
--- a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
@@ -377,6 +377,21 @@
     @Column(name = "affinity_group_description")
     private String affinityGroupDescription;
 
+    @Column(name = "user_data_id")
+    private Long userDataId;
+
+    @Column(name = "user_data_uuid")
+    private String userDataUuid;
+
+    @Column(name = "user_data_name")
+    private String userDataName;
+
+    @Column(name = "user_data_policy")
+    private String userDataPolicy;
+
+    @Column(name = "user_data_details")
+    private String userDataDetails;
+
     transient String password;
 
     @Transient
@@ -871,4 +886,24 @@
         return VirtualMachine.class;
     }
 
+    public Long getUserDataId() {
+        return userDataId;
+    }
+
+    public String getUserDataUUid() {
+        return userDataUuid;
+    }
+
+    public String getUserDataName() {
+        return userDataName;
+    }
+
+    public String getUserDataPolicy() {
+        return userDataPolicy;
+    }
+
+    public String getUserDataDetails() {
+        return userDataDetails;
+    }
+
 }
diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java
index 1c766a3..ccc437e 100644
--- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java
+++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java
@@ -2425,7 +2425,7 @@
     }
 
     @Override
-    public List<String[]> generateVmData(String userData, String serviceOffering, long datacenterId,
+    public List<String[]> generateVmData(String userData, String userDataDetails, String serviceOffering, long datacenterId,
                                          String vmName, String vmHostName, long vmId, String vmUuid,
                                          String guestIpAddress, String publicKey, String password, Boolean isWindows, String hostname) {
 
@@ -2448,6 +2448,8 @@
         vmData.add(new String[]{METATDATA_DIR, LOCAL_HOSTNAME_FILE, StringUtils.unicodeEscape(vmHostName)});
         vmData.add(new String[]{METATDATA_DIR, LOCAL_IPV4_FILE, guestIpAddress});
 
+        addUserDataDetailsToCommand(vmData, userDataDetails);
+
         String publicIpAddress = guestIpAddress;
         String publicHostName = StringUtils.unicodeEscape(vmHostName);
 
@@ -2516,6 +2518,20 @@
         return vmData;
     }
 
+    protected void addUserDataDetailsToCommand(List<String[]> 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 11ecfb5..3867773 100644
--- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java
@@ -1324,19 +1324,19 @@
             if (zone.getNetworkType() == NetworkType.Basic) {
                 vm = _userVmService.createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, null, owner, "autoScaleVm-" + asGroup.getId() + "-" +
                     getCurrentTimeStampString(),
-                    "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null,
+                    "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, null, null,
                     null, true, null, null, null, null, null, null, null, true);
             } else {
                 if (zone.isSecurityGroupEnabled()) {
                     vm = _userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, null, null,
                         owner, "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(),
-                        "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null,
+                        "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, null,
                         null, null, true, null, null, null, null, null, null, null, true);
 
                 } else {
                     vm = _userVmService.createAdvancedVirtualMachine(zone, serviceOffering, template, null, owner, "autoScaleVm-" + asGroup.getId() + "-" +
                         getCurrentTimeStampString(), "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(),
-                        null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, addrs, true, null, null, null, null, null, null, null, true, null);
+                        null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, null, null, addrs, true, null, null, null, null, null, null, null, true, null);
 
                 }
             }
diff --git a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java
index f0f0422..39e2d9b 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 @@
 
             if (userVm != null) {
                 final boolean isWindows = isWindows(userVm.getGuestOSId());
-                List<String[]> vmData = _networkModel.generateVmData(userVm.getUserData(), _serviceOfferingDao.findById(userVm.getServiceOfferingId()).getName(), userVm.getDataCenterId(), userVm.getInstanceName(), vm.getHostName(), vm.getId(),
+                List<String[]> 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 @@
 
         LOG.debug("Creating config drive ISO for vm: " + profile.getInstanceName() + " on host: " + hostId);
 
+        Map<String, String> 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 @@
 
         LOG.debug("Creating config drive ISO for vm: " + profile.getInstanceName());
 
+        Map<String, String> 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 @@
         return true;
     }
 
+    private Map<String, String> getVMCustomUserdataParamMap(long vmId) {
+        UserVmVO userVm = _userVmDao.findById(vmId);
+        String userDataDetails = userVm.getUserDataDetails();
+        Map<String,String> 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 @@
             } else {
                 destHostname = VirtualMachineManager.getHypervisorHostname(dest.getHost().getName());
             }
-            final List<String[]> vmData = _networkModel.generateVmData(vm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(),
+            final List<String[]> 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 4f809ec..d15263f 100644
--- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java
+++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java
@@ -210,7 +210,7 @@
 
             Host host = _hostDao.findById(vm.getHostId());
             String destHostname = VirtualMachineManager.getHypervisorHostname(host != null ? host.getName() : "");
-            VmDataCommand vmDataCommand = generateVmDataCommand(router, nic.getIPv4Address(), vm.getUserData(), serviceOffering, zoneName,
+            VmDataCommand vmDataCommand = generateVmDataCommand(router, nic.getIPv4Address(), vm.getUserData(), vm.getUserDataDetails(), serviceOffering, zoneName,
                     staticNatIp == null || staticNatIp.getState() != IpAddress.State.Allocated ? null : staticNatIp.getAddress().addr(), vm.getHostName(), vm.getInstanceName(),
                     vm.getId(), vm.getUuid(), publicKey, nic.getNetworkId(), destHostname);
 
@@ -1075,9 +1075,9 @@
         return setupCmd;
     }
 
-    private VmDataCommand generateVmDataCommand(final VirtualRouter router, final String vmPrivateIpAddress, final String userData, final String serviceOffering,
-            final String zoneName, final String publicIpAddress, final String vmName, final String vmInstanceName, final long vmId, final String vmUuid, final String publicKey,
-            final long guestNetworkId, String hostname) {
+    private VmDataCommand generateVmDataCommand(final VirtualRouter router, final String vmPrivateIpAddress, final String userData, String userDataDetails, final String serviceOffering,
+                                                final String zoneName, final String publicIpAddress, final String vmName, final String vmInstanceName, final long vmId, final String vmUuid, final String publicKey,
+                                                final long guestNetworkId, String hostname) {
         final VmDataCommand cmd = new VmDataCommand(vmPrivateIpAddress, vmName, _networkModel.getExecuteInSeqNtwkElmtCmd());
 
         cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, _routerControlHelper.getRouterControlIp(router.getId()));
@@ -1093,6 +1093,8 @@
         cmd.addVmData("metadata", "local-ipv4", vmPrivateIpAddress);
         cmd.addVmData("metadata", "local-hostname", StringUtils.unicodeEscape(vmName));
 
+        addUserDataDetailsToCommand(cmd, userDataDetails);
+
         Network network = _networkDao.findById(guestNetworkId);
         if (dcVo.getNetworkType() == NetworkType.Basic || network.getGuestType() == Network.GuestType.Shared) {
             cmd.addVmData("metadata", "public-ipv4", vmPrivateIpAddress);
@@ -1127,6 +1129,20 @@
         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 c5aa4ea..7a79773 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -16,7 +16,9 @@
 // under the License.
 package com.cloud.server;
 
+import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Field;
+import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -45,6 +47,14 @@
 import com.cloud.agent.api.Answer;
 import com.cloud.agent.api.proxy.AllowConsoleAccessCommand;
 import com.cloud.exception.AgentUnavailableException;
+import com.cloud.storage.VMTemplateVO;
+import com.cloud.storage.dao.VMTemplateDao;
+import com.cloud.user.UserData;
+import com.cloud.user.UserDataVO;
+import com.cloud.user.dao.UserDataDao;
+import com.cloud.utils.db.UUIDManager;
+import com.cloud.vm.dao.DomainRouterDao;
+import com.cloud.vm.dao.NicDao;
 import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.affinity.AffinityGroupProcessor;
 import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
@@ -52,6 +62,7 @@
 import org.apache.cloudstack.annotation.dao.AnnotationDao;
 import org.apache.cloudstack.api.ApiCommandResourceType;
 import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd;
 import org.apache.cloudstack.api.command.admin.account.DeleteAccountCmd;
 import org.apache.cloudstack.api.command.admin.account.DisableAccountCmd;
@@ -482,6 +493,10 @@
 import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd;
 import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd;
 import org.apache.cloudstack.api.command.user.template.UpdateTemplatePermissionsCmd;
+import org.apache.cloudstack.api.command.user.userdata.DeleteUserDataCmd;
+import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd;
+import org.apache.cloudstack.api.command.user.userdata.ListUserDataCmd;
+import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd;
 import org.apache.cloudstack.api.command.user.vm.AddIpToVmNicCmd;
 import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd;
 import org.apache.cloudstack.api.command.user.vm.DeployVMCmd;
@@ -494,6 +509,7 @@
 import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd;
 import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd;
 import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd;
+import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd;
 import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd;
 import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd;
 import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
@@ -760,6 +776,9 @@
 import com.cloud.vm.dao.UserVmDetailsDao;
 import com.cloud.vm.dao.VMInstanceDao;
 
+import static com.cloud.configuration.ConfigurationManagerImpl.VM_USERDATA_MAX_LENGTH;
+import static com.cloud.vm.UserVmManager.MAX_USER_DATA_LENGTH_BYTES;
+
 public class ManagementServerImpl extends ManagerBase implements ManagementServer, Configurable {
     public static final Logger s_logger = Logger.getLogger(ManagementServerImpl.class.getName());
 
@@ -768,6 +787,10 @@
     static final ConfigKey<Boolean> humanReadableSizes = new ConfigKey<Boolean>("Advanced", Boolean.class, "display.human.readable.sizes", "true", "Enables outputting human readable byte sizes to logs and usage records.", false, ConfigKey.Scope.Global);
     public static final ConfigKey<String> customCsIdentifier = new ConfigKey<String>("Advanced", String.class, "custom.cs.identifier", UUID.randomUUID().toString().split("-")[0].substring(4), "Custom identifier for the cloudstack installation", true, ConfigKey.Scope.Global);
 
+    private static final int MAX_HTTP_GET_LENGTH = 2 * MAX_USER_DATA_LENGTH_BYTES;
+    private static final int NUM_OF_2K_BLOCKS = 512;
+    private static final int MAX_HTTP_POST_LENGTH = NUM_OF_2K_BLOCKS * MAX_USER_DATA_LENGTH_BYTES;
+
     @Inject
     public AccountManager _accountMgr;
     @Inject
@@ -801,7 +824,7 @@
     @Inject
     private UserDao _userDao;
     @Inject
-    private UserVmDao _userVmDao;
+    protected UserVmDao _userVmDao;
     @Inject
     private ConfigurationDao _configDao;
     @Inject
@@ -899,9 +922,19 @@
     @Inject
     private VpcDao _vpcDao;
     @Inject
-    private AnnotationDao annotationDao;
-    @Inject
     private DomainVlanMapDao _domainVlanMapDao;
+    @Inject
+    private NicDao nicDao;
+    @Inject
+    DomainRouterDao routerDao;
+    @Inject
+    public UUIDManager uuidMgr;
+    @Inject
+    protected UserDataDao userDataDao;
+    @Inject
+    protected VMTemplateDao templateDao;
+    @Inject
+    protected AnnotationDao annotationDao;
 
     private LockControllerListener _lockControllerListener;
     private final ScheduledExecutorService _eventExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("EventChecker"));
@@ -3332,6 +3365,7 @@
         cmdList.add(RemoveNicFromVMCmd.class);
         cmdList.add(ResetVMPasswordCmd.class);
         cmdList.add(ResetVMSSHKeyCmd.class);
+        cmdList.add(ResetVMUserDataCmd.class);
         cmdList.add(RestoreVMCmd.class);
         cmdList.add(StartVMCmd.class);
         cmdList.add(StopVMCmd.class);
@@ -3544,6 +3578,12 @@
         cmdList.add(ChangeOutOfBandManagementPasswordCmd.class);
         cmdList.add(GetUserKeysCmd.class);
         cmdList.add(CreateConsoleEndpointCmd.class);
+
+        //user data APIs
+        cmdList.add(RegisterUserDataCmd.class);
+        cmdList.add(DeleteUserDataCmd.class);
+        cmdList.add(ListUserDataCmd.class);
+        cmdList.add(LinkUserDataToTemplateCmd.class);
         return cmdList;
     }
 
@@ -4255,6 +4295,180 @@
         return createAndSaveSSHKeyPair(name, fingerprint, publicKey, null, owner);
     }
 
+    @Override
+    public boolean deleteUserData(final DeleteUserDataCmd cmd) {
+        final Account caller = getCaller();
+        final String accountName = cmd.getAccountName();
+        final Long domainId = cmd.getDomainId();
+        final Long projectId = cmd.getProjectId();
+
+        Account owner = null;
+        try {
+            owner = _accountMgr.finalizeOwner(caller, accountName, domainId, projectId);
+        } catch (InvalidParameterValueException ex) {
+            if (caller.getType() == Account.ACCOUNT_TYPE_ADMIN && accountName != null && domainId != null) {
+                owner = _accountDao.findAccountIncludingRemoved(accountName, domainId);
+            }
+            if (owner == null) {
+                throw ex;
+            }
+        }
+
+        final UserDataVO userData = userDataDao.findById(cmd.getId());
+        if (userData == null) {
+            final InvalidParameterValueException ex = new InvalidParameterValueException(
+                    "A UserData with id '" + cmd.getId() + "' does not exist for account " + owner.getAccountName() + " in specified domain id");
+            final DomainVO domain = ApiDBUtils.findDomainById(owner.getDomainId());
+            String domainUuid = String.valueOf(owner.getDomainId());
+            if (domain != null) {
+                domainUuid = domain.getUuid();
+            }
+            ex.addProxyObject(domainUuid, "domainId");
+            throw ex;
+        }
+
+        List<VMTemplateVO> 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<UserVmVO> 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<List<? extends UserData>, 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<Long> permittedAccounts = new ArrayList<Long>();
+
+        final Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<Long, Boolean, ListProjectResourcesCriteria>(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<UserDataVO> 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<UserDataVO> 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<List<UserDataVO>, 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
@@ -4314,6 +4528,15 @@
     }
 
     /**
+     * @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
      */
     protected Account getCaller() {
@@ -4336,6 +4559,20 @@
         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 dc7b262..203c917 100755
--- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
+++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
@@ -33,6 +33,7 @@
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+import com.cloud.user.UserData;
 import org.apache.cloudstack.acl.SecurityChecker.AccessType;
 import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.BaseListTemplateOrIsoPermissionsCmd;
@@ -54,6 +55,7 @@
 import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd;
 import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd;
 import org.apache.cloudstack.api.command.user.template.UpdateTemplatePermissionsCmd;
+import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd;
 import org.apache.cloudstack.api.response.GetUploadParamsResponse;
 import org.apache.cloudstack.context.CallContext;
 import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
@@ -2265,4 +2267,41 @@
         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 cde2d04..9824639 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 @@
     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<Long> securityGroupIdList, Map<String, Map<Integer, String>> extraDhcpOptionsMap) throws ResourceUnavailableException, InsufficientCapacityException;
+                                Long userDataId, String userDataDetails, Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List<Long> securityGroupIdList, Map<String, Map<Integer, String>> 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 58e1d71..3bc93b3 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
@@ -76,6 +76,7 @@
 import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd;
 import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd;
 import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd;
+import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd;
 import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd;
 import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd;
 import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction;
@@ -323,8 +324,10 @@
 import com.cloud.user.dao.AccountDao;
 import com.cloud.user.dao.SSHKeyPairDao;
 import com.cloud.user.dao.UserDao;
+import com.cloud.user.dao.UserDataDao;
 import com.cloud.user.dao.UserStatisticsDao;
 import com.cloud.user.dao.VmDiskStatisticsDao;
+import com.cloud.user.UserData;
 import com.cloud.uservm.UserVm;
 import com.cloud.utils.DateUtil;
 import com.cloud.utils.Journal;
@@ -552,10 +555,14 @@
     private AnnotationDao annotationDao;
     @Inject
     protected CommandSetupHelper commandSetupHelper;
+    @Inject
+    private UserDataDao _userDataDao;
     @Autowired
     @Qualifier("networkHelper")
     protected NetworkHelper nwHelper;
     @Inject
+    private UserDataDao userDataDao;
+    @Inject
     ReservationDao reservationDao;
     @Inject
     ResourceLimitService resourceLimitService;
@@ -885,6 +892,49 @@
     }
 
     @Override
+    @ActionEvent(eventType = EventTypes.EVENT_VM_RESETUSERDATA, eventDescription = "resetting VM userdata", async = true)
+    public UserVm resetVMUserData(ResetVMUserDataCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException {
+
+        Account caller = CallContext.current().getCallingAccount();
+        Long vmId = cmd.getId();
+        UserVmVO userVm = _vmDao.findById(cmd.getId());
+
+        if (userVm == null) {
+            throw new InvalidParameterValueException("unable to find a virtual machine by id" + cmd.getId());
+        }
+        _accountMgr.checkAccess(caller, null, true, userVm);
+
+        VMTemplateVO template = _templateDao.findByIdIncludingRemoved(userVm.getTemplateId());
+
+        // Do parameters input validation
+
+        if (userVm.getState() != State.Stopped) {
+            s_logger.error("vm is not in the right state: " + vmId);
+            throw new InvalidParameterValueException(String.format("VM %s should be stopped to do UserData reset", userVm));
+        }
+
+        String userData = cmd.getUserData();
+        Long userDataId = cmd.getUserdataId();
+        String userDataDetails = null;
+        if (MapUtils.isNotEmpty(cmd.getUserdataDetails())) {
+            userDataDetails = cmd.getUserdataDetails().toString();
+        }
+        userData = finalizeUserData(userData, userDataId, template);
+        userData = validateUserData(userData, cmd.getHttpMethod());
+
+        userVm.setUserDataId(userDataId);
+        userVm.setUserData(userData);
+        userVm.setUserDataDetails(userDataDetails);
+        _vmDao.update(userVm.getId(), userVm);
+
+        updateUserData(userVm);
+
+        UserVmVO vm = _vmDao.findById(vmId);
+        _vmDao.loadDetails(vm);
+        return vm;
+    }
+
+    @Override
     @ActionEvent(eventType = EventTypes.EVENT_VM_RESETSSHKEY, eventDescription = "resetting Vm SSHKey", async = true)
     public UserVm resetVMSSHKey(ResetVMSSHKeyCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException {
 
@@ -2430,6 +2480,11 @@
                     return false;
                 }
 
+                if (vm.getUserDataId() != null) {
+                    vm.setUserDataId(null);
+                    _vmDao.update(vm.getId(), vm);
+                }
+
                 _vmDao.remove(vm.getId());
             }
 
@@ -2710,7 +2765,6 @@
         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<String,String> details = cmd.getDetails();
@@ -2719,12 +2773,19 @@
         String extraConfig = cmd.getExtraConfig();
 
         UserVmVO vmInstance = _vmDao.findById(cmd.getId());
+        VMTemplateVO template = _templateDao.findById(vmInstance.getTemplateId());
         if (MapUtils.isNotEmpty(details) || cmd.isCleanupDetails()) {
-            VMTemplateVO template = _templateDao.findById(vmInstance.getTemplateId());
             if (template != null && template.isDeployAsIs()) {
                 throw new CloudRuntimeException("Detail settings are read from OVA, it cannot be changed by API call.");
             }
         }
+        String userData = cmd.getUserData();
+        Long userDataId = cmd.getUserdataId();
+        String userDataDetails = null;
+        if (MapUtils.isNotEmpty(cmd.getUserdataDetails())) {
+            userDataDetails = cmd.getUserdataDetails().toString();
+        }
+        userData = finalizeUserData(userData, userDataId, template);
 
         long accountId = vmInstance.getAccountId();
 
@@ -2786,7 +2847,7 @@
                 }
             }
         }
-        return updateVirtualMachine(id, displayName, group, ha, isDisplayVm, osTypeId, userData, isDynamicallyScalable,
+        return updateVirtualMachine(id, displayName, group, ha, isDisplayVm, osTypeId, userData, userDataId, userDataDetails, isDynamicallyScalable,
                 cmd.getHttpMethod(), cmd.getCustomId(), hostName, cmd.getInstanceName(), securityGroupIdList, cmd.getDhcpOptionsMap());
     }
 
@@ -2884,7 +2945,7 @@
 
     @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<Long> securityGroupIdList, Map<String, Map<Integer, String>> extraDhcpOptionsMap)
+            Long userDataId, String userDataDetails, Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List<Long> securityGroupIdList, Map<String, Map<Integer, String>> extraDhcpOptionsMap)
                     throws ResourceUnavailableException, InsufficientCapacityException {
         UserVmVO vm = _vmDao.findById(id);
         if (vm == null) {
@@ -2931,6 +2992,14 @@
             userData = vm.getUserData();
         }
 
+        if (userDataId == null) {
+            userDataId = vm.getUserDataId();
+        }
+
+        if (userDataDetails == null) {
+            userDataDetails = vm.getUserDataDetails();
+        }
+
         if (osTypeId == null) {
             osTypeId = vm.getGuestOSId();
         }
@@ -3026,7 +3095,7 @@
                     .getUuid(), nic.getId(), extraDhcpOptionsMap);
         }
 
-        _vmDao.updateVM(id, displayName, ha, osTypeId, userData, isDisplayVmEnabled, isDynamicallyScalable, customId, hostName, instanceName);
+        _vmDao.updateVM(id, displayName, ha, osTypeId, userData, userDataId, userDataDetails, isDisplayVmEnabled, isDynamicallyScalable, customId, hostName, instanceName);
 
         if (updateUserdata) {
             updateUserData(vm);
@@ -3039,7 +3108,7 @@
         return _vmDao.findById(id);
     }
 
-    private void updateUserData(UserVm vm) throws ResourceUnavailableException, InsufficientCapacityException {
+    protected void updateUserData(UserVm vm) throws ResourceUnavailableException, InsufficientCapacityException {
         boolean result = updateUserDataInternal(vm);
         if (result) {
             s_logger.debug(String.format("User data successfully updated for vm id:  %s", vm.getId()));
@@ -3448,7 +3517,7 @@
     @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm", create = true)
     public UserVm createBasicSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List<Long> securityGroupIdList,
             Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod,
-            String userData, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List<Long> affinityGroupIdList,
+            String userData, Long userDataId, String userDataDetails, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List<Long> affinityGroupIdList,
             Map<String, String> customParametes, String customId, Map<String, Map<Integer, String>> dhcpOptionMap,
             Map<Long, DiskOffering> dataDiskTemplateToDiskOfferingMap, Map<String, String> userVmOVFProperties, boolean dynamicScalingEnabled) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException,
     StorageUnavailableException, ResourceAllocationException {
@@ -3498,7 +3567,7 @@
         }
 
         return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod,
-                userData, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParametes, customId, dhcpOptionMap,
+                userData, userDataId, userDataDetails, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParametes, customId, dhcpOptionMap,
                 dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null);
 
     }
@@ -3507,7 +3576,7 @@
     @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm", create = true)
     public UserVm createAdvancedSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List<Long> networkIdList,
             List<Long> securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor,
-            HTTPMethod httpmethod, String userData, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard,
+            HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard,
             List<Long> affinityGroupIdList, Map<String, String> customParameters, String customId, Map<String, Map<Integer, String>> dhcpOptionMap,
             Map<Long, DiskOffering> dataDiskTemplateToDiskOfferingMap, Map<String, String> userVmOVFProperties, boolean dynamicScalingEnabled) throws InsufficientCapacityException, ConcurrentOperationException,
     ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException {
@@ -3609,15 +3678,15 @@
         }
 
         return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod,
-                userData, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap,
+                userData, userDataId, userDataDetails, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap,
                 userVmOVFProperties, dynamicScalingEnabled, null);
     }
 
     @Override
     @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm", create = true)
     public UserVm createAdvancedVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List<Long> networkIdList, Account owner,
-            String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData,
-            String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayvm, String keyboard, List<Long> affinityGroupIdList,
+            String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, Long userDataId,
+            String userDataDetails, String sshKeyPair, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean displayvm, String keyboard, List<Long> affinityGroupIdList,
             Map<String, String> customParametrs, String customId, Map<String, Map<Integer, String>> dhcpOptionsMap, Map<Long, DiskOffering> dataDiskTemplateToDiskOfferingMap,
             Map<String, String> userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String type) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException,
     StorageUnavailableException, ResourceAllocationException {
@@ -3670,7 +3739,7 @@
         verifyExtraDhcpOptionsNetwork(dhcpOptionsMap, networkList);
 
         return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, null, group, httpmethod, userData,
-                sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayvm, keyboard, affinityGroupIdList, customParametrs, customId, dhcpOptionsMap,
+                userDataId, userDataDetails, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayvm, keyboard, affinityGroupIdList, customParametrs, customId, dhcpOptionsMap,
                 dataDiskTemplateToDiskOfferingMap, userVmOVFPropertiesMap, dynamicScalingEnabled, type);
     }
 
@@ -3787,7 +3856,7 @@
     @DB
     private UserVm createVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate tmplt, String hostName, String displayName, Account owner,
                                         Long diskOfferingId, Long diskSize, List<NetworkVO> networkList, List<Long> securityGroupIdList, String group, HTTPMethod httpmethod, String userData,
-                                        String sshKeyPair, HypervisorType hypervisor, Account caller, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard,
+                                        Long userDataId, String userDataDetails, String sshKeyPair, HypervisorType hypervisor, Account caller, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard,
                                         List<Long> affinityGroupIdList, Map<String, String> customParameters, String customId, Map<String, Map<Integer, String>> dhcpOptionMap,
                                         Map<Long, DiskOffering> datadiskTemplateToDiskOfferringMap,
                                         Map<String, String> userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String type) throws InsufficientCapacityException, ResourceUnavailableException,
@@ -3885,7 +3954,7 @@
             _volumeService.validateVolumeSizeInBytes(size);
             volumesSize += size;
         }
-        UserVm vm = getCheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, type, template, hypervisorType, accountId, offering, isIso, volumesSize);
+        UserVm vm = getCheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, type, template, hypervisorType, accountId, offering, isIso, volumesSize);
 
         _securityGroupMgr.addInstanceToGroups(vm.getId(), securityGroupIdList);
 
@@ -3897,13 +3966,13 @@
         return vm;
     }
 
-    private UserVm getCheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List<NetworkVO> networkList, List<Long> securityGroupIdList, String group, HTTPMethod httpmethod, String userData, String sshKeyPair, Account caller, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List<Long> affinityGroupIdList, Map<String, String> customParameters, String customId, Map<String, Map<Integer, String>> dhcpOptionMap, Map<Long, DiskOffering> datadiskTemplateToDiskOfferringMap, Map<String, String> userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, long volumesSize) throws ResourceAllocationException, StorageUnavailableException, InsufficientCapacityException {
+    private UserVm getCheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List<NetworkVO> networkList, List<Long> securityGroupIdList, String group, HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, String sshKeyPair, Account caller, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List<Long> affinityGroupIdList, Map<String, String> customParameters, String customId, Map<String, Map<Integer, String>> dhcpOptionMap, Map<Long, DiskOffering> datadiskTemplateToDiskOfferringMap, Map<String, String> userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, long volumesSize) throws ResourceAllocationException, StorageUnavailableException, InsufficientCapacityException {
         if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) {
             try (CheckedReservation vmReservation = new CheckedReservation(owner, ResourceType.user_vm, 1l, reservationDao, resourceLimitService);
                  CheckedReservation cpuReservation = new CheckedReservation(owner, ResourceType.cpu, Long.valueOf(offering.getCpu()), reservationDao, resourceLimitService);
                  CheckedReservation memReservation = new CheckedReservation(owner, ResourceType.memory, Long.valueOf(offering.getRamSize()), reservationDao, resourceLimitService);
                  ) {
-                return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, volumesSize);
+                return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, volumesSize);
             } catch (ResourceAllocationException | CloudRuntimeException  e) {
                 throw e;
             } catch (Exception e) {
@@ -3912,11 +3981,11 @@
             }
 
         } else {
-            return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, volumesSize);
+            return getUncheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPair, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, volumesSize);
         }
     }
 
-    private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List<NetworkVO> networkList, List<Long> securityGroupIdList, String group, HTTPMethod httpmethod, String userData, String sshKeyPair, Account caller, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List<Long> affinityGroupIdList, Map<String, String> customParameters, String customId, Map<String, Map<Integer, String>> dhcpOptionMap, Map<Long, DiskOffering> datadiskTemplateToDiskOfferringMap, Map<String, String> userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, long volumesSize) throws ResourceAllocationException, StorageUnavailableException, InsufficientCapacityException {
+    private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List<NetworkVO> networkList, List<Long> securityGroupIdList, String group, HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, String sshKeyPair, Account caller, Map<Long, IpAddresses> requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, List<Long> affinityGroupIdList, Map<String, String> customParameters, String customId, Map<String, Map<Integer, String>> dhcpOptionMap, Map<Long, DiskOffering> datadiskTemplateToDiskOfferringMap, Map<String, String> userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, long volumesSize) throws ResourceAllocationException, StorageUnavailableException, InsufficientCapacityException {
         try (CheckedReservation volumeReservation = new CheckedReservation(owner, ResourceType.volume, (isIso || diskOfferingId == null ? 1l : 2), reservationDao, resourceLimitService);
              CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, volumesSize, reservationDao, resourceLimitService)) {
 
@@ -4196,7 +4265,7 @@
 
             dynamicScalingEnabled = dynamicScalingEnabled && checkIfDynamicScalingCanBeEnabled(null, offering, template, zone.getId());
 
-            UserVm vm = commitUserVm(zone, template, hostName, displayName, owner, diskOfferingId, diskSize, userData, caller, isDisplayVm, keyboard, accountId, userId, offering,
+            UserVm vm = commitUserVm(zone, template, hostName, displayName, owner, diskOfferingId, diskSize, userData, userDataId, userDataDetails, caller, isDisplayVm, keyboard, accountId, userId, offering,
                     isIso, sshPublicKey, networkNicMap, id, instanceName, uuidName, hypervisorType, customParameters, dhcpOptionMap,
                     datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType);
             // Assign instance to the group
@@ -4308,7 +4377,7 @@
     }
 
     private UserVmVO commitUserVm(final boolean isImport, final DataCenter zone, final Host host, final Host lastHost, final VirtualMachineTemplate template, final String hostName, final String displayName, final Account owner,
-                                  final Long diskOfferingId, final Long diskSize, final String userData, final Account caller, final Boolean isDisplayVm, final String keyboard,
+                                  final Long diskOfferingId, final Long diskSize, final String userData, Long userDataId, String userDataDetails, final Account caller, final Boolean isDisplayVm, final String keyboard,
                                   final long accountId, final long userId, final ServiceOffering offering, final boolean isIso, final String sshPublicKey, final LinkedHashMap<String, List<NicProfile>> networkNicMap,
                                   final long id, final String instanceName, final String uuidName, final HypervisorType hypervisorType, final Map<String, String> customParameters,
                                   final Map<String, Map<Integer, String>> extraDhcpOptionMap, final Map<Long, DiskOffering> dataDiskTemplateToDiskOfferingMap,
@@ -4317,7 +4386,7 @@
             @Override
             public UserVmVO doInTransaction(TransactionStatus status) throws InsufficientCapacityException {
                 UserVmVO vm = new UserVmVO(id, instanceName, displayName, template.getId(), hypervisorType, template.getGuestOSId(), offering.isOfferHA(),
-                        offering.getLimitCpuUse(), owner.getDomainId(), owner.getId(), userId, offering.getId(), userData, hostName, diskOfferingId);
+                        offering.getLimitCpuUse(), owner.getDomainId(), owner.getId(), userId, offering.getId(), userData, userDataId, userDataDetails, hostName, diskOfferingId);
                 vm.setUuid(uuidName);
                 vm.setDynamicallyScalable(dynamicScalingEnabled);
 
@@ -4503,13 +4572,13 @@
     }
 
     private UserVmVO commitUserVm(final DataCenter zone, final VirtualMachineTemplate template, final String hostName, final String displayName, final Account owner,
-            final Long diskOfferingId, final Long diskSize, final String userData, final Account caller, final Boolean isDisplayVm, final String keyboard,
+            final Long diskOfferingId, final Long diskSize, final String userData, Long userDataId, String userDataDetails, final Account caller, final Boolean isDisplayVm, final String keyboard,
             final long accountId, final long userId, final ServiceOfferingVO offering, final boolean isIso, final String sshPublicKey, final LinkedHashMap<String, List<NicProfile>> networkNicMap,
             final long id, final String instanceName, final String uuidName, final HypervisorType hypervisorType, final Map<String, String> customParameters, final Map<String,
             Map<Integer, String>> extraDhcpOptionMap, final Map<Long, DiskOffering> dataDiskTemplateToDiskOfferingMap,
             Map<String, String> userVmOVFPropertiesMap, final boolean dynamicScalingEnabled, String type) throws InsufficientCapacityException {
         return commitUserVm(false, zone, null, null, template, hostName, displayName, owner,
-                diskOfferingId, diskSize, userData, caller, isDisplayVm, keyboard,
+                diskOfferingId, diskSize, userData, userDataId, userDataDetails, caller, isDisplayVm, keyboard,
                 accountId, userId, offering, isIso, sshPublicKey, networkNicMap,
                 id, instanceName, uuidName, hypervisorType, customParameters,
                 extraDhcpOptionMap, dataDiskTemplateToDiskOfferingMap,
@@ -4880,7 +4949,7 @@
                 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<String[]> vmData = _networkModel.generateVmData(vm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(),
+                List<String[]> vmData = _networkModel.generateVmData(vm.getUserData(), vm.getUserDataDetails(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(),
                         vm.getUuid(), defaultNic.getIPv4Address(), vm.getDetail(VmDetailConstants.SSH_PUBLIC_KEY), (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows, destHostname);
                 String vmName = vm.getInstanceName();
                 String configDriveIsoRootFolder = "/tmp";
@@ -5629,6 +5698,73 @@
         return userVm.getHypervisorType();
     }
 
+    protected String finalizeUserData(String userData, Long userDataId, VirtualMachineTemplate template) {
+        if (StringUtils.isEmpty(userData) && userDataId == null && (template == null || template.getUserDataId() == null)) {
+            return null;
+        }
+
+        if (userDataId != null && StringUtils.isNotEmpty(userData)) {
+            throw new InvalidParameterValueException("Both userdata and userdata ID inputs are not allowed, please provide only one");
+        }
+        if (template != null && template.getUserDataId() != null) {
+            switch (template.getUserDataOverridePolicy()) {
+                case DENYOVERRIDE:
+                    if (StringUtils.isNotEmpty(userData) || userDataId != null) {
+                        String msg = String.format("UserData input is not allowed here since template %s is configured to deny any userdata", template.getName());
+                        throw new CloudRuntimeException(msg);
+                    }
+                case ALLOWOVERRIDE:
+                    if (userDataId != null) {
+                        UserData apiUserDataVO = userDataDao.findById(userDataId);
+                        return apiUserDataVO.getUserData();
+                    } else if (StringUtils.isNotEmpty(userData)) {
+                        return userData;
+                    } else {
+                        UserData templateUserDataVO = userDataDao.findById(template.getUserDataId());
+                        if (templateUserDataVO == null) {
+                            String msg = String.format("UserData linked to the template %s is not found", template.getName());
+                            throw new CloudRuntimeException(msg);
+                        }
+                        return templateUserDataVO.getUserData();
+                    }
+                case APPEND:
+                    UserData templateUserDataVO = userDataDao.findById(template.getUserDataId());
+                    if (templateUserDataVO == null) {
+                        String msg = String.format("UserData linked to the template %s is not found", template.getName());
+                        throw new CloudRuntimeException(msg);
+                    }
+                    if (userDataId != null) {
+                        UserData apiUserDataVO = userDataDao.findById(userDataId);
+                        return doConcateUserDatas(templateUserDataVO.getUserData(), apiUserDataVO.getUserData());
+                    } else if (StringUtils.isNotEmpty(userData)) {
+                        return doConcateUserDatas(templateUserDataVO.getUserData(), userData);
+                    } else {
+                        return templateUserDataVO.getUserData();
+                    }
+                default:
+                    String msg = String.format("This userdataPolicy %s is not supported for use with this feature", template.getUserDataOverridePolicy().toString());
+                    throw new CloudRuntimeException(msg);            }
+        } else {
+            if (userDataId != null) {
+                UserData apiUserDataVO = userDataDao.findById(userDataId);
+                return apiUserDataVO.getUserData();
+            } else if (StringUtils.isNotEmpty(userData)) {
+                return userData;
+            }
+        }
+        return null;
+    }
+
+    private String doConcateUserDatas(String userdata1, String userdata2) {
+        byte[] userdata1Bytes = Base64.decodeBase64(userdata1.getBytes());
+        byte[] userdata2Bytes = Base64.decodeBase64(userdata2.getBytes());
+        byte[] finalUserDataBytes = new byte[userdata1Bytes.length + userdata2Bytes.length];
+        System.arraycopy(userdata1Bytes, 0, finalUserDataBytes, 0, userdata1Bytes.length);
+        System.arraycopy(userdata2Bytes, 0, finalUserDataBytes, userdata1Bytes.length, userdata2Bytes.length);
+
+        return Base64.encodeBase64String(finalUserDataBytes);
+    }
+
     @Override
     public UserVm createVirtualMachine(DeployVMCmd cmd) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException,
     StorageUnavailableException, ResourceAllocationException {
@@ -5710,6 +5846,14 @@
             networkIds = new ArrayList<>(userVmNetworkMap.values());
         }
 
+        String userData = cmd.getUserData();
+        Long userDataId = cmd.getUserdataId();
+        String userDataDetails = null;
+        if (MapUtils.isNotEmpty(cmd.getUserdataDetails())) {
+            userDataDetails = cmd.getUserdataDetails().toString();
+        }
+        userData = finalizeUserData(userData, userDataId, template);
+
         Account caller = CallContext.current().getCallingAccount();
         Long callerId = caller.getId();
 
@@ -5727,7 +5871,6 @@
         IpAddresses addrs = new IpAddresses(ipAddress, ip6Address, macAddress);
         Long size = cmd.getSize();
         String group = cmd.getGroup();
-        String userData = cmd.getUserData();
         String sshKeyPairName = cmd.getSSHKeyPairName();
         Boolean displayVm = cmd.isDisplayVm();
         String keyboard = cmd.getKeyboard();
@@ -5738,14 +5881,14 @@
                 throw new InvalidParameterValueException("Can't specify network Ids in Basic zone");
             } else {
                 vm = createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, getSecurityGroupIdList(cmd), owner, name, displayName, diskOfferingId,
-                        size , group , cmd.getHypervisor(), cmd.getHttpMethod(), userData , sshKeyPairName , cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(),
+                        size , group , cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairName , cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(),
                         cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(),
                         dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled);
             }
         } else {
             if (zone.isSecurityGroupEnabled())  {
                 vm = createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, networkIds, getSecurityGroupIdList(cmd), owner, name,
-                        displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard,
+                        displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard,
                         cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(),
                         dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled);
 
@@ -5754,7 +5897,7 @@
                     throw new InvalidParameterValueException("Can't create vm with security groups; security group feature is not enabled per zone");
                 }
                 vm = createAdvancedVirtualMachine(zone, serviceOffering, template, networkIds, owner, name, displayName, diskOfferingId, size, group,
-                        cmd.getHypervisor(), cmd.getHttpMethod(), userData, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(),
+                        cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(),
                         cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null);
             }
         }
@@ -7900,7 +8043,7 @@
         final Host lastHost = powerState != VirtualMachine.PowerState.PowerOn ? host : null;
         final Boolean dynamicScalingEnabled = checkIfDynamicScalingCanBeEnabled(null, serviceOffering, template, zone.getId());
         return commitUserVm(true, zone, host, lastHost, template, hostName, displayName, owner,
-                null, null, userData, caller, isDisplayVm, keyboard,
+                null, null, userData, null, null, caller, isDisplayVm, keyboard,
                 accountId, userId, serviceOffering, template.getFormat().equals(ImageFormat.ISO), sshPublicKey, null,
                 id, instanceName, uuidName, hypervisorType, customParameters,
                 null, null, null, powerState, dynamicScalingEnabled, null);
diff --git a/server/src/main/java/org/apache/cloudstack/annotation/AnnotationManagerImpl.java b/server/src/main/java/org/apache/cloudstack/annotation/AnnotationManagerImpl.java
index ecbce89..c65b6d1 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 javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+import com.cloud.user.dao.UserDataDao;
 import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.acl.Role;
 import org.apache.cloudstack.acl.RoleService;
@@ -155,6 +156,8 @@
     @Inject
     private NetworkOfferingDao networkOfferingDao;
     @Inject
+    private UserDataDao userDataDao;
+    @Inject
     EntityManager entityManager;
 
     private static final List<RoleType> adminRoles = Collections.singletonList(RoleType.Admin);
@@ -168,6 +171,7 @@
         s_typeMap.put(EntityType.VM_SNAPSHOT, ApiCommandResourceType.VmSnapshot);
         s_typeMap.put(EntityType.INSTANCE_GROUP, ApiCommandResourceType.None);
         s_typeMap.put(EntityType.SSH_KEYPAIR, ApiCommandResourceType.None);
+        s_typeMap.put(EntityType.USER_DATA, ApiCommandResourceType.None);
         s_typeMap.put(EntityType.NETWORK, ApiCommandResourceType.Network);
         s_typeMap.put(EntityType.VPC, ApiCommandResourceType.Vpc);
         s_typeMap.put(EntityType.PUBLIC_IP_ADDRESS, ApiCommandResourceType.IpAddress);
@@ -510,6 +514,8 @@
                 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 6fd57fc..360d737 100644
--- a/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java
+++ b/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java
@@ -898,7 +898,7 @@
     }
 
     @Override
-    public List<String[]> 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<String[]> 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 06ec0b0..e93fcae 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 @@
         PowerMockito.when(CallContext.current()).thenReturn(callContextMock);
         Mockito.doReturn(Mockito.mock(Account.class)).when(callContextMock).getCallingAccount();
         Method method = ReflectionUtils.getMethods(ConfigDriveBuilder.class, ReflectionUtils.withName("buildConfigDrive")).iterator().next();
-        PowerMockito.when(ConfigDriveBuilder.class, method).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.anyString()).thenReturn("content");
+        PowerMockito.when(ConfigDriveBuilder.class, method).withArguments(Mockito.anyListOf(String[].class), Mockito.anyString(), Mockito.anyString(), Mockito.anyMap()).thenReturn("content");
 
         final HandleConfigDriveIsoAnswer answer = mock(HandleConfigDriveIsoAnswer.class);
         final UserVmDetailVO userVmDetailVO = mock(UserVmDetailVO.class);
diff --git a/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java b/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java
new file mode 100644
index 0000000..30e79ed
--- /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<String[]> 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<String[]> 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 488c5f5..247009f 100644
--- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java
+++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java
@@ -16,29 +16,67 @@
 // under the License.
 package com.cloud.server;
 
+import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.when;
 
+import com.cloud.storage.VMTemplateVO;
+import com.cloud.storage.dao.VMTemplateDao;
+import com.cloud.user.AccountManager;
+import com.cloud.user.User;
+import com.cloud.user.UserData;
+import com.cloud.user.UserDataVO;
+import com.cloud.user.dao.UserDataDao;
+import com.cloud.utils.Pair;
+import com.cloud.utils.db.Filter;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.UserVmVO;
+import com.cloud.vm.dao.UserVmDao;
+import org.apache.cloudstack.annotation.dao.AnnotationDao;
+import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.api.command.user.ssh.RegisterSSHKeyPairCmd;
+import org.apache.cloudstack.api.command.user.userdata.DeleteUserDataCmd;
+import org.apache.cloudstack.api.command.user.userdata.ListUserDataCmd;
+import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
-import org.mockito.runners.MockitoJUnitRunner;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.powermock.reflect.Whitebox;
 
 import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.network.IpAddressManagerImpl;
+import com.cloud.network.dao.IPAddressVO;
 import com.cloud.user.Account;
 import com.cloud.user.SSHKeyPair;
 import com.cloud.user.SSHKeyPairVO;
 import com.cloud.user.dao.SSHKeyPairDao;
+import com.cloud.utils.db.SearchCriteria;
 
-@RunWith(MockitoJUnitRunner.class)
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(CallContext.class)
 public class ManagementServerImplTest {
 
     @Mock
+    SearchCriteria<IPAddressVO> sc;
+
+    @Mock
     RegisterSSHKeyPairCmd regCmd;
 
     @Mock
@@ -49,13 +87,50 @@
 
     @Mock
     SSHKeyPairDao sshKeyPairDao;
-    ManagementServerImpl ms = new ManagementServerImpl();
 
     @Mock
     SSHKeyPair sshKeyPair;
 
+    @Mock
+    IpAddressManagerImpl ipAddressManagerImpl;
+
+    @Mock
+    AccountManager _accountMgr;
+
+    @Mock
+    UserDataDao _userDataDao;
+
+    @Mock
+    VMTemplateDao _templateDao;
+
+    @Mock
+    AnnotationDao annotationDao;
+
+    @Mock
+    UserVmDao _userVmDao;
+
     @Spy
-    ManagementServerImpl spy;
+    ManagementServerImpl spy = new ManagementServerImpl();
+
+    ConfigKey mockConfig;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        CallContext.register(Mockito.mock(User.class), Mockito.mock(Account.class));
+        mockConfig = Mockito.mock(ConfigKey.class);
+        Whitebox.setInternalState(ipAddressManagerImpl.getClass(), "SystemVmPublicIpReservationModeStrictness", mockConfig);
+        spy._accountMgr = _accountMgr;
+        spy.userDataDao = _userDataDao;
+        spy.templateDao = _templateDao;
+        spy._userVmDao = _userVmDao;
+        spy.annotationDao = annotationDao;
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        CallContext.unregister();
+    }
 
     @Test(expected = InvalidParameterValueException.class)
     public void testDuplicateRegistraitons(){
@@ -107,4 +182,264 @@
         spy.registerSSHKeyPair(regCmd);
         Mockito.verify(spy, Mockito.times(3)).getPublicKeyFromKeyKeyMaterial(anyString());
     }
+
+    @Test
+    public void testSuccessfulRegisterUserdata() {
+        PowerMockito.mockStatic(CallContext.class);
+        CallContext callContextMock = PowerMockito.mock(CallContext.class);
+        when(CallContext.current()).thenReturn(callContextMock);
+        when(account.getAccountId()).thenReturn(1L);
+        when(account.getDomainId()).thenReturn(2L);
+        when(callContextMock.getCallingAccount()).thenReturn(account);
+        when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account);
+
+        RegisterUserDataCmd cmd = Mockito.mock(RegisterUserDataCmd.class);
+        when(cmd.getUserData()).thenReturn("testUserdata");
+        when(cmd.getName()).thenReturn("testName");
+        when(cmd.getHttpMethod()).thenReturn(BaseCmd.HTTPMethod.GET);
+
+        when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(null);
+        when(_userDataDao.findByUserData(account.getAccountId(), account.getDomainId(), "testUserdata")).thenReturn(null);
+
+        UserData userData = spy.registerUserData(cmd);
+        Assert.assertEquals("testName", userData.getName());
+        Assert.assertEquals("testUserdata", userData.getUserData());
+        Assert.assertEquals(1L, userData.getAccountId());
+        Assert.assertEquals(2L, userData.getDomainId());
+    }
+
+    @Test(expected = InvalidParameterValueException.class)
+    public void testRegisterExistingUserdata() {
+        PowerMockito.mockStatic(CallContext.class);
+        CallContext callContextMock = PowerMockito.mock(CallContext.class);
+        when(CallContext.current()).thenReturn(callContextMock);
+        when(account.getAccountId()).thenReturn(1L);
+        when(account.getDomainId()).thenReturn(2L);
+        when(callContextMock.getCallingAccount()).thenReturn(account);
+        when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account);
+
+        RegisterUserDataCmd cmd = Mockito.mock(RegisterUserDataCmd.class);
+        when(cmd.getUserData()).thenReturn("testUserdata");
+        when(cmd.getName()).thenReturn("testName");
+
+        UserDataVO userData = Mockito.mock(UserDataVO.class);
+        when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(null);
+        when(_userDataDao.findByUserData(account.getAccountId(), account.getDomainId(), "testUserdata")).thenReturn(userData);
+
+        spy.registerUserData(cmd);
+    }
+
+    @Test(expected = InvalidParameterValueException.class)
+    public void testRegisterExistingName() {
+        PowerMockito.mockStatic(CallContext.class);
+        CallContext callContextMock = PowerMockito.mock(CallContext.class);
+        when(CallContext.current()).thenReturn(callContextMock);
+        when(account.getAccountId()).thenReturn(1L);
+        when(account.getDomainId()).thenReturn(2L);
+        PowerMockito.when(callContextMock.getCallingAccount()).thenReturn(account);
+        when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account);
+
+        RegisterUserDataCmd cmd = Mockito.mock(RegisterUserDataCmd.class);
+        when(cmd.getUserData()).thenReturn("testUserdata");
+        when(cmd.getName()).thenReturn("testName");
+
+        UserDataVO userData = Mockito.mock(UserDataVO.class);
+        when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(userData);
+
+        spy.registerUserData(cmd);
+    }
+
+    @Test
+    public void testSuccessfulDeleteUserdata() {
+        PowerMockito.mockStatic(CallContext.class);
+        CallContext callContextMock = PowerMockito.mock(CallContext.class);
+        when(CallContext.current()).thenReturn(callContextMock);
+        when(account.getAccountId()).thenReturn(1L);
+        when(account.getDomainId()).thenReturn(2L);
+        when(callContextMock.getCallingAccount()).thenReturn(account);
+        when(_accountMgr.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account);
+
+        DeleteUserDataCmd cmd = Mockito.mock(DeleteUserDataCmd.class);
+        when(cmd.getAccountName()).thenReturn("testAccountName");
+        when(cmd.getDomainId()).thenReturn(1L);
+        when(cmd.getProjectId()).thenReturn(2L);
+        when(cmd.getId()).thenReturn(1L);
+        UserDataVO userData = Mockito.mock(UserDataVO.class);
+
+        Mockito.when(userData.getId()).thenReturn(1L);
+        when(_userDataDao.findById(1L)).thenReturn(userData);
+        when(_userDataDao.findByName(account.getAccountId(), account.getDomainId(), "testName")).thenReturn(null);
+        when(_templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(new ArrayList<VMTemplateVO>());
+        when(_userVmDao.findByUserDataId(1L)).thenReturn(new ArrayList<UserVmVO>());
+        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<VMTemplateVO> 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<VMTemplateVO>());
+
+        UserVmVO userVmVO = Mockito.mock(UserVmVO.class);
+        List<UserVmVO> 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<UserDataVO> sb = Mockito.mock(SearchBuilder.class);
+        when(_userDataDao.createSearchBuilder()).thenReturn(sb);
+        when(sb.entity()).thenReturn(userData);
+
+        SearchCriteria<UserDataVO> sc = Mockito.mock(SearchCriteria.class);
+        when(sb.create()).thenReturn(sc);
+
+        List<UserDataVO> userDataList = new ArrayList<UserDataVO>();
+        userDataList.add(userData);
+        Pair<List<UserDataVO>, Integer> result = new Pair(userDataList, 1);
+        when(_userDataDao.searchAndCount(nullable(SearchCriteria.class), nullable(Filter.class))).thenReturn(result);
+
+        Pair<List<? extends UserData>, 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<UserDataVO> sb = Mockito.mock(SearchBuilder.class);
+        when(_userDataDao.createSearchBuilder()).thenReturn(sb);
+        when(sb.entity()).thenReturn(userData);
+
+        SearchCriteria<UserDataVO> sc = Mockito.mock(SearchCriteria.class);
+        when(sb.create()).thenReturn(sc);
+
+        List<UserDataVO> userDataList = new ArrayList<UserDataVO>();
+        userDataList.add(userData);
+        Pair<List<UserDataVO>, Integer> result = new Pair(userDataList, 1);
+        when(_userDataDao.searchAndCount(nullable(SearchCriteria.class), nullable(Filter.class))).thenReturn(result);
+
+        Pair<List<? extends UserData>, 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<UserDataVO> sb = Mockito.mock(SearchBuilder.class);
+        when(_userDataDao.createSearchBuilder()).thenReturn(sb);
+        when(sb.entity()).thenReturn(userData);
+
+        SearchCriteria<UserDataVO> sc = Mockito.mock(SearchCriteria.class);
+        when(sb.create()).thenReturn(sc);
+
+        List<UserDataVO> userDataList = new ArrayList<UserDataVO>();
+        userDataList.add(userData);
+        Pair<List<UserDataVO>, Integer> result = new Pair(userDataList, 1);
+        when(_userDataDao.searchAndCount(nullable(SearchCriteria.class), nullable(Filter.class))).thenReturn(result);
+
+        Pair<List<? extends UserData>, 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 b5f460e..81e2489 100644
--- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
+++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
@@ -216,7 +216,7 @@
             VolumeVO volumeOfRunningVm = new VolumeVO("root", 1L, 1L, 1L, 1L, 1L, "root", "root", Storage.ProvisioningType.THIN, 1, null, null, "root", Volume.Type.ROOT);
             when(volumeDaoMock.findById(1L)).thenReturn(volumeOfRunningVm);
 
-            UserVmVO runningVm = new UserVmVO(1L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null);
+            UserVmVO runningVm = new UserVmVO(1L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null);
             runningVm.setState(State.Running);
             runningVm.setDataCenterId(1L);
             when(userVmDaoMock.findById(1L)).thenReturn(runningVm);
@@ -226,13 +226,13 @@
             volumeOfStoppedVm.setPoolId(1L);
             when(volumeDaoMock.findById(2L)).thenReturn(volumeOfStoppedVm);
 
-            UserVmVO stoppedVm = new UserVmVO(2L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null);
+            UserVmVO stoppedVm = new UserVmVO(2L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null);
             stoppedVm.setState(State.Stopped);
             stoppedVm.setDataCenterId(1L);
             when(userVmDaoMock.findById(2L)).thenReturn(stoppedVm);
 
             // volume of hyperV vm id=3
-            UserVmVO hyperVVm = new UserVmVO(3L, "vm", "vm", 1, HypervisorType.Hyperv, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null);
+            UserVmVO hyperVVm = new UserVmVO(3L, "vm", "vm", 1, HypervisorType.Hyperv, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null);
             hyperVVm.setState(State.Stopped);
             hyperVVm.setDataCenterId(1L);
             when(userVmDaoMock.findById(3L)).thenReturn(hyperVVm);
@@ -289,7 +289,7 @@
             when(volumeDaoMock.findById(7L)).thenReturn(managedVolume1);
 
             // vm having root volume
-            UserVmVO vmHavingRootVolume = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null);
+            UserVmVO vmHavingRootVolume = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.XenServer, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null);
             vmHavingRootVolume.setState(State.Stopped);
             vmHavingRootVolume.setDataCenterId(1L);
             when(userVmDaoMock.findById(4L)).thenReturn(vmHavingRootVolume);
@@ -313,7 +313,7 @@
             upVolume.setState(Volume.State.Uploaded);
             when(volumeDaoMock.findById(8L)).thenReturn(upVolume);
 
-            UserVmVO kvmVm = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.KVM, 1L, false, false, 1L, 1L, 1, 1L, null, "vm", null);
+            UserVmVO kvmVm = new UserVmVO(4L, "vm", "vm", 1, HypervisorType.KVM, 1L, false, false, 1L, 1L, 1, 1L, null, null, null, "vm", null);
             kvmVm.setState(State.Running);
             kvmVm.setDataCenterId(1L);
             when(userVmDaoMock.findById(4L)).thenReturn(kvmVm);
diff --git a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java
index e6171bb..849718e 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.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.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.TemplateDataStoreVO;
 import org.apache.cloudstack.test.utils.SpringUtils;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
+import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
@@ -130,6 +134,7 @@
 import static org.mockito.Mockito.eq;
 
 @RunWith(SpringJUnit4ClassRunner.class)
+@PrepareForTest(CallContext.class)
 @ContextConfiguration(loader = AnnotationConfigContextLoader.class)
 public class TemplateManagerImplTest {
 
@@ -184,6 +189,9 @@
     @Inject
     HypervisorGuruManager _hvGuruMgr;
 
+    @Inject
+    AccountManager _accountMgr;
+
     public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
         AtomicInteger ai = new AtomicInteger(0);
         public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
@@ -504,6 +512,80 @@
         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 c9460c3..c0a1d38 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.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.List;
 import java.util.Map;
 
+import com.cloud.template.VirtualMachineTemplate;
+import com.cloud.user.UserData;
+import com.cloud.user.UserDataVO;
+import com.cloud.user.dao.UserDataDao;
+import com.cloud.utils.exception.CloudRuntimeException;
 import org.apache.cloudstack.api.BaseCmd.HTTPMethod;
+import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd;
 import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd;
 import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd;
 import org.apache.cloudstack.context.CallContext;
@@ -159,6 +167,9 @@
     @Mock
     VolumeApiService volumeApiService;
 
+    @Mock
+    UserDataDao userDataDao;
+
     private long vmId = 1l;
 
     private static final long GiB_TO_BYTES = 1024 * 1024 * 1024;
@@ -257,6 +268,7 @@
         Mockito.when(_serviceOfferingDao.findById(Mockito.anyLong(), Mockito.anyLong())).thenReturn((ServiceOfferingVO) offering);
         Mockito.when(userVmVoMock.isDisplay()).thenReturn(true);
         Mockito.doNothing().when(userVmManagerImpl).updateDisplayVmFlag(false, vmId, userVmVoMock);
+        Mockito.when(updateVmCommand.getUserdataId()).thenReturn(null);
         userVmManagerImpl.updateVirtualMachine(updateVmCommand);
         verifyMethodsThatAreAlwaysExecuted();
 
@@ -272,6 +284,7 @@
         Mockito.when(updateVmCommand.isCleanupDetails()).thenReturn(true);
         Mockito.lenient().doNothing().when(userVmManagerImpl).updateDisplayVmFlag(false, vmId, userVmVoMock);
         Mockito.doNothing().when(userVmDetailVO).removeDetails(vmId);
+        Mockito.when(updateVmCommand.getUserdataId()).thenReturn(null);
 
         userVmManagerImpl.updateVirtualMachine(updateVmCommand);
         verifyMethodsThatAreAlwaysExecuted();
@@ -322,6 +335,7 @@
         if(!isDetailsEmpty) {
             details.put("", "");
         }
+        Mockito.when(updateVmCommand.getUserdataId()).thenReturn(null);
         Mockito.when(updateVmCommand.getDetails()).thenReturn(details);
         Mockito.when(updateVmCommand.isCleanupDetails()).thenReturn(cleanUpDetails);
         configureDoNothingForDetailsMethod();
@@ -348,7 +362,7 @@
 
         Mockito.verify(userVmManagerImpl).updateVirtualMachine(nullable(Long.class), nullable(String.class), nullable(String.class), nullable(Boolean.class),
                 nullable(Boolean.class), nullable(Long.class),
-                nullable(String.class), nullable(Boolean.class), nullable(HTTPMethod.class), nullable(String.class), nullable(String.class), nullable(String.class), nullable(List.class),
+                nullable(String.class), nullable(Long.class), nullable(String.class), nullable(Boolean.class), nullable(HTTPMethod.class), nullable(String.class), nullable(String.class), nullable(String.class), nullable(List.class),
                 nullable(Map.class));
 
     }
@@ -359,7 +373,7 @@
         Mockito.doReturn(new ArrayList<Long>()).when(userVmManagerImpl).getSecurityGroupIdList(updateVmCommand);
         Mockito.lenient().doReturn(Mockito.mock(UserVm.class)).when(userVmManagerImpl).updateVirtualMachine(Mockito.anyLong(), Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean(),
                 Mockito.anyBoolean(), Mockito.anyLong(),
-                Mockito.anyString(), Mockito.anyBoolean(), Mockito.any(HTTPMethod.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyListOf(Long.class),
+                Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.any(HTTPMethod.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyListOf(Long.class),
                 Mockito.anyMap());
     }
 
@@ -579,4 +593,231 @@
         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 37cd6e5..c75f2c4 100644
--- a/server/src/test/java/com/cloud/vm/UserVmManagerTest.java
+++ b/server/src/test/java/com/cloud/vm/UserVmManagerTest.java
@@ -517,7 +517,7 @@
         AccountVO newAccount = new AccountVO("testaccount", 1, "networkdomain", (short)1, UUID.randomUUID().toString());
         newAccount.setId(2L);
 
-        UserVmVO vm = new UserVmVO(10L, "test", "test", 1L, HypervisorType.Any, 1L, false, false, 1L, 1L, 1, 5L, "test", "test", 1L);
+        UserVmVO vm = new UserVmVO(10L, "test", "test", 1L, HypervisorType.Any, 1L, false, false, 1L, 1L, 1, 5L, "test", null, null, "test", 1L);
         vm.setState(VirtualMachine.State.Stopped);
         when(_vmDao.findById(anyLong())).thenReturn(vm);
 
diff --git a/server/src/test/java/com/cloud/vm/dao/UserVmDaoImplTest.java b/server/src/test/java/com/cloud/vm/dao/UserVmDaoImplTest.java
index 49de99a..b9c9061 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 @@
         // Persist the data.
         UserVmVO vo =
             new UserVmVO(vmId, instanceName, displayName, templateId, hypervisor, guestOsId, haEnabled, limitCpuUse, domainId, accountId, 1, serviceOfferingId, userdata,
-                name, diskOfferingId);
+               null, null, name, diskOfferingId);
         dao.persist(vo);
 
         vo = dao.findById(vmId);
diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java
index a731f7c..1dd8df2 100644
--- a/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java
+++ b/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java
@@ -913,7 +913,7 @@
     }
 
     @Override
-    public List<String[]> 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<String[]> 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 0d36648..990e6f3 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 @@
     @Test(expected = InvalidParameterValueException.class)
     public void updateAffinityGroupVMRunning() throws ResourceInUseException {
 
-        UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", "test", 1L);
+        UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, domainId, 200L, 1, 5L, "", null, null, "test", 1L);
         vm.setState(VirtualMachine.State.Running);
         when(_vmDao.findById(10L)).thenReturn(vm);
 
diff --git a/server/src/test/java/org/apache/cloudstack/affinity/AffinityGroupServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/affinity/AffinityGroupServiceImplTest.java
index 8aa4aa8..c5caef8 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 @@
     @Test(expected = InvalidParameterValueException.class)
     public void updateAffinityGroupVMRunning() throws ResourceInUseException {
         when(_acctMgr.finalizeOwner((Account)anyObject(), anyString(), anyLong(), anyLong())).thenReturn(acct);
-        UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, DOMAIN_ID, 200L, 1, 5L, "", "test", 1L);
+        UserVmVO vm = new UserVmVO(10L, "test", "test", 101L, HypervisorType.Any, 21L, false, false, DOMAIN_ID, 200L, 1, 5L, "", null, null, "test", 1L);
         vm.setState(VirtualMachine.State.Running);
         when(_vmDao.findById(10L)).thenReturn(vm);
 
diff --git a/test/integration/smoke/test_register_userdata.py b/test/integration/smoke/test_register_userdata.py
new file mode 100644
index 0000000..8cf44c7
--- /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 9096a3b..73c83e0 100755
--- a/tools/marvin/marvin/lib/base.py
+++ b/tools/marvin/marvin/lib/base.py
@@ -522,7 +522,7 @@
                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 @@
         if "userdata" in services:
             cmd.userdata = base64.urlsafe_b64encode(services["userdata"].encode()).decode()
 
+        if userdataid is not None:
+            cmd.userdataid = userdataid
+
+        if userdatadetails is not None:
+            cmd.userdatadetails = userdatadetails
+
         if "dhcpoptionsnetworklist" in services:
             cmd.dhcpoptionsnetworklist = services["dhcpoptionsnetworklist"]
 
@@ -1622,6 +1628,18 @@
             cmd.listall = True
         return (apiclient.listTemplates(cmd))
 
+    @classmethod
+    def linkUserDataToTemplate(cls, apiclient, templateid, userdataid=None, userdatapolicy=None):
+        "Link userdata to template "
+
+        cmd = linkUserDataToTemplate.linkUserDataToTemplateCmd()
+        cmd.templateid = templateid
+        if userdataid is not None:
+            cmd.userdataid = userdataid
+        if userdatapolicy is not None:
+            cmd.userdatapolicy = userdatapolicy
+
+        return apiclient.linkUserDataToTemplate(cmd)
 
 class Iso:
     """Manage ISO life cycle"""
@@ -4907,6 +4925,45 @@
             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 db6f1e1..83ba35a 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -286,6 +286,7 @@
 "label.action.update.os.preference.processing": "Updating OS Preference....",
 "label.action.update.resource.count": "Update Resource Count",
 "label.action.update.resource.count.processing": "Updating Resource Count....",
+"label.action.userdata.reset": "Reset userdata",
 "label.action.vmsnapshot.create": "Take VM Snapshot",
 "label.action.vmsnapshot.delete": "Delete VM snapshot",
 "label.action.vmsnapshot.revert": "Revert to VM snapshot",
@@ -1814,6 +1815,7 @@
 "label.region": "Region",
 "label.region.details": "Region details",
 "label.register.template": "Register Template",
+"label.register.user.data": "Register a userdata",
 "label.reinstall.vm": "Reinstall VM",
 "label.reject": "Reject",
 "label.related": "Related",
@@ -1848,6 +1850,7 @@
 "label.remove.static.route": "Remove static route",
 "label.remove.this.physical.network": "Remove this physical network",
 "label.remove.tier": "Remove tier",
+"label.remove.user.data": "Remove userdata",
 "label.remove.vm.from.lb": "Remove VM from load balancer rule",
 "label.remove.vm.load.balancer": "Remove VM from load balancer",
 "label.remove.vmware.datacenter": "Remove VMware Datacenter",
@@ -1874,6 +1877,7 @@
 "label.reset.ssh.key.pair": "Reset SSH Key Pair",
 "label.reset.ssh.key.pair.on.vm": "Reset SSH Key Pair on VM",
 "label.reset.to.default": "Reset to default",
+"label.reset.userdata.on.vm": "Reset Userdata on VM",
 "label.reset.vpn.connection": "Reset VPN connection",
 "label.resetvm": "Reset VM",
 "label.resource": "Resource",
@@ -2075,6 +2079,17 @@
 "label.srx.firewall": "Juniper SRX Firewall",
 "label.ssh.key.pair.details": "SSH Key Pair Details",
 "label.ssh.key.pairs": "SSH Key Pairs",
+"label.userdataid": "Userdata ID",
+"label.userdataname": "Userdata name",
+"label.userdatadetails": "Userdata details",
+"label.userdataparams": "Userdata parameters",
+"label.userdatapolicy": "Userdata link policy",
+"label.userdata.text": "Userdata Text",
+"label.userdata.registered": "Userdata Registered",
+"label.userdata.do.override": "Userdata override",
+"label.userdata.do.append": "Userdata append",
+"label.userdatapolicy.tooltip": "Userdata linked to the template can be overridden by userdata provided during VM deploy. Select the override policy as required.",
+"label.user.data": "User Data",
 "label.ssh.port": "SSH Port",
 "label.sshkeypair": "New SSH Key Pair",
 "label.sshkeypairs": "SSH keypairs",
@@ -2839,6 +2854,8 @@
 "message.desc.primary.storage": "Each cluster must contain one or more primary storage servers, and we will add the first one now. Primary storage contains the disk volumes for all the VMs running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.",
 "message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this VM. Please note the root password will be changed by this operation if password is enabled.",
 "message.desc.secondary.storage": "Each zone must have at least one NFS or secondary storage server, and we will add the first one now. Secondary storage stores VM templates, ISO images, and VM disk volume snapshots. This server must be available to all hosts in the zone.<br/><br/>Provide the IP address and exported path.",
+"message.desc.register.user.data": "Please fill in the following data to register a user data.",
+"message.desc.registered.user.data": "Registered a User Data.",
 "message.desc.zone": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.",
 "message.detach.disk": "Are you sure you want to detach this disk?",
 "message.detach.iso.confirm": "Please confirm that you want to detach the ISO from this virtual instance.",
@@ -2990,6 +3007,7 @@
 "message.error.upload.template": "Template Upload Failed",
 "message.error.upload.template.description": "Only one template can be uploaded at a time",
 "message.error.url": "Please enter URL",
+"message.error.userdata": "Please enter userdata",
 "message.error.username": "Enter your username",
 "message.error.valid.iops.range": "Please enter a valid IOPS range",
 "message.error.vcenter.datacenter": "Please enter vCenter Datacenter",
@@ -3161,6 +3179,7 @@
 "message.pending.projects.2": "To view, please go to the projects section, then select invitations from the drop-down.",
 "message.please.add.at.lease.one.traffic.range": "Please add at least one traffic range.",
 "message.please.confirm.remove.ssh.key.pair": "Please confirm that you want to remove this SSH Key Pair",
+"message.please.confirm.remove.user.data": "Please confirm that you want to remove this userdata",
 "message.please.enter.valid.value": "Please enter a valid value",
 "message.please.enter.value": "Please enter values",
 "message.please.proceed": "Please proceed to the next step.",
@@ -3324,6 +3343,7 @@
 "message.success.register.iso": "Successfully registered ISO",
 "message.success.register.keypair": "Successfully registered SSH key pair",
 "message.success.register.template": "Successfully registered template",
+"message.success.register.user.data": "Successfully registered Userdata",
 "message.success.release.ip": "Successfully released IP",
 "message.success.remove.egress.rule": "Successfully removed Egress rule",
 "message.success.remove.firewall.rule": "Successfully removed Firewall rule",
@@ -3343,6 +3363,7 @@
 "message.success.update.kubeversion": "Successfully updated Kubernetes supported version",
 "message.success.update.network": "Successfully updated Network",
 "message.success.update.template": "Successfully updated Template",
+"message.success.update.iso": "Successfully updated ISO",
 "message.success.update.user": "Successfully updated user",
 "message.success.upgrade.kubernetes": "Successfully upgraded Kubernetes cluster",
 "message.success.upload": "Upload Successfully",
diff --git a/ui/public/locales/pl.json b/ui/public/locales/pl.json
index 595b007..fbc94c5 100644
--- a/ui/public/locales/pl.json
+++ b/ui/public/locales/pl.json
@@ -2115,6 +2115,7 @@
 "message.desc.primary.storage": "Each cluster must contain one or more primary storage servers, and we will add the first one now. Primary storage contains the disk volumes for all the VMs running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.",
 "message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this VM. Please note the root password will be changed by this operation if password is enabled.",
 "message.desc.secondary.storage": "Each zone must have at least one NFS or secondary storage server, and we will add the first one now. Secondary storage stores VM templates, ISO images, and VM disk volume snapshots. This server must be available to all hosts in the zone.<br/><br/>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 5223c40..e0cdb10 100644
--- a/ui/src/components/view/AnnotationsTab.vue
+++ b/ui/src/components/view/AnnotationsTab.vue
@@ -168,6 +168,7 @@
         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 14508ba..10b759f 100644
--- a/ui/src/components/view/DetailsTab.vue
+++ b/ui/src/components/view/DetailsTab.vue
@@ -45,6 +45,9 @@
         <div v-else-if="['created', 'sent', 'lastannotated'].includes(item)">
           {{ $toLocaleDate(resource[item]) }}
         </div>
+        <div v-else-if="$route.meta.name === 'userdata' && item === 'userdata'">
+          <div style="white-space: pre-wrap;"> {{ decodeUserData(resource.userdata) }} </div>
+        </div>
         <div v-else>{{ resource[item] }}</div>
       </div>
     </a-list-item>
@@ -119,6 +122,10 @@
     }
   },
   methods: {
+    decodeUserData (userdata) {
+      const decodedData = Buffer.from(userdata, 'base64')
+      return decodedData.toString('utf-8')
+    },
     fetchProjectAdmins () {
       if (!this.resource.owner) {
         return false
diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue
index e94fd1e..588582d 100644
--- a/ui/src/components/view/InfoCard.vue
+++ b/ui/src/components/view/InfoCard.vue
@@ -541,6 +541,14 @@
             <span v-else>{{ resource.zone || resource.zonename || resource.zoneid }}</span>
           </div>
         </div>
+        <div class="resource-detail-item" v-if="resource.userdataname">
+          <div class="resource-detail-item__label">{{ $t('label.userdata') }}</div>
+          <div class="resource-detail-item__details">
+            <solution-outlined />
+            <router-link v-if="!isStatic && $router.resolve('/userdata/' + resource.userdataid).route.name !== '404'" :to="{ path: '/userdata/' + resource.userdataid }">{{ resource.userdataname || resource.userdataid }}</router-link>
+            <span v-else>{{ resource.userdataname || resource.userdataid }}</span>
+          </div>
+        </div>
         <div class="resource-detail-item" v-if="resource.owner">
           <div class="resource-detail-item__label">{{ $t('label.owners') }}</div>
           <div class="resource-detail-item__details">
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue
index f060b16..9f567e6 100644
--- a/ui/src/components/view/ListView.vue
+++ b/ui/src/components/view/ListView.vue
@@ -468,7 +468,7 @@
   },
   methods: {
     quickViewEnabled () {
-      return new RegExp(['/vm', '/kubernetes', '/ssh', '/vmgroup', '/affinitygroup',
+      return new RegExp(['/vm', '/kubernetes', '/ssh', '/userdata', '/vmgroup', '/affinitygroup',
         '/volume', '/snapshot', '/vmsnapshot', '/backup',
         '/guestnetwork', '/vpc', '/vpncustomergateway',
         '/template', '/iso',
@@ -478,7 +478,7 @@
         .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 835923d..867c229 100644
--- a/ui/src/config/section/compute.js
+++ b/ui/src/config/section/compute.js
@@ -66,7 +66,7 @@
         return fields
       },
       searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'],
-      details: ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename'],
+      details: ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy'],
       tabs: [{
         component: () => import('@/views/compute/InstanceTab.vue')
       }],
@@ -370,6 +370,17 @@
           }
         },
         {
+          api: 'resetUserDataForVirtualMachine',
+          icon: 'solution',
+          label: 'label.reset.userdata.on.vm',
+          message: 'message.desc.reset.userdata',
+          docHelp: 'adminguide/virtual_machines.html#resetting-userdata',
+          dataView: true,
+          show: (record) => { return ['Stopped'].includes(record.state) },
+          popup: true,
+          component: () => import('@/views/compute/ResetUserData')
+        },
+        {
           api: 'assignVirtualMachine',
           icon: 'user-add',
           label: 'label.assign.instance.another',
@@ -638,6 +649,77 @@
       ]
     },
     {
+      name: 'userdata',
+      title: 'label.user.data',
+      icon: 'solution',
+      docHelp: 'adminguide/virtual_machines.html#user-data-and-meta-data',
+      permission: ['listUserData'],
+      columns: () => {
+        var fields = ['name', 'id']
+        if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
+          fields.push('account')
+        }
+        return fields
+      },
+      resourceType: 'UserData',
+      details: ['id', 'name', 'userdata', 'account', 'domain', 'params'],
+      related: [{
+        name: 'vm',
+        title: 'label.instances',
+        param: 'userdata'
+      }],
+      tabs: [
+        {
+          name: 'details',
+          component: () => import('@/components/view/DetailsTab.vue')
+        },
+        {
+          name: 'comments',
+          component: () => import('@/components/view/AnnotationsTab.vue')
+        }
+      ],
+      actions: [
+        {
+          api: 'registerUserData',
+          icon: 'plus',
+          label: 'label.register.user.data',
+          docHelp: 'adminguide/virtual_machines.html#creating-the-ssh-keypair',
+          listView: true,
+          popup: true,
+          component: () => import('@/views/compute/RegisterUserData.vue')
+        },
+        {
+          api: 'deleteUserData',
+          icon: 'delete',
+          label: 'label.remove.user.data',
+          message: 'message.please.confirm.remove.user.data',
+          dataView: true,
+          args: ['id', 'account', 'domainid'],
+          mapping: {
+            id: {
+              value: (record, params) => { return record.id }
+            },
+            account: {
+              value: (record, params) => { return record.account }
+            },
+            domainid: {
+              value: (record, params) => { return record.domainid }
+            }
+          },
+          groupAction: true,
+          popup: true,
+          groupMap: (selection, values, record) => {
+            return selection.map(x => {
+              const data = record.filter(y => { return y.id === x })
+              return {
+                id: x, account: data[0].account, domainid: data[0].domainid
+              }
+            })
+          }
+        }
+      ]
+    },
+    {
       name: 'affinitygroup',
       title: 'label.affinity.groups',
       icon: 'swap',
diff --git a/ui/src/config/section/image.js b/ui/src/config/section/image.js
index a93dc20..abdb9a8 100644
--- a/ui/src/config/section/image.js
+++ b/ui/src/config/section/image.js
@@ -46,7 +46,7 @@
       details: () => {
         var fields = ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'isready', 'passwordenabled',
           'directdownload', 'deployasis', 'ispublic', 'isfeatured', 'isextractable', 'isdynamicallyscalable', 'crosszones', 'type',
-          'account', 'domain', 'created']
+          'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy']
         if (['Admin'].includes(store.getters.userInfo.roletype)) {
           fields.push('templatetype', 'url')
         }
@@ -194,7 +194,7 @@
         }
         return fields
       },
-      details: ['name', 'id', 'displaytext', 'checksum', 'ostypename', 'size', 'bootable', 'isready', 'directdownload', 'isextractable', 'ispublic', 'isfeatured', 'crosszones', 'account', 'domain', 'created'],
+      details: ['name', 'id', 'displaytext', 'checksum', 'ostypename', 'size', 'bootable', 'isready', 'directdownload', 'isextractable', 'ispublic', 'isfeatured', 'crosszones', 'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy'],
       searchFilters: ['name', 'zoneid', 'tags'],
       related: [{
         name: 'vm',
diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ui/src/core/lazy_lib/icons_use.js
diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue
index 3c330e1..d5b80ad 100644
--- a/ui/src/views/compute/DeployVM.vue
+++ b/ui/src/views/compute/DeployVM.vue
@@ -543,9 +543,116 @@
                       </a-form-item>
                     </a-form-item>
                     <a-form-item :label="$t('label.userdata')">
-                      <a-textarea
-                        v-decorator="['userdata']">
-                      </a-textarea>
+                      <a-card>
+                        <div v-if="this.template && this.template.userdataid">
+                          Userdata "{{ $t(this.template.userdataname) }}" is linked with template "{{ $t(this.template.name) }}" with override policy "{{ $t(this.template.userdatapolicy) }}"
+                          <br/><br/>
+                          <div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
+                            <div v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
+                              Enter the values for the variables in userdata
+                            </div>
+                            <a-input-group>
+                              <a-table
+                                size="small"
+                                style="overflow-y: auto"
+                                :columns="userDataParamCols"
+                                :dataSource="templateUserDataParams"
+                                :pagination="false"
+                                :rowKey="record => record.key">
+                                <template slot="value" slot-scope="text, record">
+                                  <a-input v-model="templateUserDataValues[record.key]" />
+                                </template>
+                              </a-table>
+                            </a-input-group>
+                          </div>
+                        </div>
+                        <div v-if="this.iso && this.iso.userdataid">
+                          Userdata "{{ $t(this.iso.userdataname) }}" is linked with ISO "{{ $t(this.iso.name) }}" with override policy "{{ $t(this.iso.userdatapolicy) }}"
+                          <br/><br/>
+                          <div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
+                            <div v-if="this.iso && this.iso.userdataid && templateUserDataParams.length > 0">
+                              Enter the values for the variables in userdata
+                            </div>
+                            <a-input-group>
+                              <a-table
+                                size="small"
+                                style="overflow-y: auto"
+                                :columns="userDataParamCols"
+                                :dataSource="templateUserDataParams"
+                                :pagination="false"
+                                :rowKey="record => record.key">
+                                <template slot="value" slot-scope="text, record">
+                                  <a-input v-model="templateUserDataValues[record.key]" />
+                                </template>
+                              </a-table>
+                            </a-input-group>
+                          </div>
+                        </div><br/><br/>
+                        <div v-if="userdataDefaultOverridePolicy === 'ALLOWOVERRIDE' || userdataDefaultOverridePolicy === 'APPEND' || !userdataDefaultOverridePolicy">
+                          <span v-if="userdataDefaultOverridePolicy === 'ALLOWOVERRIDE'" >
+                            {{ $t('label.userdata.do.override') }}
+                            <a-switch @change="val => { doUserdataOverride = val }" :checked="doUserdataOverride" style="margin-left: 10px"/>
+                          </span>
+                          <span v-if="userdataDefaultOverridePolicy === 'APPEND'">
+                            {{ $t('label.userdata.do.append') }}
+                            <a-switch @change="val => { doUserdataAppend = val }" :checked="doUserdataAppend" style="margin-left: 10px"/>
+                          </span>
+                          <a-step
+                            :status="zoneSelected ? 'process' : 'wait'">
+                            <template #description>
+                              <div v-if="doUserdataOverride || doUserdataAppend || !userdataDefaultOverridePolicy" style="margin-top: 15px">
+                                <a-card
+                                  :tabList="userdataTabList"
+                                  :activeTabKey="userdataTabKey"
+                                  @tabChange="key => onUserdataTabChange(key, 'userdataTabKey')">
+                                  <div v-if="userdataTabKey === 'userdataregistered'">
+                                    <a-step
+                                      v-if="isUserAllowedToListUserDatas"
+                                      :status="zoneSelected ? 'process' : 'wait'">
+                                      <template #description>
+                                        <div v-if="zoneSelected">
+                                          <user-data-selection
+                                            :items="options.userDatas"
+                                            :row-count="rowCount.userDatas"
+                                            :zoneId="zoneId"
+                                            :disabled="template.userdatapolicy === 'DENYOVERRIDE'"
+                                            :loading="loading.userDatas"
+                                            :preFillContent="dataPreFill"
+                                            @select-user-data-item="($event) => updateUserData($event)"
+                                            @handle-search-filter="($event) => handleSearchFilter('userData', $event)"
+                                          />
+                                          <div v-if="userDataParams.length > 0">
+                                            <a-input-group>
+                                              <a-table
+                                                size="small"
+                                                style="overflow-y: auto"
+                                                :columns="userDataParamCols"
+                                                :dataSource="userDataParams"
+                                                :pagination="false"
+                                                :rowKey="record => record.key">
+                                                <template slot="value" slot-scope="text, record">
+                                                  <a-input v-model="userDataValues[record.key]" />
+                                                </template>
+                                              </a-table>
+                                            </a-input-group>
+                                          </div>
+                                        </div>
+                                      </template>
+                                    </a-step>
+                                  </div>
+                                  <div v-else>
+                                    <a-form-item :label="$t('label.userdata')">
+                                      <a-textarea
+                                        v-decorator="['userdata']">
+                                      </a-textarea>
+                                    </a-form-item>
+                                  </div>
+                                </a-card>
+                              </div>
+                            </template>
+                          </a-step>
+                        </div>
+                      </a-card>
                     </a-form-item>
                     <a-form-item :label="this.$t('label.affinity.groups')">
                       <affinity-group-selection
@@ -688,6 +795,7 @@
 import NetworkSelection from '@views/compute/wizard/NetworkSelection'
 import NetworkConfiguration from '@views/compute/wizard/NetworkConfiguration'
 import SshKeyPairSelection from '@views/compute/wizard/SshKeyPairSelection'
+import UserDataSelection from '@views/compute/wizard/UserDataSelection'
 import SecurityGroupSelection from '@views/compute/wizard/SecurityGroupSelection'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
 import InstanceNicsNetworkSelectListView from '@/components/view/InstanceNicsNetworkSelectListView.vue'
@@ -696,6 +804,7 @@
   name: 'Wizard',
   components: {
     SshKeyPairSelection,
+    UserDataSelection,
     NetworkConfiguration,
     NetworkSelection,
     AffinityGroupSelection,
@@ -729,6 +838,11 @@
       zoneSelected: false,
       startvm: true,
       dynamicscalingenabled: true,
+      templateKey: 0,
+      doUserdataOverride: false,
+      doUserdataAppend: false,
+      userdataDefaultOverridePolicy: 'ALLOWOVERRIDE',
+      userdataid: null,
       vm: {
         name: null,
         zoneid: null,
@@ -758,6 +872,7 @@
         affinityGroups: [],
         networks: [],
         sshKeyPairs: [],
+        UserDatas: [],
         pods: [],
         clusters: [],
         hosts: [],
@@ -778,6 +893,7 @@
         affinityGroups: false,
         networks: false,
         sshKeyPairs: false,
+        userDatas: false,
         zones: false,
         pods: false,
         clusters: false,
@@ -800,6 +916,34 @@
       networksAdd: [],
       zone: {},
       sshKeyPair: {},
+      userData: {},
+      userDataParams: [],
+      userDataParamCols: [
+        {
+          title: this.$t('label.key'),
+          dataIndex: 'key'
+        },
+        {
+          title: this.$t('label.value'),
+          dataIndex: 'value',
+          scopedSlots: { customRender: 'value' }
+        }
+      ],
+      userDataValues: {},
+      templateUserDataCols: [
+        {
+          title: this.$t('label.userdata'),
+          dataIndex: 'userdata'
+        },
+        {
+          title: this.$t('label.userdatapolicy'),
+          dataIndex: 'userdataoverridepolicy',
+          scopedSlots: { customRender: 'value' }
+        }
+      ],
+      templateUserDataParams: [],
+      templateUserDataValues: {},
+      overrideDiskOffering: {},
       templateFilter: [
         'featured',
         'community',
@@ -817,6 +961,7 @@
       networkConfig: [],
       dataNetworkCreated: [],
       tabKey: 'templateid',
+      userdataTabKey: 'userdataregistered',
       dataPreFill: {},
       showDetails: false,
       showRootDiskSizeChanger: false,
@@ -905,6 +1050,15 @@
             listall: false
           }
         },
+        userDatas: {
+          list: 'listUserData',
+          options: {
+            page: 1,
+            pageSize: 10,
+            keyword: undefined,
+            listall: false
+          }
+        },
         networks: {
           list: 'listNetworks',
           options: {
@@ -1061,12 +1215,28 @@
 
       return tabList
     },
+    userdataTabList () {
+      let tabList = []
+      tabList = [{
+        key: 'userdataregistered',
+        tab: this.$t('label.userdata.registered')
+      },
+      {
+        key: 'userdatatext',
+        tab: this.$t('label.userdata.text')
+      }]
+
+      return tabList
+    },
     showSecurityGroupSection () {
       return (this.networks.length > 0 && this.zone.securitygroupsenabled) || (this.zone && this.zone.networktype === 'Basic')
     },
     isUserAllowedToListSshKeys () {
       return Boolean('listSSHKeyPairs' in this.$store.getters.apis)
     },
+    isUserAllowedToListUserDatas () {
+      return Boolean('listUserData' in this.$store.getters.apis)
+    },
     dynamicScalingVmConfigValue () {
       return this.options.dynamicScalingVmConfig?.[0]?.value === 'true'
     },
@@ -1203,6 +1373,8 @@
   template (oldValue, newValue) {
     if (oldValue && newValue && oldValue.id !== newValue.id) {
       this.dynamicscalingenabled = this.isDynamicallyScalable()
+      this.doUserdataOverride = false
+      this.doUserdataAppend = false
     }
   },
   created () {
@@ -1442,6 +1614,8 @@
         if (template) {
           var size = template.size / (1024 * 1024 * 1024) || 0 // bytes to GB
           this.dataPreFill.minrootdisksize = Math.ceil(size)
+          this.updateTemplateLinkedUserData(this.template.userdataid)
+          this.userdataDefaultOverridePolicy = this.template.userdatapolicy
         }
       } else if (name === 'isoid') {
         this.templateConfigurations = []
@@ -1455,6 +1629,8 @@
           isoid: value,
           templateid: null
         })
+        this.updateTemplateLinkedUserData(this.iso.userdataid)
+        this.userdataDefaultOverridePolicy = this.iso.userdatapolicy
       } else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) {
         this.vm[name] = value
         this.form.setFieldsValue({
@@ -1520,6 +1696,56 @@
         keypair: name
       })
     },
+    updateUserData (id) {
+      if (id === '0') {
+        this.userdataid = undefined
+        return
+      }
+      this.userdataid = id
+      this.userDataParams = []
+      api('listUserData', { id: id }).then(json => {
+        const resp = json?.listuserdataresponse?.userdata || []
+        if (resp) {
+          var params = resp[0].params
+          if (params) {
+            var dataParams = params.split(',')
+          }
+          var that = this
+          dataParams.forEach(function (val, index) {
+            that.userDataParams.push({
+              id: index,
+              key: val
+            })
+          })
+        }
+      })
+    },
+    updateTemplateLinkedUserData (id) {
+      if (id === '0') {
+        return
+      }
+      this.templateUserDataParams = []
+
+      api('listUserData', { id: id }).then(json => {
+        const resp = json?.listuserdataresponse?.userdata || []
+        if (resp) {
+          var params = resp[0].params
+          if (params) {
+            var dataParams = params.split(',')
+          }
+          var that = this
+          that.templateUserDataParams = []
+          if (dataParams) {
+            dataParams.forEach(function (val, index) {
+              that.templateUserDataParams.push({
+                id: index,
+                key: val
+              })
+            })
+          }
+        }
+      })
+    },
     escapePropertyKey (key) {
       return key.split('.').join('\\002E')
     },
@@ -1721,6 +1947,8 @@
         }
         // step 7: select ssh key pair
         deployVmData.keypair = values.keypair
+        deployVmData.userdataid = this.userdataid
+
         if (values.name) {
           deployVmData.name = values.name
           deployVmData.displayname = values.name
@@ -1748,6 +1976,20 @@
         deployVmData = Object.fromEntries(
           Object.entries(deployVmData).filter(([key, value]) => value !== undefined))
 
+        var idx = 0
+        if (this.templateUserDataValues) {
+          for (const [key, value] of Object.entries(this.templateUserDataValues)) {
+            deployVmData['userdatadetails[' + idx + '].' + `${key}`] = value
+            idx++
+          }
+        }
+        if (this.userDataValues) {
+          for (const [key, value] of Object.entries(this.userDataValues)) {
+            deployVmData['userdatadetails[' + idx + '].' + `${key}`] = value
+            idx++
+          }
+        }
+
         api('deployVirtualMachine', {}, 'POST', deployVmData).then(response => {
           const jobId = response.deployvirtualmachineresponse.jobid
           if (jobId) {
@@ -2027,6 +2269,10 @@
         this.fetchAllIsos()
       }
     },
+    onUserdataTabChange (key, type) {
+      this[type] = key
+      this.userDataParams = []
+    },
     sanitizeReverse (value) {
       const reversedValue = value
         .replace(/&amp;/g, '&')
diff --git a/ui/src/views/compute/RegisterUserData.vue b/ui/src/views/compute/RegisterUserData.vue
new file mode 100644
index 0000000..0084d22
--- /dev/null
+++ b/ui/src/views/compute/RegisterUserData.vue
@@ -0,0 +1,236 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div class="form-layout">
+    <a-spin :spinning="loading" v-if="!isSubmitted">
+      <p v-html="$t('message.desc.register.user.data')"></p>
+      <a-form
+        v-ctrl-enter="handleSubmit"
+        :form="form"
+        @finish="handleSubmit"
+        layout="vertical">
+        <a-form-item>
+          <tooltip-label slot="label" :title="$t('label.name')" :tooltip="apiParams.name.description"/>
+          <a-input
+            v-decorator="['name', {
+              rules: [{ required: true, message: `${$t('message.error.required.input')}` }]
+            }]"
+            :placeholder="apiParams.name.description"
+            v-focus="true" />
+        </a-form-item>
+        <a-form-item>
+          <tooltip-label slot="label" :title="$t('label.userdata')" :tooltip="apiParams.userdata.description"/>
+          <a-textarea
+            v-decorator="['userdata']"
+            :placeholder="apiParams.userdata.description"/>
+        </a-form-item>
+        <a-form-item>
+          <tooltip-label slot="label" :title="$t('label.userdataparams')" :tooltip="apiParams.params.description"/>
+          <a-select
+            mode="tags"
+            v-decorator="['params']"
+            showSearch
+            optionFilterProp="children"
+            :filterOption="(input, option) => {
+              return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+            }"
+            :placeholder="apiParams.params.description">
+            <a-select-option v-for="opt in params" :key="opt">
+              {{ opt }}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item v-if="isAdminOrDomainAdmin()">
+          <tooltip-label slot="label" :title="$t('label.domainid')" :tooltip="apiParams.domainid.description"/>
+          <a-select
+            id="domain-selection"
+            v-decorator="['domainid']"
+            showSearch
+            optionFilterProp="children"
+            :filterOption="(input, option) => {
+              return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+            }"
+            :loading="domainLoading"
+            :placeholder="apiParams.domainid.description"
+            @change="val => { handleDomainChanged(domains[val]) }">
+            <a-select-option v-for="(opt, optIndex) in domains" :key="optIndex">
+              {{ opt.path || opt.name || opt.description }}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item v-if="isAdminOrDomainAdmin() && !isObjectEmpty(selectedDomain) && selectedDomain.id !== null">
+          <tooltip-label :slot="label" :title="$t('label.account')" :tooltip="apiParams.account.description"/>
+          <a-input
+            v-decorator="['account']"
+            :placeholder="apiParams.account.description"/>
+        </a-form-item>
+
+        <div :span="24" class="action-button">
+          <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+          <a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
+        </div>
+      </a-form>
+    </a-spin>
+    <div v-if="isSubmitted">
+      <p v-html="$t('message.desc.registered.user.data')"></p>
+      <div :span="24" class="action-button">
+        <a-button @click="notifyCopied" v-clipboard:copy="hiddenElement.innerHTML" type="primary">{{ $t('label.copy.clipboard') }}</a-button>
+        <a-button @click="downloadKey" type="primary">{{ $t('label.download') }}</a-button>
+        <a-button @click="closeAction">{{ $t('label.close') }}</a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+
+export default {
+  name: 'RegisterUserData',
+  props: {},
+  components: {
+    TooltipLabel
+  },
+  data () {
+    return {
+      domains: [],
+      domainLoading: false,
+      selectedDomain: {},
+      loading: false,
+      isSubmitted: false,
+      hiddenElement: null
+    }
+  },
+  beforeCreate () {
+    this.apiParams = this.$getApiParams('registerUserData')
+    this.form = this.$form.createForm(this)
+  },
+  created () {
+    this.domains = [
+      {
+        id: null,
+        name: ''
+      }
+    ]
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      if (this.isAdminOrDomainAdmin()) {
+        this.fetchDomainData()
+      }
+    },
+    isAdminOrDomainAdmin () {
+      return ['Admin', 'DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
+    },
+    isValidValueForKey (obj, key) {
+      return key in obj && obj[key] != null
+    },
+    arrayHasItems (array) {
+      return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
+    },
+    isObjectEmpty (obj) {
+      return !(obj !== null && obj !== undefined && Object.keys(obj).length > 0 && obj.constructor === Object)
+    },
+    fetchDomainData () {
+      const params = {}
+      this.domainLoading = true
+      api('listDomains', params).then(json => {
+        const listdomains = json.listdomainsresponse.domain
+        this.domains = this.domains.concat(listdomains)
+      }).finally(() => {
+        this.domainLoading = false
+        if (this.arrayHasItems(this.domains)) {
+          this.form.domainid = 0
+          this.handleDomainChanged(this.domains[0])
+        }
+      })
+    },
+    handleDomainChanged (domain) {
+      this.selectedDomain = domain
+    },
+    sanitizeReverse (value) {
+      const reversedValue = value
+        .replace(/&amp;/g, '&')
+        .replace(/&lt;/g, '<')
+        .replace(/&gt;/g, '>')
+
+      return reversedValue
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+      if (this.loading) return
+      this.form.validateFieldsAndScroll((err, values) => {
+        if (err) {
+          return
+        }
+        this.loading = true
+        const params = {
+          name: values.name
+        }
+        if (this.isValidValueForKey(values, 'domainid') &&
+          this.arrayHasItems(this.domains) &&
+          this.domains[values.domainid].id !== null) {
+          params.domainid = this.domains[values.domainid].id
+        }
+        if (this.isValidValueForKey(values, 'account') && values.account.length > 0) {
+          params.account = values.account
+        }
+        params.userdata = encodeURIComponent(btoa(this.sanitizeReverse(values.userdata)))
+
+        if (values.params != null && values.params.length > 0) {
+          var userdataparams = values.params.join(',')
+          params.params = userdataparams
+        }
+
+        api('registerUserData', params).then(json => {
+          this.$message.success(this.$t('message.success.register.user.data') + ' ' + values.name)
+        }).catch(error => {
+          this.$notifyError(error)
+        }).finally(() => {
+          this.$emit('refresh-data')
+          this.loading = false
+          this.closeAction()
+        })
+      })
+    },
+    downloadKey () {
+      this.hiddenElement.click()
+    },
+    notifyCopied () {
+      this.$notification.info({
+        message: this.$t('message.success.copy.clipboard')
+      })
+    },
+    closeAction () {
+      this.$emit('close-action')
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+  .form-layout {
+    width: 80vw;
+
+    @media (min-width: 600px) {
+      width: 450px;
+    }
+  }
+</style>
diff --git a/ui/src/views/compute/ResetUserData.vue b/ui/src/views/compute/ResetUserData.vue
new file mode 100644
index 0000000..9e2f9cb
--- /dev/null
+++ b/ui/src/views/compute/ResetUserData.vue
@@ -0,0 +1,389 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <a-spin :spinning="loadingData">
+    <a-form
+      :form="form"
+      @submit="handleSubmit"
+      layout="vertical">
+      <div v-if="template && template.userdataid">
+        The template "{{ $t(this.template.name) }}" is linked to Userdata "{{ $t(this.template.userdataname) }}" with override policy "{{ $t(this.template.userdatapolicy) }}"
+        <br/><br/>
+        <div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
+          <div v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
+            Enter the values for the variables in userdata
+          </div>
+          <a-input-group>
+            <a-table
+              size="small"
+              style="overflow-y: auto"
+              :columns="userDataParamCols"
+              :dataSource="templateUserDataParams"
+              :pagination="false"
+              :rowKey="record => record.key">
+              <template #value="{ record }">
+                <a-input v-model="templateUserDataValues[record.key]" />
+              </template>
+            </a-table>
+          </a-input-group>
+        </div><br/><br/>
+      </div>
+      <div v-if="userdataDefaultOverridePolicy === 'ALLOWOVERRIDE' || userdataDefaultOverridePolicy === 'APPEND' || !userdataDefaultOverridePolicy">
+        <span v-if="userdataDefaultOverridePolicy === 'ALLOWOVERRIDE'" >
+          {{ $t('label.userdata.do.override') }}
+          <a-switch @change="val => { doUserdataOverride = val }" :checked="doUserdataOverride" style="margin-left: 10px"/>
+        </span>
+        <span v-if="userdataDefaultOverridePolicy === 'APPEND'">
+          {{ $t('label.userdata.do.append') }}
+          <a-switch @change="val => { doUserdataAppend = val }" :checked="doUserdataAppend" style="margin-left: 10px"/>
+        </span>
+        <a-step>
+          <template #description>
+            <div v-if="doUserdataOverride || doUserdataAppend || !userdataDefaultOverridePolicy" style="margin-top: 15px">
+              <a-card
+                :tabList="userdataTabList"
+                :activeTabKey="userdataTabKey"
+                @tabChange="key => onUserdataTabChange(key, 'userdataTabKey')">
+                <div v-if="userdataTabKey === 'userdataregistered'">
+                  <a-step
+                    v-if="isUserAllowedToListUserDatas">
+                    <template #description>
+                      <div>
+                        <user-data-selection
+                          :items="items"
+                          :row-count="rowCount.userDatas"
+                          :disabled="template.userdatapolicy === 'DENYOVERRIDE'"
+                          :loading="loading.userDatas"
+                          :preFillContent="dataPreFill"
+                          @select-user-data-item="($event) => updateUserData($event)"
+                          @handle-search-filter="($event) => handleSearchFilter('userData', $event)"
+                        />
+                        <div v-if="userDataParams.length > 0">
+                          <a-input-group>
+                            <a-table
+                              size="small"
+                              style="overflow-y: auto"
+                              :columns="userDataParamCols"
+                              :dataSource="userDataParams"
+                              :pagination="false"
+                              :rowKey="record => record.key">
+                              <template slot="value" slot-scope="text, record">
+                                <a-input v-model="userDataValues[record.key]" />
+                              </template>
+                            </a-table>
+                          </a-input-group>
+                        </div>
+                      </div>
+                    </template>
+                  </a-step>
+                </div>
+                <div v-else>
+                  <a-form-item :label="$t('label.userdata')">
+                    <a-textarea
+                      v-decorator="['userdata']">
+                    </a-textarea>
+                  </a-form-item>
+                </div>
+              </a-card>
+            </div>
+          </template>
+        </a-step>
+      </div>
+      <div :span="24" class="action-button">
+        <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+        <a-button :loading="loadingData" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
+      </div>
+    </a-form>
+  </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+import { genericCompare } from '@/utils/sort.js'
+import UserDataSelection from '@views/compute/wizard/UserDataSelection'
+
+export default {
+  name: 'ResetUserData',
+  components: {
+    UserDataSelection
+  },
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    },
+    preFillContent: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  data () {
+    return {
+      items: [],
+      total: 0,
+      columns: [
+        {
+          dataIndex: 'name',
+          title: this.$t('label.name'),
+          sorter: function (a, b) { return genericCompare(a[this.dataIndex] || '', b[this.dataIndex] || '') },
+          width: '40%'
+        },
+        {
+          dataIndex: 'account',
+          slots: { title: 'account' },
+          width: '30%'
+        },
+        {
+          dataIndex: 'domain',
+          slots: { title: 'domain' },
+          width: '30%'
+        }
+      ],
+      selectedRowKeys: [],
+      options: {
+        page: 1,
+        pageSize: 10,
+        listall: false,
+        response: 'json'
+      },
+      filter: '',
+      rowCount: {},
+      loading: {
+        userDatas: false
+      },
+      loadingData: false,
+      doUserdataOverride: false,
+      doUserdataAppend: false,
+      userdataTabList: [],
+      userdataDefaultOverridePolicy: 'ALLOWOVERRIDE',
+      userData: {},
+      userdataid: null,
+      userDataParams: [],
+      userDataParamCols: [
+        {
+          title: this.$t('label.key'),
+          dataIndex: 'key'
+        },
+        {
+          title: this.$t('label.value'),
+          dataIndex: 'value',
+          scopedSlots: { customRender: 'value' }
+        }
+      ],
+      userDataValues: {},
+      templateUserDataCols: [
+        {
+          title: this.$t('label.userdata'),
+          dataIndex: 'userdata'
+        },
+        {
+          title: this.$t('label.userdatapolicy'),
+          dataIndex: 'userdataoverridepolicy'
+        }
+      ],
+      templateUserDataParams: [],
+      templateUserDataValues: {},
+      template: {},
+      userdataTabKey: 'userdataregistered',
+      dataPreFill: {},
+      userdata: {}
+    }
+  },
+  beforeCreate () {
+    this.form = this.$form.createForm(this)
+  },
+  created () {
+    this.fetchData()
+    this.loadUserdataTabList()
+    this.fetchTemplateData()
+    this.dataPreFill = this.preFillContent && Object.keys(this.preFillContent).length > 0 ? this.preFillContent : {}
+  },
+  methods: {
+    fetchData () {
+      this.loadingData = true
+      this.items = []
+      this.total = 0
+      api('listUserData', this.options).then(response => {
+        this.total = response.listuserdataresponse.count
+        if (this.total !== 0) {
+          this.items = response.listuserdataresponse.userdata
+        }
+      }).finally(() => {
+        this.loadingData = false
+      })
+    },
+    fetchTemplateData () {
+      const params = {}
+      params.id = this.resource.templateid
+      params.isrecursive = true
+      params.templatefilter = 'all'
+      var apiName = 'listTemplates'
+      api(apiName, params).then(json => {
+        const templateResponses = json.listtemplatesresponse.template
+        this.template = templateResponses[0]
+        this.updateTemplateLinkedUserData(this.template.userdataid)
+        this.userdataDefaultOverridePolicy = this.template.userdatapolicy
+      })
+    },
+    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
+              })
+            })
+          }
+        }
+      })
+    },
+    loadUserdataTabList () {
+      this.userdataTabList = [{
+        key: 'userdataregistered',
+        tab: this.$t('label.userdata.registered')
+      },
+      {
+        key: 'userdatatext',
+        tab: this.$t('label.userdata.text')
+      }]
+    },
+    onUserdataTabChange (key, type) {
+      this[type] = key
+      this.userDataParams = []
+    },
+    sanitizeReverse (value) {
+      const reversedValue = value
+        .replace(/&amp;/g, '&')
+        .replace(/&lt;/g, '<')
+        .replace(/&gt;/g, '>')
+
+      return reversedValue
+    },
+    isUserAllowedToListUserDatas () {
+      return Boolean('listUserData' in this.$store.getters.apis)
+    },
+    updateUserData (id) {
+      if (id === '0') {
+        this.userdataid = undefined
+        return
+      }
+      this.userdataid = id
+      this.userDataParams = []
+      api('listUserData', { id: id }).then(json => {
+        const resp = json?.listuserdataresponse?.userdata || []
+        if (resp) {
+          var params = resp[0].params
+          if (params) {
+            var dataParams = params.split(',')
+          }
+          var that = this
+          dataParams.forEach(function (val, index) {
+            that.userDataParams.push({
+              id: index,
+              key: val
+            })
+          })
+        }
+      })
+    },
+    onSelectChange (selectedRowKeys) {
+      this.selectedRowKeys = selectedRowKeys
+    },
+    handleSearch (keyword) {
+      this.filter = keyword
+      this.options.keyword = keyword
+      this.fetchData()
+    },
+    handleTableChange (pagination) {
+      this.options.page = pagination.current
+      this.options.pageSize = pagination.pageSize
+      this.fetchData()
+    },
+    handleSubmit () {
+      if (this.loadingData) return
+      this.form.validateFields((err, values) => {
+        if (err) {
+          return
+        }
+        this.loadingData = true
+        console.log(values)
+        const params = {
+          id: this.resource.id
+        }
+        if (values.userdata && values.userdata.length > 0) {
+          params.userdata = encodeURIComponent(btoa(this.sanitizeReverse(values.userdata)))
+        }
+        if (this.userdataid) {
+          params.userdataid = this.userdataid
+        }
+        var idx = 0
+        if (this.templateUserDataValues) {
+          for (const [key, value] of Object.entries(this.templateUserDataValues)) {
+            params['userdatadetails[' + idx + '].' + `${key}`] = value
+            idx++
+          }
+        }
+        if (this.userDataValues) {
+          for (const [key, value] of Object.entries(this.userDataValues)) {
+            params['userdatadetails[' + idx + '].' + `${key}`] = value
+            idx++
+          }
+        }
+        api('resetUserDataForVirtualMachine', params).then(json => {
+          this.$message.success({
+            content: `${this.$t('label.action.userdata.reset')} - ${this.$t('label.success')}`,
+            duration: 2
+          })
+          this.$emit('refresh-data')
+          this.closeAction()
+        }).catch(error => {
+          this.$notifyError(error)
+        }).finally(() => {
+          this.loadingData = false
+        })
+      })
+    },
+    closeAction () {
+      this.$emit('close-action')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.form {
+  width: 90vw;
+  @media (min-width: 800px) {
+    width: 45vw;
+  }
+}
+</style>
diff --git a/ui/src/views/compute/wizard/UserDataSelection.vue b/ui/src/views/compute/wizard/UserDataSelection.vue
new file mode 100644
index 0000000..4f7025a
--- /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.
+
+<template>
+  <div>
+    <a-input-search
+      style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
+      :placeholder="$t('label.search')"
+      v-model="filter"
+      @search="handleSearch" />
+    <a-table
+      :loading="loading"
+      :columns="columns"
+      :dataSource="tableSource"
+      :rowSelection="rowSelection"
+      :customRow="onClickRow"
+      :pagination="true"
+      size="middle"
+      :scroll="{ y: 225 }"
+    >
+      <template v-slot:account><a-icon type="user" /> {{ $t('label.account') }}</template>
+      <template v-slot:domain><a-icon type="block" /> {{ $t('label.domain') }}</template>
+    </a-table>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'UserDataSelection',
+  props: {
+    items: {
+      type: Array,
+      default: () => []
+    },
+    rowCount: {
+      type: Number,
+      default: () => 0
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    preFillContent: {
+      type: Object,
+      default: () => {}
+    },
+    zoneId: {
+      type: String,
+      default: () => ''
+    }
+  },
+  data () {
+    return {
+      filter: '',
+      columns: [
+        {
+          dataIndex: 'name',
+          title: this.$t('label.userdata'),
+          width: '40%'
+        },
+        {
+          dataIndex: 'account',
+          slots: { title: 'account' },
+          width: '30%'
+        },
+        {
+          dataIndex: 'domain',
+          slots: { title: 'domain' },
+          width: '30%'
+        }
+      ],
+      selectedRowKeys: [this.$t('label.noselect')],
+      dataItems: [],
+      oldZoneId: null,
+      options: {
+        page: 1,
+        pageSize: 10,
+        keyword: null
+      }
+    }
+  },
+  computed: {
+    tableSource () {
+      const dataItems = []
+
+      if (this.options.page === 1) {
+        dataItems.push({
+          key: this.$t('label.noselect'),
+          name: this.$t('label.noselect'),
+          account: '-',
+          domain: '-'
+        })
+      }
+
+      this.items.map((item) => {
+        dataItems.push({
+          key: item.id,
+          name: item.name,
+          account: item.account,
+          domain: item.domain
+        })
+      })
+
+      return dataItems
+    },
+    rowSelection () {
+      return {
+        type: 'radio',
+        selectedRowKeys: this.selectedRowKeys,
+        onChange: this.onSelectRow
+      }
+    }
+  },
+  watch: {
+    value (newValue, oldValue) {
+      if (newValue && newValue !== oldValue) {
+        this.selectedRowKeys = [newValue]
+      }
+    },
+    loading () {
+      if (!this.loading) {
+        if (this.preFillContent.userdataid) {
+          this.selectedRowKeys = [this.preFillContent.userdataid]
+          this.$emit('select-user-data-item', this.preFillContent.userdataid)
+        } else {
+          if (this.oldZoneId === this.zoneId) {
+            return
+          }
+          this.oldZoneId = this.zoneId
+          this.selectedRowKeys = [this.$t('label.noselect')]
+          this.$emit('select-user-data-item', this.$t('label.noselect'))
+        }
+      }
+    }
+  },
+  methods: {
+    onSelectRow (value) {
+      this.selectedRowKeys = value
+      this.$emit('select-user-data-item', value[0])
+    },
+    handleSearch (value) {
+      this.filter = value
+      this.options.page = 1
+      this.options.pageSize = 10
+      this.options.keyword = this.filter
+      this.$emit('handle-search-filter', this.options)
+    },
+    onChangePage (page, pageSize) {
+      this.options.page = page
+      this.options.pageSize = pageSize
+      this.$emit('handle-search-filter', this.options)
+    },
+    onChangePageSize (page, pageSize) {
+      this.options.page = page
+      this.options.pageSize = pageSize
+      this.$emit('handle-search-filter', this.options)
+    },
+    onClickRow (record) {
+      return {
+        on: {
+          click: () => {
+            this.selectedRowKeys = [record.key]
+            this.$emit('select-user-data-item', record.key)
+          }
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .ant-table-wrapper {
+    margin: 2rem 0;
+  }
+
+  :deep(.ant-table-tbody) > tr > td {
+    cursor: pointer;
+  }
+</style>
diff --git a/ui/src/views/image/RegisterOrUploadIso.vue b/ui/src/views/image/RegisterOrUploadIso.vue
index eaff52f..fd3b65f 100644
--- a/ui/src/views/image/RegisterOrUploadIso.vue
+++ b/ui/src/views/image/RegisterOrUploadIso.vue
@@ -24,7 +24,7 @@
       {{ $t('message.upload.file.processing') }}
       <a-progress :percent="uploadPercentage" />
     </span>
-    <a-spin :spinning="loading" v-else>
+    <a-spin v-else :spinning="loading">
       <a-form
         v-ctrl-enter="handleSubmit"
         :form="form"
@@ -136,6 +136,44 @@
           </a-select>
         </a-form-item>
 
+        <a-row :gutter="12">
+          <a-col :md="24" :lg="12">
+            <a-form-item :label="$t('label.userdata')">
+              <a-select
+                v-decorator="['userdataid', {}]"
+                v-model="userdatapolicy"
+                :placeholder="linkUserDataParams.userdataid.description"
+                showSearch
+                optionFilterProp="children"
+                :filterOption="(input, option) => {
+                  return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }"
+                :loading="userdata.loading" >
+                <a-select-option v-for="opt in userdata.opts" :key="opt.id">
+                  {{ opt.name || opt.description }}
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+          <a-col :md="24" :lg="12">
+            <a-form-item>
+              <tooltip-label slot="label" :title="$t('label.userdatapolicy')" :tooltip="$t('label.userdatapolicy.tooltip')"/>
+              <a-select
+                v-decorator="['userdatapolicy', {}]"
+                v-model="userdatapolicy"
+                :placeholder="linkUserDataParams.userdatapolicy.description"
+                optionFilterProp="children"
+                :filterOption="(input, option) => {
+                  return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }" >
+                <a-select-option v-for="opt in userdatapolicylist.opts" :key="opt.id">
+                  {{ opt.id || opt.description }}
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+        </a-row>
+
         <a-form-item :label="$t('label.isextractable')">
           <a-switch
             v-decorator="['isextractable', {
@@ -167,6 +205,7 @@
 import store from '@/store'
 import { axios } from '../../utils/request'
 import ResourceIcon from '@/components/view/ResourceIcon'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
 
 export default {
   name: 'RegisterIso',
@@ -181,7 +220,8 @@
     }
   },
   components: {
-    ResourceIcon
+    ResourceIcon,
+    TooltipLabel
   },
   data () {
     return {
@@ -190,6 +230,10 @@
       osTypes: [],
       zoneLoading: false,
       osTypeLoading: false,
+      userdata: {},
+      userdataid: null,
+      userdatapolicy: null,
+      userdatapolicylist: {},
       defaultOsType: '',
       loading: false,
       allowed: false,
@@ -203,6 +247,7 @@
   beforeCreate () {
     this.form = this.$form.createForm(this)
     this.apiParams = this.$getApiParams('registerIso')
+    this.linkUserDataParams = this.$getApiParams('linkUserDataToTemplate')
   },
   created () {
     this.zones = []
@@ -222,6 +267,8 @@
     fetchData () {
       this.fetchZoneData()
       this.fetchOsType()
+      this.fetchUserData()
+      this.fetchUserdataPolicy()
     },
     fetchZoneData () {
       const params = {}
@@ -254,6 +301,36 @@
         this.defaultOsType = 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()
@@ -344,6 +421,9 @@
         if (this.currentForm === 'Create') {
           this.loading = true
           api('registerIso', params).then(json => {
+            if (values.userdataid !== null) {
+              this.linkUserdataToTemplate(values.userdataid, json.registerisoresponse.iso[0].id, values.userdatapolicy)
+            }
             this.$notification.success({
               message: this.$t('label.action.register.iso'),
               description: `${this.$t('message.success.register.iso')} ${params.name}`
@@ -364,6 +444,9 @@
           api('getUploadParamsForIso', params).then(json => {
             this.uploadParams = (json.postuploadisoresponse && json.postuploadisoresponse.getuploadparams) ? json.postuploadisoresponse.getuploadparams : ''
             const response = this.handleUpload()
+            if (values.userdataid !== null) {
+              this.linkUserdataToTemplate(values.userdataid, json.postuploadisoresponse.iso[0].id, values.userdatapolicy)
+            }
             if (response === 'upload successful') {
               this.$notification.success({
                 message: this.$t('message.success.upload'),
@@ -381,6 +464,22 @@
     },
     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 43e8288..ac8487f 100644
--- a/ui/src/views/image/RegisterOrUploadTemplate.vue
+++ b/ui/src/views/image/RegisterOrUploadTemplate.vue
@@ -334,6 +334,43 @@
           </a-select>
         </a-form-item>
         <a-row :gutter="12">
+          <a-col :md="24" :lg="12">
+            <a-form-item :label="$t('label.userdata')">
+              <a-select
+                v-decorator="['userdataid', {}]"
+                v-model="userdatapolicy"
+                :placeholder="linkUserDataParams.userdataid.description"
+                showSearch
+                optionFilterProp="children"
+                :filterOption="(input, option) => {
+                  return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }"
+                :loading="userdata.loading" >
+                <a-select-option v-for="opt in userdata.opts" :key="opt.id">
+                  {{ opt.name || opt.description }}
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+          <a-col :md="24" :lg="12">
+            <a-form-item>
+              <tooltip-label slot="label" :title="$t('label.userdatapolicy')" :tooltip="$t('label.userdatapolicy.tooltip')"/>
+              <a-select
+                v-decorator="['userdatapolicy', {}]"
+                v-model="userdatapolicy"
+                :placeholder="linkUserDataParams.userdatapolicy.description"
+                optionFilterProp="label"
+                :filterOption="(input, option) => {
+                  return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }" >
+                <a-select-option v-for="opt in userdatapolicylist.opts" :key="opt.id">
+                  {{ opt.id || opt.description }}
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+        </a-row>
+        <a-row :gutter="12">
           <a-col :md="24" :lg="24">
             <a-form-item>
               <a-checkbox-group
@@ -396,6 +433,7 @@
 import store from '@/store'
 import { axios } from '../../utils/request'
 import ResourceIcon from '@/components/view/ResourceIcon'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
 
 export default {
   name: 'RegisterOrUploadTemplate',
@@ -410,7 +448,8 @@
     }
   },
   components: {
-    ResourceIcon
+    ResourceIcon,
+    TooltipLabel
   },
   data () {
     return {
@@ -427,6 +466,10 @@
       format: {},
       osTypes: {},
       defaultOsType: '',
+      userdata: {},
+      userdataid: null,
+      userdatapolicy: null,
+      userdatapolicylist: {},
       defaultOsId: null,
       xenServerProvider: false,
       hyperKVMShow: false,
@@ -447,6 +490,7 @@
   beforeCreate () {
     this.form = this.$form.createForm(this)
     this.apiParams = this.$getApiParams('registerTemplate')
+    this.linkUserDataParams = this.$getApiParams('linkUserDataToTemplate')
   },
   created () {
     this.$set(this.zones, 'loading', false)
@@ -474,6 +518,8 @@
     fetchData () {
       this.fetchZone()
       this.fetchOsTypes()
+      this.fetchUserData()
+      this.fetchUserdataPolicy()
       if (Object.prototype.hasOwnProperty.call(store.getters.apis, 'listConfigurations')) {
         this.fetchXenServerProvider()
       }
@@ -590,6 +636,20 @@
         this.osTypes.loading = false
       })
     },
+    fetchUserData () {
+      const params = {}
+      params.listAll = true
+
+      this.userdata.opts = []
+      this.userdata.loading = true
+
+      api('listUserData', params).then(json => {
+        const listUserdata = json.listuserdataresponse.userdata
+        this.userdata.opts = listUserdata
+      }).finally(() => {
+        this.userdata.loading = false
+      })
+    },
     fetchXenServerProvider () {
       const params = {}
       params.name = 'xenserver.pvdriver.version'
@@ -782,6 +842,23 @@
       }
       this.$set(this.format, 'opts', format)
     },
+    fetchUserdataPolicy () {
+      const userdataPolicy = []
+      userdataPolicy.push({
+        id: 'allowoverride',
+        description: 'allowoverride'
+      })
+      userdataPolicy.push({
+        id: 'append',
+        description: 'append'
+      })
+      userdataPolicy.push({
+        id: 'denyoverride',
+        description: 'denyoverride'
+      })
+      this.userdatapolicylist.opts = userdataPolicy
+    },
+
     handlerSelectZone (value) {
       if (!Array.isArray(value)) {
         value = [value]
@@ -888,6 +965,9 @@
         if (this.currentForm === 'Create') {
           this.loading = true
           api('registerTemplate', params).then(json => {
+            if (values.userdataid !== null) {
+              this.linkUserdataToTemplate(values.userdataid, json.registertemplateresponse.template[0].id, values.userdatapolicy)
+            }
             this.$notification.success({
               message: this.$t('label.register.template'),
               description: `${this.$t('message.success.register.template')} ${params.name}`
@@ -911,6 +991,9 @@
           api('getUploadParamsForTemplate', params).then(json => {
             this.uploadParams = (json.postuploadtemplateresponse && json.postuploadtemplateresponse.getuploadparams) ? json.postuploadtemplateresponse.getuploadparams : ''
             this.handleUpload()
+            if (values.userdataid !== null) {
+              this.linkUserdataToTemplate(values.userdataid, json.postuploadtemplateresponse.template[0].id, values.userdatapolicy)
+            }
           }).catch(error => {
             this.$notifyError(error)
           }).finally(() => {
@@ -936,6 +1019,22 @@
     closeAction () {
       this.$emit('close-action')
     },
+    linkUserdataToTemplate (userdataid, templateid, userdatapolicy) {
+      this.loading = true
+      const params = {}
+      params.userdataid = userdataid
+      params.templateid = templateid
+      if (userdatapolicy) {
+        params.userdatapolicy = userdatapolicy
+      }
+      api('linkUserDataToTemplate', params).then(json => {
+        this.closeAction()
+      }).catch(error => {
+        this.$notifyError(error)
+      }).finally(() => {
+        this.loading = false
+      })
+    },
     resetSelect () {
       this.form.setFieldsValue({
         hypervisor: undefined,
diff --git a/ui/src/views/image/UpdateISO.vue b/ui/src/views/image/UpdateISO.vue
new file mode 100644
index 0000000..b566c68
--- /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.
+
+<template>
+  <div class="form-layout" v-ctrl-enter="handleSubmit">
+    <a-spin :spinning="loading">
+      <a-form
+        :ref="formRef"
+        :model="form"
+        :rules="rules"
+        @finish="handleSubmit"
+        layout="vertical">
+        <a-form-item name="name" ref="name">
+          <template #label>
+            <tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
+          </template>
+          <a-input
+            v-model="form.name"
+            :placeholder="apiParams.name.description"
+            autoFocus />
+        </a-form-item>
+        <a-form-item name="displaytext" ref="displaytext">
+          <template #label>
+            <tooltip-label :title="$t('label.displaytext')" :tooltip="apiParams.displaytext.description"/>
+          </template>
+          <a-input
+            v-model="form.displaytext"
+            :placeholder="apiParams.displaytext.description"
+            autoFocus />
+        </a-form-item>
+
+        <a-form-item name="ostypeid" ref="ostypeid" :label="$t('label.ostypeid')">
+          <a-select
+            showSearch
+            optionFilterProp="label"
+            :filterOption="(input, option) => {
+              return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+            }"
+            v-model="form.ostypeid"
+            :loading="osTypes.loading"
+            :placeholder="apiParams.ostypeid.description">
+            <a-select-option v-for="opt in osTypes.opts" :key="opt.id">
+              {{ opt.name || opt.description }}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+
+        <a-row :gutter="12">
+          <a-col :md="24" :lg="12">
+            <a-form-item
+              name="userdataid"
+              ref="userdataid"
+              :label="$t('label.userdata')">
+              <a-select
+                showSearch
+                optionFilterProp="label"
+                :filterOption="(input, option) => {
+                  return option.children?.[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }"
+                v-model="userdataid"
+                :placeholder="linkUserDataParams.userdataid.description"
+                :loading="userdata.loading">
+                <a-select-option v-for="opt in userdata.opts" :key="opt.id">
+                  {{ opt.name || opt.description }}
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+          <a-col :md="24" :lg="12">
+            <a-form-item ref="userdatapolicy" name="userdatapolicy">
+              <template #label>
+                <tooltip-label :title="$t('label.userdatapolicy')" :tooltip="$t('label.userdatapolicy.tooltip')"/>
+              </template>
+              <a-select
+                v-model="userdatapolicy"
+                :placeholder="linkUserDataParams.userdatapolicy.description"
+                optionFilterProp="label"
+                :filterOption="(input, option) => {
+                  return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }" >
+                <a-select-option v-for="opt in userdatapolicylist.opts" :key="opt.id">
+                  {{ opt.name || opt.description }}
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+        </a-row>
+
+        <div :span="24" class="action-button">
+          <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+          <a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
+        </div>
+      </a-form>
+    </a-spin>
+  </div>
+</template>
+
+<script>
+import { ref, reactive } from 'vue'
+import { api } from '@/api'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+
+export default {
+  name: 'UpdateIso',
+  components: {
+    TooltipLabel
+  },
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      osTypes: {},
+      loading: false,
+      userdata: {},
+      userdataid: null,
+      userdatapolicy: null,
+      userdatapolicylist: {}
+    }
+  },
+  beforeCreate () {
+    this.apiParams = this.$getApiParams('updateIso')
+    this.isAdmin = ['Admin'].includes(this.$store.getters.userInfo.roletype)
+    this.linkUserDataParams = this.$getApiParams('linkUserDataToTemplate')
+  },
+  created () {
+    this.initForm()
+    this.osTypes.loading = false
+    this.osTypes.opts = []
+    this.fetchData()
+  },
+  methods: {
+    initForm () {
+      this.formRef = ref()
+      this.form = reactive({})
+      this.rules = reactive({
+        name: [{ required: true, message: this.$t('message.error.required.input') }],
+        displaytext: [{ required: true, message: this.$t('message.error.required.input') }],
+        ostypeid: [{ required: true, message: this.$t('message.error.select') }]
+      })
+      const resourceFields = ['name', 'displaytext', 'ostypeid', 'userdataid', 'userdatapolicy']
+
+      for (var field of resourceFields) {
+        var fieldValue = this.resource[field]
+        if (fieldValue) {
+          switch (field) {
+            case 'userdataid':
+              this.userdataid = fieldValue
+              break
+            case 'userdatapolicy':
+              this.userdatapolicy = fieldValue
+              break
+            default:
+              this.form[field] = fieldValue
+              break
+          }
+        }
+      }
+    },
+    fetchData () {
+      this.fetchOsTypes()
+      this.fetchUserdata()
+      this.fetchUserdataPolicy()
+    },
+    isValidValueForKey (obj, key) {
+      return key in obj && obj[key] != null && obj[key] !== undefined && obj[key] !== ''
+    },
+    fetchOsTypes () {
+      const params = {}
+      params.listAll = true
+      this.osTypes.opts = []
+      this.osTypes.loading = true
+      api('listOsTypes', params).then(json => {
+        const listOsTypes = json.listostypesresponse.ostype
+        this.osTypes.opts = listOsTypes
+      }).finally(() => {
+        this.osTypes.loading = false
+      })
+    },
+    fetchUserdataPolicy () {
+      const userdataPolicy = []
+      userdataPolicy.push({
+        id: '',
+        description: ''
+      })
+      userdataPolicy.push({
+        id: 'allowoverride',
+        description: 'allowoverride'
+      })
+      userdataPolicy.push({
+        id: 'append',
+        description: 'append'
+      })
+      userdataPolicy.push({
+        id: 'denyoverride',
+        description: 'denyoverride'
+      })
+      this.userdatapolicylist.opts = userdataPolicy
+    },
+    fetchUserdata () {
+      const params = {}
+      params.listAll = true
+
+      this.userdata.opts = []
+      this.userdata.loading = true
+
+      api('listUserData', params).then(json => {
+        const userdataIdAndName = []
+        const userdataOpts = json.listuserdataresponse.userdata
+        userdataIdAndName.push({
+          id: '',
+          name: ''
+        })
+
+        Object.values(userdataOpts).forEach(userdata => {
+          userdataIdAndName.push({
+            id: userdata.id,
+            name: userdata.name
+          })
+        })
+
+        this.userdata.opts = userdataIdAndName
+      }).finally(() => {
+        this.userdata.loading = false
+      })
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+      if (this.loading) return
+      this.form.validateFields((err, values) => {
+        if (err) {
+          return
+        }
+        this.loading = true
+        const params = {
+          id: this.resource.id
+        }
+        for (const key in values) {
+          if (!this.isValidValueForKey(values, key)) continue
+          params[key] = values[key]
+        }
+        api('updateIso', params).then(json => {
+          if (values.userdataid !== null) {
+            this.linkUserdataToTemplate(values.userdataid, json.updateisoresponse.iso.id, values.userdatapolicy)
+          }
+          this.$message.success(`${this.$t('message.success.update.iso')}: ${this.resource.name}`)
+          this.$emit('refresh-data')
+          this.closeAction()
+        }).catch(error => {
+          this.$notifyError(error)
+        }).finally(() => {
+          this.loading = false
+        })
+      })
+    },
+    closeAction () {
+      this.$emit('close-action')
+    },
+    linkUserdataToTemplate (userdataid, isoid, userdatapolicy) {
+      this.loading = true
+      const params = {}
+      console.log(userdataid)
+      if (userdataid && userdataid.length > 0) {
+        params.userdataid = userdataid
+      }
+      params.isoid = isoid
+      if (userdatapolicy) {
+        params.userdatapolicy = userdatapolicy
+      }
+      api('linkUserDataToTemplate', params).then(json => {
+        this.closeAction()
+      }).catch(error => {
+        this.$notifyError(error)
+      }).finally(() => {
+        this.loading = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+  .form-layout {
+    width: 60vw;
+
+    @media (min-width: 500px) {
+      width: 450px;
+    }
+  }
+</style>
diff --git a/ui/src/views/image/UpdateTemplate.vue b/ui/src/views/image/UpdateTemplate.vue
index 0d9239c..587c71e 100644
--- a/ui/src/views/image/UpdateTemplate.vue
+++ b/ui/src/views/image/UpdateTemplate.vue
@@ -121,6 +121,41 @@
             </a-form-item>
           </a-col>
         </a-row>
+        <a-row :gutter="12">
+          <a-col :md="24" :lg="12">
+            <a-form-item :label="$t('label.userdata')">
+              <a-select
+                showSearch
+                optionFilterProp="children"
+                :filterOption="(input, option) => {
+                  return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }"
+                v-decorator="['userdataid', {}]"
+                :placeholder="linkUserDataParams.userdataid.description"
+                :loading="userdata.loading">
+                <a-select-option v-for="opt in userdata.opts" :key="opt.id">
+                  {{ opt.name || opt.description }}
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+          <a-col :md="24" :lg="12">
+            <a-form-item>
+              <tooltip-label slot="label" :title="$t('label.userdatapolicy')" :tooltip="$t('label.userdatapolicy.tooltip')"/>
+              <a-select
+                v-decorator="['userdatapolicy', {}]"
+                :placeholder="linkUserDataParams.userdatapolicy.description"
+                optionFilterProp="children"
+                :filterOption="(input, option) => {
+                  return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }" >
+                <a-select-option v-for="opt in userdatapolicylist.opts" :key="opt.id">
+                  {{ opt.id || opt.description }}
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+        </a-row>
         <a-form-item>
           <tooltip-label slot="label" :title="$t('label.isdynamicallyscalable')" :tooltip="apiParams.isdynamicallyscalable.description"/>
           <a-switch v-decorator="['isdynamicallyscalable', {}]" />
@@ -180,13 +215,18 @@
       keyboardType: {},
       osTypes: {},
       loading: false,
-      selectedTemplateType: ''
+      selectedTemplateType: '',
+      userdata: {},
+      userdataid: null,
+      userdatapolicy: null,
+      userdatapolicylist: {}
     }
   },
   beforeCreate () {
     this.form = this.$form.createForm(this)
     this.apiParams = this.$getApiParams('updateTemplate')
     this.isAdmin = ['Admin'].includes(this.$store.getters.userInfo.roletype)
+    this.linkUserDataParams = this.$getApiParams('linkUserDataToTemplate')
   },
   created () {
     this.$set(this.rootDisk, 'loading', false)
@@ -197,7 +237,7 @@
     this.$set(this.keyboardType, 'opts', [])
     this.$set(this.osTypes, 'loading', false)
     this.$set(this.osTypes, 'opts', [])
-    const resourceFields = ['name', 'displaytext', 'passwordenabled', 'ostypeid', 'isdynamicallyscalable']
+    const resourceFields = ['name', 'displaytext', 'passwordenabled', 'ostypeid', 'isdynamicallyscalable', 'userdataid', 'userdatapolicy']
     if (this.isAdmin) {
       resourceFields.push('templatetype')
     }
@@ -205,6 +245,17 @@
       var fieldValue = this.resource[field]
       if (fieldValue) {
         this.form.getFieldDecorator(field, { initialValue: fieldValue })
+        switch (field) {
+          case 'userdataid':
+            this.userdataid = fieldValue
+            break
+          case 'userdatapolicy':
+            this.userdatapolicy = fieldValue
+            break
+          default:
+            this.form[field] = fieldValue
+            break
+        }
       }
     }
     const resourceDetailsFields = []
@@ -227,6 +278,8 @@
       this.fetchRootDiskControllerTypes(this.resource.hypervisor)
       this.fetchNicAdapterTypes()
       this.fetchKeyboardTypes()
+      this.fetchUserdata()
+      this.fetchUserdataPolicy()
     },
     isValidValueForKey (obj, key) {
       return key in obj && obj[key] != null && obj[key] !== undefined && obj[key] !== ''
@@ -347,6 +400,53 @@
 
       this.$set(this.keyboardType, 'opts', keyboardType)
     },
+    fetchUserdataPolicy () {
+      const userdataPolicy = []
+      userdataPolicy.push({
+        id: '',
+        description: ''
+      })
+      userdataPolicy.push({
+        id: 'allowoverride',
+        description: 'allowoverride'
+      })
+      userdataPolicy.push({
+        id: 'append',
+        description: 'append'
+      })
+      userdataPolicy.push({
+        id: 'denyoverride',
+        description: 'denyoverride'
+      })
+      this.userdatapolicylist.opts = userdataPolicy
+    },
+    fetchUserdata () {
+      const params = {}
+      params.listAll = true
+
+      this.userdata.opts = []
+      this.userdata.loading = true
+
+      api('listUserData', params).then(json => {
+        const userdataIdAndName = []
+        const userdataOpts = json.listuserdataresponse.userdata
+        userdataIdAndName.push({
+          id: '',
+          name: ''
+        })
+
+        Object.values(userdataOpts).forEach(userdata => {
+          userdataIdAndName.push({
+            id: userdata.id,
+            name: userdata.name
+          })
+        })
+
+        this.userdata.opts = userdataIdAndName
+      }).finally(() => {
+        this.userdata.loading = false
+      })
+    },
     handleSubmit (e) {
       e.preventDefault()
       if (this.loading) return
@@ -368,6 +468,9 @@
           params[key] = values[key]
         }
         api('updateTemplate', params).then(json => {
+          if (values.userdataid !== null) {
+            this.linkUserdataToTemplate(values.userdataid, json.updatetemplateresponse.template.id, values.userdatapolicy)
+          }
           this.$message.success(`${this.$t('message.success.update.template')}: ${this.resource.name}`)
           this.$emit('refresh-data')
           this.closeAction()
@@ -380,6 +483,24 @@
     },
     closeAction () {
       this.$emit('close-action')
+    },
+    linkUserdataToTemplate (userdataid, templateid, userdatapolicy) {
+      this.loading = true
+      const params = {}
+      if (userdataid && userdataid.length > 0) {
+        params.userdataid = userdataid
+      }
+      params.templateid = templateid
+      if (userdatapolicy) {
+        params.userdatapolicy = userdatapolicy
+      }
+      api('linkUserDataToTemplate', params).then(json => {
+        this.closeAction()
+      }).catch(error => {
+        this.$notifyError(error)
+      }).finally(() => {
+        this.loading = false
+      })
     }
   }
 }