You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by nv...@apache.org on 2022/04/12 01:57:28 UTC

[cloudstack] branch main updated: Direct download certificates additions and improvements (#6104)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 5435b0abfe4 Direct download certificates additions and improvements (#6104)
5435b0abfe4 is described below

commit 5435b0abfe47e5c6e933b651331905467cfa2574
Author: Nicolas Vazquez <ni...@gmail.com>
AuthorDate: Mon Apr 11 22:57:23 2022 -0300

    Direct download certificates additions and improvements (#6104)
    
    * Add direct download certificates listing
    
    * Restore class to original project
    
    * Small refactor
    
    * Register API
    
    * Apply suggestions from code review
    
    Co-authored-by: Suresh Kumar Anaparti <su...@gmail.com>
    
    * Refactor after review
    
    * Fix checkstyle
    
    * Add hosts mapping to API response
    
    * Improvements on revoke certificate
    
    * Refactor revoke certificate API
    
    * Fix condition
    
    * Filter only certificates not revoked for revokeCertificate API
    
    * Improve upload certificate and add provision certificate API
    
    * Improve certificate response output
    
    * Address review comments
    
    * Refactor revoke cert test
    
    * Fix marvin test
    
    * Address review comments
    
    * Fix issues
    
    * Improvements
    
    * Refactor upload template API response
    
    * Fix response
    
    Co-authored-by: Suresh Kumar Anaparti <su...@gmail.com>
---
 .../org/apache/cloudstack/api/ApiConstants.java    |   8 +
 .../apache/cloudstack/api/ResponseGenerator.java   |  13 ++
 .../ListTemplateDirectDownloadCertificatesCmd.java | 110 +++++++++++
 ...isionTemplateDirectDownloadCertificateCmd.java} |  58 ++----
 ...RevokeTemplateDirectDownloadCertificateCmd.java |  72 +++++--
 ...UploadTemplateDirectDownloadCertificateCmd.java |  44 ++++-
 ...irectDownloadCertificateHostStatusResponse.java |  73 +++++++
 .../DirectDownloadCertificateResponse.java         | 162 +++++++++++++++
 .../direct/download/DirectDownloadCertificate.java |   1 +
 ....java => DirectDownloadCertificateHostMap.java} |  13 +-
 .../direct/download/DirectDownloadManager.java     |  60 +++++-
 .../DirectDownloadCertificateHostMapDao.java       |   1 +
 .../DirectDownloadCertificateHostMapDaoImpl.java   |   9 +
 .../DirectDownloadCertificateHostMapVO.java        |  16 +-
 framework/direct-download/pom.xml                  |   8 +
 .../direct/download/DirectDownloadService.java     |   9 +-
 .../main/java/com/cloud/api/ApiResponseHelper.java |  88 +++++++++
 .../com/cloud/server/ManagementServerImpl.java     |   4 +
 .../direct/download/DirectDownloadManagerImpl.java | 217 ++++++++++++++++-----
 .../java/com/cloud/api/ApiResponseHelperTest.java  |  43 ++++
 test/integration/smoke/test_direct_download.py     |  14 +-
 21 files changed, 870 insertions(+), 153 deletions(-)

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 871f99b48f5..698d5d88213 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -26,6 +26,7 @@ public class ApiConstants {
     public static final String ADAPTER_TYPE = "adaptertype";
     public static final String ADDRESS = "address";
     public static final String ALGORITHM = "algorithm";
+    public static final String ALIAS = "alias";
     public static final String ALLOCATED_ONLY = "allocatedonly";
     public static final String ANNOTATION = "annotation";
     public static final String API_KEY = "apikey";
@@ -58,6 +59,10 @@ public class ApiConstants {
     public static final String CERTIFICATE_CHAIN = "certchain";
     public static final String CERTIFICATE_FINGERPRINT = "fingerprint";
     public static final String CERTIFICATE_ID = "certid";
+    public static final String CERTIFICATE_ISSUER = "issuer";
+    public static final String CERTIFICATE_SERIALNUM = "serialnum";
+    public static final String CERTIFICATE_SUBJECT = "subject";
+    public static final String CERTIFICATE_VALIDITY = "validity";
     public static final String ENABLED_REVOCATION_CHECK = "enabledrevocationcheck";
     public static final String CONTROLLER = "controller";
     public static final String CONTROLLER_UNIT = "controllerunit";
@@ -188,6 +193,7 @@ public class ApiConstants {
     public static final String HOST_ID = "hostid";
     public static final String HOST_IDS = "hostids";
     public static final String HOST_NAME = "hostname";
+    public static final String HOSTS_MAP = "hostsmap";
     public static final String HYPERVISOR = "hypervisor";
     public static final String INLINE = "inline";
     public static final String INSTANCE = "instance";
@@ -237,6 +243,7 @@ public class ApiConstants {
     public static final String LEVEL = "level";
     public static final String LENGTH = "length";
     public static final String LIMIT_CPU_USE = "limitcpuuse";
+    public static final String LIST_HOSTS = "listhosts";
     public static final String LOCK = "lock";
     public static final String LUN = "lun";
     public static final String LBID = "lbruleid";
@@ -322,6 +329,7 @@ public class ApiConstants {
     public static final String RESOURCE_TYPE_NAME = "resourcetypename";
     public static final String RESPONSE = "response";
     public static final String REVERTABLE = "revertable";
+    public static final String REVOKED = "revoked";
     public static final String REGISTERED = "registered";
     public static final String QUALIFIERS = "qualifiers";
     public static final String QUERY_FILTER = "queryfilter";
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 03f0a3c8369..6e149b60edf 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java
@@ -23,10 +23,16 @@ import java.util.Map;
 import java.util.Set;
 
 import com.cloud.server.ResourceIcon;
+import com.cloud.utils.Pair;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
 import org.apache.cloudstack.api.response.ResourceIconResponse;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateHostStatusResponse;
 import org.apache.cloudstack.api.response.RouterHealthCheckResultResponse;
 import com.cloud.resource.RollingMaintenanceManager;
 import org.apache.cloudstack.api.response.RollingMaintenanceResponse;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificate;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMap;
+import org.apache.cloudstack.direct.download.DirectDownloadManager;
 import org.apache.cloudstack.management.ManagementServerHost;
 import org.apache.cloudstack.affinity.AffinityGroup;
 import org.apache.cloudstack.affinity.AffinityGroupResponse;
@@ -491,4 +497,11 @@ public interface ResponseGenerator {
 
     ResourceIconResponse createResourceIconResponse(ResourceIcon resourceIcon);
 
+    DirectDownloadCertificateResponse createDirectDownloadCertificateResponse(DirectDownloadCertificate certificate);
+
+    List<DirectDownloadCertificateHostStatusResponse> createDirectDownloadCertificateHostMapResponse(List<DirectDownloadCertificateHostMap> hostMappings);
+
+    DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateHostStatusResponse(DirectDownloadManager.HostCertificateStatus status);
+
+    DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateProvisionResponse(Long certificateId, Long hostId, Pair<Boolean, String> result);
 }
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/ListTemplateDirectDownloadCertificatesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/ListTemplateDirectDownloadCertificatesCmd.java
new file mode 100644
index 00000000000..21480c1ef53
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/ListTemplateDirectDownloadCertificatesCmd.java
@@ -0,0 +1,110 @@
+// 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.admin.direct.download;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.BaseListCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateHostStatusResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
+import org.apache.cloudstack.api.response.ZoneResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificate;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMap;
+import org.apache.cloudstack.direct.download.DirectDownloadManager;
+import org.apache.log4j.Logger;
+
+import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+
+@APICommand(name = ListTemplateDirectDownloadCertificatesCmd.APINAME,
+        description = "List the uploaded certificates for direct download templates",
+        responseObject = DirectDownloadCertificateResponse.class,
+        since = "4.17.0",
+        authorized = {RoleType.Admin})
+public class ListTemplateDirectDownloadCertificatesCmd extends BaseListCmd {
+
+    @Inject
+    DirectDownloadManager directDownloadManager;
+
+    @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DirectDownloadCertificateResponse.class,
+            description = "list direct download certificate by ID")
+    private Long id;
+
+    @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class,
+            description = "the zone where certificates are uploaded")
+    private Long zoneId;
+
+    @Parameter(name = ApiConstants.LIST_HOSTS, type = CommandType.BOOLEAN,
+            description = "if set to true: include the hosts where the certificate is uploaded to")
+    private Boolean listHosts;
+
+    private static final Logger LOG = Logger.getLogger(ListTemplateDirectDownloadCertificatesCmd.class);
+    public static final String APINAME = "listTemplateDirectDownloadCertificates";
+
+    public boolean isListHosts() {
+        return listHosts != null && listHosts;
+    }
+
+    private void createResponse(final List<DirectDownloadCertificate> certificates) {
+        final ListResponse<DirectDownloadCertificateResponse> response = new ListResponse<>();
+        final List<DirectDownloadCertificateResponse> responses = new ArrayList<>();
+        for (final DirectDownloadCertificate certificate : certificates) {
+            if (certificate == null) {
+                continue;
+            }
+            DirectDownloadCertificateResponse certificateResponse = _responseGenerator.createDirectDownloadCertificateResponse(certificate);
+            if (isListHosts()) {
+                List<DirectDownloadCertificateHostMap> hostMappings = directDownloadManager.getCertificateHostsMapping(certificate.getId());
+                List<DirectDownloadCertificateHostStatusResponse> hostMapResponses = _responseGenerator.createDirectDownloadCertificateHostMapResponse(hostMappings);
+                certificateResponse.setHostsMap(hostMapResponses);
+            }
+            responses.add(certificateResponse);
+        }
+        response.setResponses(responses);
+        response.setResponseName(getCommandName());
+        setResponseObject(response);
+    }
+
+    @Override
+    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
+            ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+        List<DirectDownloadCertificate> certificates = directDownloadManager.listDirectDownloadCertificates(id, zoneId);
+        createResponse(certificates);
+    }
+
+    @Override
+    public String getCommandName() {
+        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        return CallContext.current().getCallingAccount().getId();
+    }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/ProvisionTemplateDirectDownloadCertificateCmd.java
similarity index 52%
copy from api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java
copy to api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/ProvisionTemplateDirectDownloadCertificateCmd.java
index ef9fa8b1fa2..b7574b438d0 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/ProvisionTemplateDirectDownloadCertificateCmd.java
@@ -23,67 +23,47 @@ import com.cloud.exception.InsufficientCapacityException;
 import com.cloud.exception.NetworkRuleConflictException;
 import com.cloud.exception.ResourceAllocationException;
 import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.utils.Pair;
 import org.apache.cloudstack.acl.RoleType;
 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.ServerApiException;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateHostStatusResponse;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
 import org.apache.cloudstack.api.response.HostResponse;
-import org.apache.cloudstack.api.response.SuccessResponse;
-import org.apache.cloudstack.api.response.ZoneResponse;
 import org.apache.cloudstack.context.CallContext;
 import org.apache.cloudstack.direct.download.DirectDownloadManager;
-import org.apache.log4j.Logger;
 
 import javax.inject.Inject;
 
-@APICommand(name = RevokeTemplateDirectDownloadCertificateCmd.APINAME,
-        description = "Revoke a certificate alias from a KVM host",
-        responseObject = SuccessResponse.class,
-        requestHasSensitiveInfo = true,
-        responseHasSensitiveInfo = true,
-        since = "4.13",
+@APICommand(name = ProvisionTemplateDirectDownloadCertificateCmd.APINAME,
+        description = "Provisions a host with a direct download certificate",
+        responseObject = DirectDownloadCertificateHostStatusResponse.class,
+        since = "4.17.0",
         authorized = {RoleType.Admin})
-public class RevokeTemplateDirectDownloadCertificateCmd extends BaseCmd {
+public class ProvisionTemplateDirectDownloadCertificateCmd extends BaseCmd {
+
+    public static final String APINAME = "provisionTemplateDirectDownloadCertificate";
 
     @Inject
     DirectDownloadManager directDownloadManager;
 
-    private static final Logger LOG = Logger.getLogger(RevokeTemplateDirectDownloadCertificateCmd.class);
-    public static final String APINAME = "revokeTemplateDirectDownloadCertificate";
-
-    @Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = true,
-            description = "alias of the SSL certificate")
-    private String certificateAlias;
-
-    @Parameter(name = ApiConstants.HYPERVISOR, type = BaseCmd.CommandType.STRING, required = true,
-            description = "hypervisor type")
-    private String hypervisor;
-
-    @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class,
-            description = "zone to revoke certificate", required = true)
-    private Long zoneId;
+    @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DirectDownloadCertificateResponse.class,
+            description = "the id of the direct download certificate to provision", required = true)
+    private Long id;
 
     @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class,
-            description = "(optional) the host ID to revoke certificate")
+            description = "the host to provision the certificate", required = true)
     private Long hostId;
 
     @Override
     public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
-        if (!hypervisor.equalsIgnoreCase("kvm")) {
-            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Currently supporting KVM hosts only");
-        }
-        SuccessResponse response = new SuccessResponse(getCommandName());
-        try {
-            LOG.debug("Revoking certificate " + certificateAlias + " from " + hypervisor + " hosts");
-            boolean result = directDownloadManager.revokeCertificateAlias(certificateAlias, hypervisor, zoneId, hostId);
-            response.setSuccess(result);
-            setResponseObject(response);
-        } catch (Exception e) {
-            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
-        }
+        Pair<Boolean, String> result = directDownloadManager.provisionCertificate(id, hostId);
+        DirectDownloadCertificateHostStatusResponse response = _responseGenerator.createDirectDownloadCertificateProvisionResponse(id, hostId, result);
+        response.setResponseName(getCommandName());
+        setResponseObject(response);
     }
 
     @Override
@@ -95,4 +75,4 @@ public class RevokeTemplateDirectDownloadCertificateCmd extends BaseCmd {
     public long getEntityOwnerId() {
         return CallContext.current().getCallingAccount().getId();
     }
-}
\ No newline at end of file
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java
index ef9fa8b1fa2..507d73f134e 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java
@@ -30,20 +30,26 @@ import org.apache.cloudstack.api.ApiErrorCode;
 import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.api.Parameter;
 import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
 import org.apache.cloudstack.api.response.HostResponse;
-import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateHostStatusResponse;
 import org.apache.cloudstack.api.response.ZoneResponse;
 import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificate;
 import org.apache.cloudstack.direct.download.DirectDownloadManager;
+import org.apache.cloudstack.direct.download.DirectDownloadManager.HostCertificateStatus;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.log4j.Logger;
 
 import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
 
 @APICommand(name = RevokeTemplateDirectDownloadCertificateCmd.APINAME,
-        description = "Revoke a certificate alias from a KVM host",
-        responseObject = SuccessResponse.class,
-        requestHasSensitiveInfo = true,
-        responseHasSensitiveInfo = true,
+        description = "Revoke a direct download certificate from hosts in a zone",
+        responseObject = DirectDownloadCertificateHostStatusResponse.class,
         since = "4.13",
         authorized = {RoleType.Admin})
 public class RevokeTemplateDirectDownloadCertificateCmd extends BaseCmd {
@@ -54,35 +60,63 @@ public class RevokeTemplateDirectDownloadCertificateCmd extends BaseCmd {
     private static final Logger LOG = Logger.getLogger(RevokeTemplateDirectDownloadCertificateCmd.class);
     public static final String APINAME = "revokeTemplateDirectDownloadCertificate";
 
-    @Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = true,
-            description = "alias of the SSL certificate")
+    @Parameter(name = ApiConstants.ID, type = CommandType.UUID,
+            entityType = DirectDownloadCertificateResponse.class,
+            description = "id of the certificate")
+    private Long certificateId;
+
+    @Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING,
+            description = "(optional) alias of the SSL certificate")
     private String certificateAlias;
 
-    @Parameter(name = ApiConstants.HYPERVISOR, type = BaseCmd.CommandType.STRING, required = true,
-            description = "hypervisor type")
+    @Parameter(name = ApiConstants.HYPERVISOR, type = BaseCmd.CommandType.STRING,
+            description = "(optional) hypervisor type")
     private String hypervisor;
 
     @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class,
-            description = "zone to revoke certificate", required = true)
+            description = "(optional) zone to revoke certificate", required = true)
     private Long zoneId;
 
     @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class,
             description = "(optional) the host ID to revoke certificate")
     private Long hostId;
 
-    @Override
-    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
-        if (!hypervisor.equalsIgnoreCase("kvm")) {
+    private void createResponse(final List<HostCertificateStatus> hostsRevokeStatusList) {
+        final ListResponse<DirectDownloadCertificateHostStatusResponse> response = new ListResponse<>();
+        final List<DirectDownloadCertificateHostStatusResponse> responses = new ArrayList<>();
+        for (final HostCertificateStatus status : hostsRevokeStatusList) {
+            if (status == null) {
+                continue;
+            }
+            DirectDownloadCertificateHostStatusResponse revokeResponse =
+                    _responseGenerator.createDirectDownloadCertificateHostStatusResponse(status);
+            responses.add(revokeResponse);
+        }
+        response.setResponses(responses);
+        response.setResponseName(getCommandName());
+        setResponseObject(response);
+    }
+
+    private void validateParameters() {
+        if (ObjectUtils.allNull(certificateId, certificateAlias, hypervisor) ||
+                certificateId == null && !ObjectUtils.allNotNull(certificateAlias, hypervisor)) {
+            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please specify the hypervisor and the" +
+                    "certificate name to revoke or the certificate ID");
+        }
+        if (StringUtils.isNotBlank(hypervisor) && !hypervisor.equalsIgnoreCase("kvm")) {
             throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Currently supporting KVM hosts only");
         }
-        SuccessResponse response = new SuccessResponse(getCommandName());
+    }
+
+    @Override
+    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+        validateParameters();
         try {
-            LOG.debug("Revoking certificate " + certificateAlias + " from " + hypervisor + " hosts");
-            boolean result = directDownloadManager.revokeCertificateAlias(certificateAlias, hypervisor, zoneId, hostId);
-            response.setSuccess(result);
-            setResponseObject(response);
+            DirectDownloadCertificate certificate = directDownloadManager.findDirectDownloadCertificateByIdOrHypervisorAndAlias(certificateId, certificateAlias, hypervisor, zoneId);
+            List<HostCertificateStatus> hostsResult = directDownloadManager.revokeCertificate(certificate, zoneId, hostId);
+            createResponse(hostsResult);
         } catch (Exception e) {
-            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
+            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed revoking certificate: " + e.getMessage());
         }
     }
 
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/UploadTemplateDirectDownloadCertificateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/UploadTemplateDirectDownloadCertificateCmd.java
index 223f20b5bb0..e403f11baac 100755
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/UploadTemplateDirectDownloadCertificateCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/UploadTemplateDirectDownloadCertificateCmd.java
@@ -16,6 +16,8 @@
 // under the License.
 package org.apache.cloudstack.api.command.admin.direct.download;
 
+import com.cloud.utils.Pair;
+import com.cloud.utils.exception.CloudRuntimeException;
 import org.apache.cloudstack.acl.RoleType;
 import org.apache.cloudstack.api.APICommand;
 import org.apache.cloudstack.api.ApiConstants;
@@ -23,20 +25,23 @@ import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.api.Parameter;
 import org.apache.cloudstack.api.ServerApiException;
 import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
 import org.apache.cloudstack.api.response.HostResponse;
-import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateHostStatusResponse;
 import org.apache.cloudstack.api.response.ZoneResponse;
 import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificate;
 import org.apache.cloudstack.direct.download.DirectDownloadManager;
+import org.apache.cloudstack.direct.download.DirectDownloadManager.HostCertificateStatus;
 import org.apache.log4j.Logger;
 
 import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
 
 @APICommand(name = UploadTemplateDirectDownloadCertificateCmd.APINAME,
         description = "Upload a certificate for HTTPS direct template download on KVM hosts",
-        responseObject = SuccessResponse.class,
-        requestHasSensitiveInfo = true,
-        responseHasSensitiveInfo = true,
+        responseObject = DirectDownloadCertificateResponse.class,
         since = "4.11.0",
         authorized = {RoleType.Admin})
 public class UploadTemplateDirectDownloadCertificateCmd extends BaseCmd {
@@ -63,9 +68,29 @@ public class UploadTemplateDirectDownloadCertificateCmd extends BaseCmd {
     private Long zoneId;
 
     @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class,
-            description = "(optional) the host ID to revoke certificate")
+            description = "(optional) the host ID to upload certificate")
     private Long hostId;
 
+    private void createResponse(DirectDownloadCertificate certificate, final List<HostCertificateStatus> hostStatusList) {
+        final List<DirectDownloadCertificateHostStatusResponse> hostMapsResponse = new ArrayList<>();
+        if (certificate == null) {
+            throw new CloudRuntimeException("Unable to upload certificate");
+        }
+        DirectDownloadCertificateResponse response = _responseGenerator.createDirectDownloadCertificateResponse(certificate);
+        for (final HostCertificateStatus status : hostStatusList) {
+            if (status == null) {
+                continue;
+            }
+            DirectDownloadCertificateHostStatusResponse uploadResponse =
+                    _responseGenerator.createDirectDownloadCertificateHostStatusResponse(status);
+            hostMapsResponse.add(uploadResponse);
+        }
+        response.setHostsMap(hostMapsResponse);
+        response.setResponseName(getCommandName());
+        response.setObjectName("uploadtemplatedirectdownloadcertificate");
+        setResponseObject(response);
+    }
+
     @Override
     public void execute() {
         if (!hypervisor.equalsIgnoreCase("kvm")) {
@@ -74,10 +99,11 @@ public class UploadTemplateDirectDownloadCertificateCmd extends BaseCmd {
 
         try {
             LOG.debug("Uploading certificate " + name + " to agents for Direct Download");
-            boolean result = directDownloadManager.uploadCertificateToHosts(certificate, name, hypervisor, zoneId, hostId);
-            SuccessResponse response = new SuccessResponse(getCommandName());
-            response.setSuccess(result);
-            setResponseObject(response);
+            Pair<DirectDownloadCertificate, List<HostCertificateStatus>> uploadStatus =
+                    directDownloadManager.uploadCertificateToHosts(certificate, name, hypervisor, zoneId, hostId);
+            DirectDownloadCertificate certificate = uploadStatus.first();
+            List<HostCertificateStatus> hostStatus = uploadStatus.second();
+            createResponse(certificate, hostStatus);
         } catch (Exception e) {
             throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
         }
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DirectDownloadCertificateHostStatusResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DirectDownloadCertificateHostStatusResponse.java
new file mode 100644
index 00000000000..cc9f2fc366a
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/DirectDownloadCertificateHostStatusResponse.java
@@ -0,0 +1,73 @@
+// 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.google.gson.annotations.SerializedName;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+
+public class DirectDownloadCertificateHostStatusResponse extends BaseResponse {
+
+    @SerializedName(ApiConstants.HOST_ID)
+    @Param(description = "the ID of the host")
+    private String hostId;
+
+    @SerializedName(ApiConstants.HOST_NAME)
+    @Param(description = "the name of the host")
+    private String hostName;
+
+    @SerializedName(ApiConstants.STATUS)
+    @Param(description = "indicates if the certificate has been revoked from the host, failed or skipped")
+    private String status;
+
+    @SerializedName(ApiConstants.DETAILS)
+    @Param(description = "indicates the details in case of failure or host skipped")
+    private String details;
+
+    public String getHostId() {
+        return hostId;
+    }
+
+    public void setHostId(String hostId) {
+        this.hostId = hostId;
+    }
+
+    public String getHostName() {
+        return hostName;
+    }
+
+    public void setHostName(String hostName) {
+        this.hostName = hostName;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public String getDetails() {
+        return details;
+    }
+
+    public void setDetails(String details) {
+        this.details = details;
+    }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DirectDownloadCertificateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DirectDownloadCertificateResponse.java
new file mode 100644
index 00000000000..f04cba812ba
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/DirectDownloadCertificateResponse.java
@@ -0,0 +1,162 @@
+// 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.google.gson.annotations.SerializedName;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificate;
+
+import java.util.List;
+
+@EntityReference(value = DirectDownloadCertificate.class)
+public class DirectDownloadCertificateResponse extends BaseResponse {
+
+    @SerializedName(ApiConstants.ID)
+    @Param(description = "the direct download certificate id")
+    private String id;
+
+    @SerializedName(ApiConstants.ALIAS)
+    @Param(description = "the direct download certificate alias")
+    private String alias;
+
+    @SerializedName(ApiConstants.ZONE_ID)
+    @Param(description = "the zone id where the certificate is uploaded")
+    private String zoneId;
+
+    @SerializedName(ApiConstants.ZONE_NAME)
+    @Param(description = "the zone name where the certificate is uploaded")
+    private String zoneName;
+
+    @SerializedName(ApiConstants.VERSION)
+    @Param(description = "the direct download certificate version")
+    private String version;
+
+    @SerializedName(ApiConstants.CERTIFICATE_SUBJECT)
+    @Param(description = "the direct download certificate subject")
+    private String subject;
+
+    @SerializedName(ApiConstants.CERTIFICATE_ISSUER)
+    @Param(description = "the direct download certificate issuer")
+    private String issuer;
+
+    @SerializedName(ApiConstants.CERTIFICATE_VALIDITY)
+    @Param(description = "the direct download certificate issuer")
+    private String validity;
+
+    @SerializedName(ApiConstants.CERTIFICATE_SERIALNUM)
+    @Param(description = "the direct download certificate serial num")
+    private String serialNum;
+
+    @SerializedName(ApiConstants.HYPERVISOR)
+    @Param(description = "the hypervisor of the hosts where the certificate is uploaded")
+    private String hypervisor;
+
+    @SerializedName(ApiConstants.HOSTS_MAP)
+    @Param(description = "the hosts where the certificate is uploaded to", responseObject = HostResponse.class)
+    private List<DirectDownloadCertificateHostStatusResponse> hostsMap;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+
+    public void setAlias(String alias) {
+        this.alias = alias;
+    }
+
+    public String getZoneId() {
+        return zoneId;
+    }
+
+    public void setZoneId(String zoneId) {
+        this.zoneId = zoneId;
+    }
+
+    public String getHypervisor() {
+        return hypervisor;
+    }
+
+    public void setHypervisor(String hypervisor) {
+        this.hypervisor = hypervisor;
+    }
+
+    public String getZoneName() {
+        return zoneName;
+    }
+
+    public void setZoneName(String zoneName) {
+        this.zoneName = zoneName;
+    }
+
+    public List<DirectDownloadCertificateHostStatusResponse> getHostsMap() {
+        return hostsMap;
+    }
+
+    public void setHostsMap(List<DirectDownloadCertificateHostStatusResponse> hosts) {
+        this.hostsMap = hosts;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getSubject() {
+        return subject;
+    }
+
+    public void setSubject(String subject) {
+        this.subject = subject;
+    }
+
+    public String getIssuer() {
+        return issuer;
+    }
+
+    public void setIssuer(String issuer) {
+        this.issuer = issuer;
+    }
+
+    public String getValidity() {
+        return validity;
+    }
+
+    public void setValidity(String validity) {
+        this.validity = validity;
+    }
+
+    public String getSerialNum() {
+        return serialNum;
+    }
+
+    public void setSerialNum(String serialNum) {
+        this.serialNum = serialNum;
+    }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java
index 6227c26ceab..9c70d84f76d 100644
--- a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java
+++ b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java
@@ -25,5 +25,6 @@ public interface DirectDownloadCertificate extends InternalIdentity, Identity {
     String getCertificate();
     String getAlias();
     Hypervisor.HypervisorType getHypervisorType();
+    Long getZoneId();
 
 }
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMap.java
similarity index 77%
copy from api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java
copy to api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMap.java
index 6227c26ceab..d48eb5564c9 100644
--- a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java
+++ b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMap.java
@@ -16,14 +16,11 @@
 // under the License.
 package org.apache.cloudstack.direct.download;
 
-import com.cloud.hypervisor.Hypervisor;
-import org.apache.cloudstack.api.Identity;
 import org.apache.cloudstack.api.InternalIdentity;
 
-public interface DirectDownloadCertificate extends InternalIdentity, Identity {
+public interface DirectDownloadCertificateHostMap extends InternalIdentity {
 
-    String getCertificate();
-    String getAlias();
-    Hypervisor.HypervisorType getHypervisorType();
-
-}
\ No newline at end of file
+    long getCertificateId();
+    long getHostId();
+    boolean isRevoked();
+}
diff --git a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
index f7dfae132a8..8a74965ceca 100644
--- a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
+++ b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
@@ -17,17 +17,21 @@
 
 package org.apache.cloudstack.direct.download;
 
+import com.cloud.host.Host;
+import com.cloud.utils.Pair;
 import org.apache.cloudstack.framework.agent.direct.download.DirectDownloadService;
 import org.apache.cloudstack.framework.config.ConfigKey;
 import org.apache.cloudstack.framework.config.Configurable;
 
 import com.cloud.utils.component.PluggableService;
 
+import java.util.List;
+
 public interface DirectDownloadManager extends DirectDownloadService, PluggableService, Configurable {
 
-    static final int DEFAULT_DIRECT_DOWNLOAD_CONNECT_TIMEOUT = 5000;
-    static final int DEFAULT_DIRECT_DOWNLOAD_SOCKET_TIMEOUT = 5000;
-    static final int DEFAULT_DIRECT_DOWNLOAD_CONNECTION_REQUEST_TIMEOUT = 5000;
+    int DEFAULT_DIRECT_DOWNLOAD_CONNECT_TIMEOUT = 5000;
+    int DEFAULT_DIRECT_DOWNLOAD_SOCKET_TIMEOUT = 5000;
+    int DEFAULT_DIRECT_DOWNLOAD_CONNECTION_REQUEST_TIMEOUT = 5000;
 
     ConfigKey<Long> DirectDownloadCertificateUploadInterval = new ConfigKey<>("Advanced", Long.class,
             "direct.download.certificate.background.task.interval",
@@ -37,26 +41,66 @@ public interface DirectDownloadManager extends DirectDownloadService, PluggableS
                     "Only certificates which have not been revoked from hosts are uploaded",
             false);
 
-    static final ConfigKey<Integer> DirectDownloadConnectTimeout = new ConfigKey<Integer>("Advanced", Integer.class,
+    ConfigKey<Integer> DirectDownloadConnectTimeout = new ConfigKey<Integer>("Advanced", Integer.class,
             "direct.download.connect.timeout",
             String.valueOf(DEFAULT_DIRECT_DOWNLOAD_CONNECT_TIMEOUT),
             "Connection establishment timeout in milliseconds for direct download",
             true);
 
-    static final ConfigKey<Integer> DirectDownloadSocketTimeout = new ConfigKey<Integer>("Advanced", Integer.class,
+    ConfigKey<Integer> DirectDownloadSocketTimeout = new ConfigKey<Integer>("Advanced", Integer.class,
             "direct.download.socket.timeout",
             String.valueOf(DEFAULT_DIRECT_DOWNLOAD_SOCKET_TIMEOUT),
             "Socket timeout (SO_TIMEOUT) in milliseconds for direct download",
             true);
 
-    static final ConfigKey<Integer> DirectDownloadConnectionRequestTimeout = new ConfigKey<Integer>("Hidden", Integer.class,
+    ConfigKey<Integer> DirectDownloadConnectionRequestTimeout = new ConfigKey<Integer>("Hidden", Integer.class,
             "direct.download.connection.request.timeout",
             String.valueOf(DEFAULT_DIRECT_DOWNLOAD_CONNECTION_REQUEST_TIMEOUT),
             "Requesting a connection from connection manager timeout in milliseconds for direct download",
             true);
 
+    class HostCertificateStatus {
+        public enum CertificateStatus {
+            REVOKED, FAILED, SKIPPED, UPLOADED
+        }
+
+        HostCertificateStatus(CertificateStatus status, Host host, String details) {
+            this.status = status;
+            this.host = host;
+            this.details = details;
+        }
+
+        private CertificateStatus status;
+        private Host host;
+        private String details;
+
+        public CertificateStatus getStatus() {
+            return status;
+        }
+
+        public Host getHost() {
+            return host;
+        }
+
+        public String getDetails() {
+            return details;
+        }
+    }
+
+    DirectDownloadCertificate findDirectDownloadCertificateByIdOrHypervisorAndAlias(Long id, String alias, String hypervisor, Long zoneId);
+
+    /**
+     * Revoke direct download certificate from the hosts in the zone or a specific host
+     */
+    List<HostCertificateStatus> revokeCertificate(DirectDownloadCertificate certificate, Long zoneId, Long hostId);
+
+    List<DirectDownloadCertificate> listDirectDownloadCertificates(Long certificateId, Long zoneId);
+
+    List<DirectDownloadCertificateHostMap> getCertificateHostsMapping(Long certificateId);
+
     /**
-     * Revoke direct download certificate with alias 'alias' from hosts of hypervisor type 'hypervisor'
+     * Upload client certificate to each running host
+     * @return
      */
-    boolean revokeCertificateAlias(String certificateAlias, String hypervisor, Long zoneId, Long hostId);
+    Pair<DirectDownloadCertificate, List<HostCertificateStatus>> uploadCertificateToHosts(String certificateCer, String certificateName, String hypervisor, Long zoneId, Long hostId);
 }
diff --git a/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java
index e119b1d491e..fa056f374c9 100644
--- a/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java
@@ -23,4 +23,5 @@ import java.util.List;
 public interface DirectDownloadCertificateHostMapDao extends GenericDao<DirectDownloadCertificateHostMapVO, Long> {
     DirectDownloadCertificateHostMapVO findByCertificateAndHost(long certificateId, long hostId);
     List<DirectDownloadCertificateHostMapVO> listByCertificateId(long certificateId);
+    List<DirectDownloadCertificateHostMapVO> listByCertificateIdAndRevoked(long certificateId, boolean revoked);
 }
\ No newline at end of file
diff --git a/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDaoImpl.java
index 7a0b732bbfd..de895cb6f98 100644
--- a/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDaoImpl.java
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDaoImpl.java
@@ -29,6 +29,7 @@ public class DirectDownloadCertificateHostMapDaoImpl extends GenericDaoBase<Dire
         mapSearchBuilder = createSearchBuilder();
         mapSearchBuilder.and("certificate_id", mapSearchBuilder.entity().getCertificateId(), SearchCriteria.Op.EQ);
         mapSearchBuilder.and("host_id", mapSearchBuilder.entity().getHostId(), SearchCriteria.Op.EQ);
+        mapSearchBuilder.and("revoked", mapSearchBuilder.entity().isRevoked(), SearchCriteria.Op.EQ);
         mapSearchBuilder.done();
     }
     @Override
@@ -45,4 +46,12 @@ public class DirectDownloadCertificateHostMapDaoImpl extends GenericDaoBase<Dire
         sc.setParameters("certificate_id", certificateId);
         return listBy(sc);
     }
+
+    @Override
+    public List<DirectDownloadCertificateHostMapVO> listByCertificateIdAndRevoked(long certificateId, boolean revoked) {
+        SearchCriteria<DirectDownloadCertificateHostMapVO> sc = mapSearchBuilder.create();
+        sc.setParameters("certificate_id", certificateId);
+        sc.setParameters("revoked", revoked);
+        return listBy(sc);
+    }
 }
\ No newline at end of file
diff --git a/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapVO.java b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapVO.java
index db5faf669ff..7bcfeb65860 100644
--- a/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapVO.java
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapVO.java
@@ -25,12 +25,12 @@ import javax.persistence.Table;
 
 @Entity
 @Table(name = "direct_download_certificate_host_map")
-public class DirectDownloadCertificateHostMapVO {
+public class DirectDownloadCertificateHostMapVO implements DirectDownloadCertificateHostMap {
 
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "id")
-    private Long id;
+    private long id;
 
     @Column(name = "host_id")
     private Long hostId;
@@ -50,15 +50,15 @@ public class DirectDownloadCertificateHostMapVO {
         this.revoked = false;
     }
 
-    public Long getId() {
+    public long getId() {
         return id;
     }
 
-    public void setId(Long id) {
+    public void setId(long id) {
         this.id = id;
     }
 
-    public Long getHostId() {
+    public long getHostId() {
         return hostId;
     }
 
@@ -66,7 +66,7 @@ public class DirectDownloadCertificateHostMapVO {
         this.hostId = hostId;
     }
 
-    public Long getCertificateId() {
+    public long getCertificateId() {
         return certificateId;
     }
 
@@ -74,8 +74,8 @@ public class DirectDownloadCertificateHostMapVO {
         this.certificateId = certificateId;
     }
 
-    public Boolean isRevoked() {
-        return revoked;
+    public boolean isRevoked() {
+        return revoked != null && revoked;
     }
 
     public void setRevoked(Boolean revoked) {
diff --git a/framework/direct-download/pom.xml b/framework/direct-download/pom.xml
index 03a37532e5b..e2ce16e27e4 100644
--- a/framework/direct-download/pom.xml
+++ b/framework/direct-download/pom.xml
@@ -21,6 +21,14 @@
     <modelVersion>4.0.0</modelVersion>
     <artifactId>cloud-framework-direct-download</artifactId>
     <name>Apache CloudStack Framework - Direct Download to Primary Storage</name>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.cloudstack</groupId>
+            <artifactId>cloud-utils</artifactId>
+            <version>4.17.0.0-SNAPSHOT</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
     <parent>
         <artifactId>cloudstack-framework</artifactId>
         <groupId>org.apache.cloudstack</groupId>
diff --git a/framework/direct-download/src/main/java/org/apache/cloudstack/framework/agent/direct/download/DirectDownloadService.java b/framework/direct-download/src/main/java/org/apache/cloudstack/framework/agent/direct/download/DirectDownloadService.java
index 983f935a2fa..6aeb16b5d85 100644
--- a/framework/direct-download/src/main/java/org/apache/cloudstack/framework/agent/direct/download/DirectDownloadService.java
+++ b/framework/direct-download/src/main/java/org/apache/cloudstack/framework/agent/direct/download/DirectDownloadService.java
@@ -17,6 +17,8 @@
 
 package org.apache.cloudstack.framework.agent.direct.download;
 
+import com.cloud.utils.Pair;
+
 public interface DirectDownloadService {
 
     /**
@@ -24,15 +26,10 @@ public interface DirectDownloadService {
      */
     void downloadTemplate(long templateId, long poolId, long hostId);
 
-    /**
-     * Upload client certificate to each running host
-     */
-    boolean uploadCertificateToHosts(String certificateCer, String certificateName, String hypervisor, Long zoneId, Long hostId);
-
     /**
      * Upload a stored certificate on database with id 'certificateId' to host with id 'hostId'
      */
-    boolean uploadCertificate(long certificateId, long hostId);
+    Pair<Boolean, String> provisionCertificate(long certificateId, long hostId);
 
     /**
      * Sync the stored certificates to host with id 'hostId'
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index e0cc8509d08..e8ac1628c64 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -18,6 +18,8 @@ package com.cloud.api;
 
 import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
 
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
 import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -34,6 +36,7 @@ import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
+import com.cloud.utils.security.CertificateHelper;
 import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.acl.ControlledEntity.ACLType;
 import org.apache.cloudstack.affinity.AffinityGroup;
@@ -67,6 +70,7 @@ import org.apache.cloudstack.api.response.ControlledViewEntityResponse;
 import org.apache.cloudstack.api.response.CounterResponse;
 import org.apache.cloudstack.api.response.CreateCmdResponse;
 import org.apache.cloudstack.api.response.CreateSSHKeyPairResponse;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
 import org.apache.cloudstack.api.response.DiskOfferingResponse;
 import org.apache.cloudstack.api.response.DomainResponse;
 import org.apache.cloudstack.api.response.DomainRouterResponse;
@@ -118,6 +122,7 @@ import org.apache.cloudstack.api.response.ResourceCountResponse;
 import org.apache.cloudstack.api.response.ResourceIconResponse;
 import org.apache.cloudstack.api.response.ResourceLimitResponse;
 import org.apache.cloudstack.api.response.ResourceTagResponse;
+import org.apache.cloudstack.api.response.DirectDownloadCertificateHostStatusResponse;
 import org.apache.cloudstack.api.response.RollingMaintenanceHostSkippedResponse;
 import org.apache.cloudstack.api.response.RollingMaintenanceHostUpdatedResponse;
 import org.apache.cloudstack.api.response.RollingMaintenanceResponse;
@@ -160,11 +165,15 @@ import org.apache.cloudstack.backup.BackupSchedule;
 import org.apache.cloudstack.backup.dao.BackupOfferingDao;
 import org.apache.cloudstack.config.Configuration;
 import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMap;
+import org.apache.cloudstack.direct.download.DirectDownloadManager;
+import org.apache.cloudstack.direct.download.DirectDownloadManager.HostCertificateStatus.CertificateStatus;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
+import org.apache.cloudstack.direct.download.DirectDownloadCertificate;
 import org.apache.cloudstack.framework.jobs.AsyncJob;
 import org.apache.cloudstack.framework.jobs.AsyncJobManager;
 import org.apache.cloudstack.management.ManagementServerHost;
@@ -363,6 +372,7 @@ import com.cloud.vm.snapshot.VMSnapshot;
 import com.cloud.vm.snapshot.VMSnapshotVO;
 import com.cloud.vm.snapshot.dao.VMSnapshotDao;
 import org.apache.commons.lang3.StringUtils;
+import sun.security.x509.X509CertImpl;
 
 public class ApiResponseHelper implements ResponseGenerator {
 
@@ -4547,4 +4557,82 @@ public class ApiResponseHelper implements ResponseGenerator {
     public ResourceIconResponse createResourceIconResponse(ResourceIcon resourceIcon) {
         return  ApiDBUtils.newResourceIconResponse(resourceIcon);
     }
+
+    protected void handleCertificateResponse(String certStr, DirectDownloadCertificateResponse response) {
+        try {
+            Certificate cert = CertificateHelper.buildCertificate(certStr);
+            if (cert instanceof X509CertImpl) {
+                X509CertImpl certificate = (X509CertImpl) cert;
+                response.setVersion(String.valueOf(certificate.getVersion()));
+                response.setSubject(certificate.getSubjectDN().toString());
+                response.setIssuer(certificate.getIssuerDN().toString());
+                response.setSerialNum(certificate.getSerialNumberObject().toString());
+                response.setValidity(String.format("From: [%s] - To: [%s]", certificate.getNotBefore(), certificate.getNotAfter()));
+            }
+        } catch (CertificateException e) {
+            s_logger.error("Error parsing direct download certificate: " + certStr, e);
+        }
+    }
+
+    @Override
+    public DirectDownloadCertificateResponse createDirectDownloadCertificateResponse(DirectDownloadCertificate certificate) {
+        DirectDownloadCertificateResponse response = new DirectDownloadCertificateResponse();
+        DataCenterVO datacenter = ApiDBUtils.findZoneById(certificate.getZoneId());
+        if (datacenter != null) {
+            response.setZoneId(datacenter.getUuid());
+            response.setZoneName(datacenter.getName());
+        }
+        response.setId(certificate.getUuid());
+        response.setAlias(certificate.getAlias());
+        handleCertificateResponse(certificate.getCertificate(), response);
+        response.setHypervisor(certificate.getHypervisorType().name());
+        response.setObjectName("directdownloadcertificate");
+        return response;
+    }
+
+    @Override
+    public List<DirectDownloadCertificateHostStatusResponse> createDirectDownloadCertificateHostMapResponse(List<DirectDownloadCertificateHostMap> hostMappings) {
+        if (CollectionUtils.isEmpty(hostMappings)) {
+            return new ArrayList<>();
+        }
+        List<DirectDownloadCertificateHostStatusResponse> responses = new ArrayList<>(hostMappings.size());
+        for (DirectDownloadCertificateHostMap map : hostMappings) {
+            DirectDownloadCertificateHostStatusResponse response = new DirectDownloadCertificateHostStatusResponse();
+            HostVO host = ApiDBUtils.findHostById(map.getHostId());
+            if (host != null) {
+                response.setHostId(host.getUuid());
+                response.setHostName(host.getName());
+            }
+            response.setStatus(map.isRevoked() ? CertificateStatus.REVOKED.name() : CertificateStatus.UPLOADED.name());
+            response.setObjectName("directdownloadcertificatehoststatus");
+            responses.add(response);
+        }
+        return responses;
+    }
+
+    private DirectDownloadCertificateHostStatusResponse getDirectDownloadHostStatusResponseInternal(Host host, CertificateStatus status, String details) {
+        DirectDownloadCertificateHostStatusResponse response = new DirectDownloadCertificateHostStatusResponse();
+        if (host != null) {
+            response.setHostId(host.getUuid());
+            response.setHostName(host.getName());
+        }
+        response.setStatus(status.name());
+        response.setDetails(details);
+        response.setObjectName("directdownloadcertificatehoststatus");
+        return response;
+    }
+
+    @Override
+    public DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateHostStatusResponse(DirectDownloadManager.HostCertificateStatus hostStatus) {
+        Host host = hostStatus.getHost();
+        CertificateStatus status = hostStatus.getStatus();
+        return getDirectDownloadHostStatusResponseInternal(host, status, hostStatus.getDetails());
+    }
+
+    @Override
+    public DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateProvisionResponse(Long certificateId, Long hostId, Pair<Boolean, String> result) {
+        HostVO host = ApiDBUtils.findHostById(hostId);
+        CertificateStatus status = result != null && result.first() ? CertificateStatus.UPLOADED : CertificateStatus.FAILED;
+        return getDirectDownloadHostStatusResponseInternal(host, status, result != null ? result.second() : "provision certificate failure");
+    }
 }
diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
index 4477eb85d8c..eae1dda4209 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -74,6 +74,8 @@ import org.apache.cloudstack.api.command.admin.config.ListHypervisorCapabilities
 import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd;
 import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
 import org.apache.cloudstack.api.command.admin.config.UpdateHypervisorCapabilitiesCmd;
+import org.apache.cloudstack.api.command.admin.direct.download.ListTemplateDirectDownloadCertificatesCmd;
+import org.apache.cloudstack.api.command.admin.direct.download.ProvisionTemplateDirectDownloadCertificateCmd;
 import org.apache.cloudstack.api.command.admin.direct.download.RevokeTemplateDirectDownloadCertificateCmd;
 import org.apache.cloudstack.api.command.admin.direct.download.UploadTemplateDirectDownloadCertificateCmd;
 import org.apache.cloudstack.api.command.admin.domain.CreateDomainCmd;
@@ -3547,6 +3549,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
         cmdList.add(DeleteManagementNetworkIpRangeCmd.class);
         cmdList.add(UploadTemplateDirectDownloadCertificateCmd.class);
         cmdList.add(RevokeTemplateDirectDownloadCertificateCmd.class);
+        cmdList.add(ListTemplateDirectDownloadCertificatesCmd.class);
+        cmdList.add(ProvisionTemplateDirectDownloadCertificateCmd.class);
         cmdList.add(ListMgmtsCmd.class);
         cmdList.add(GetUploadParamsForIsoCmd.class);
         cmdList.add(GetRouterHealthCheckResultsCmd.class);
diff --git a/server/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManagerImpl.java b/server/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManagerImpl.java
index 8efe8654026..242dc865b93 100644
--- a/server/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManagerImpl.java
@@ -19,6 +19,7 @@
 package org.apache.cloudstack.direct.download;
 
 import static com.cloud.storage.Storage.ImageFormat;
+import static org.apache.cloudstack.direct.download.DirectDownloadManager.HostCertificateStatus.CertificateStatus;
 
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -29,6 +30,7 @@ import java.security.cert.CertificateNotYetValidException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
@@ -39,6 +41,8 @@ import java.util.stream.Collectors;
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.utils.Pair;
 import org.apache.cloudstack.agent.directdownload.DirectDownloadAnswer;
 import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand;
 import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand.DownloadProtocol;
@@ -66,6 +70,7 @@ import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
 import org.apache.cloudstack.storage.to.TemplateObjectTO;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.log4j.Logger;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
@@ -448,7 +453,8 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
     }
 
     @Override
-    public boolean uploadCertificateToHosts(String certificateCer, String alias, String hypervisor, Long zoneId, Long hostId) {
+    public Pair<DirectDownloadCertificate, List<HostCertificateStatus>> uploadCertificateToHosts(
+            String certificateCer, String alias, String hypervisor, Long zoneId, Long hostId) {
         if (alias != null && (alias.equalsIgnoreCase("cloud") || alias.startsWith("cloudca"))) {
             throw new CloudRuntimeException("Please provide a different alias name for the certificate");
         }
@@ -457,6 +463,10 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
         DirectDownloadCertificateVO certificateVO;
         HypervisorType hypervisorType = HypervisorType.getType(hypervisor);
 
+        if (hypervisorType != HypervisorType.KVM) {
+            throw new CloudRuntimeException("Direct download certificates only supported on KVM");
+        }
+
         if (hostId == null) {
             hosts = getRunningHostsToUploadCertificate(zoneId, hypervisorType);
 
@@ -475,48 +485,55 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
             certificateVO = directDownloadCertificateDao.findByAlias(alias, hypervisorType, zoneId);
             if (certificateVO == null) {
                 s_logger.info("Certificate must be uploaded on zone " + zoneId);
-                return false;
+                return new Pair<>(certificateVO, new ArrayList<>());
             }
         }
 
         s_logger.info("Attempting to upload certificate: " + alias + " to " + hosts.size() + " hosts on zone " + zoneId);
-        int hostCount = 0;
+        int success = 0;
+        int failed = 0;
+        List<HostCertificateStatus> results = new ArrayList<>();
         if (CollectionUtils.isNotEmpty(hosts)) {
             for (HostVO host : hosts) {
-                if (!uploadCertificate(certificateVO.getId(), host.getId())) {
-                    String msg = "Could not upload certificate " + alias + " on host: " + host.getName() + " (" + host.getUuid() + ")";
+                if (host == null) {
+                    continue;
+                }
+                HostCertificateStatus hostStatus;
+                Pair<Boolean, String> result = provisionCertificate(certificateVO.getId(), host.getId());
+                if (!result.first()) {
+                    String msg = "Could not upload certificate " + alias + " on host: " + host.getName() + " (" + host.getUuid() + "): " + result.second();
                     s_logger.error(msg);
-                    throw new CloudRuntimeException(msg);
+                    failed++;
+                    hostStatus = new HostCertificateStatus(CertificateStatus.FAILED, host, result.second());
+                } else {
+                    success++;
+                    hostStatus = new HostCertificateStatus(CertificateStatus.UPLOADED, host, "");
                 }
-                hostCount++;
+                results.add(hostStatus);
             }
         }
-        s_logger.info("Certificate was successfully uploaded to " + hostCount + " hosts");
-        return true;
+        s_logger.info("Certificate was successfully uploaded to " + success + " hosts, " + failed + " failed");
+        return new Pair<>(certificateVO, results);
     }
 
-    /**
-     * Upload and import certificate to hostId on keystore
-     */
-    public boolean uploadCertificate(long certificateId, long hostId) {
-        DirectDownloadCertificateVO certificateVO = directDownloadCertificateDao.findById(certificateId);
-        if (certificateVO == null) {
-            throw new CloudRuntimeException("Could not find certificate with id " + certificateId + " to upload to host: " + hostId);
-        }
-
-        String certificate = certificateVO.getCertificate();
-        String alias = certificateVO.getAlias();
+    private Pair<Boolean, String> setupCertificateOnHost(DirectDownloadCertificate certificate, long hostId) {
+        String certificateStr = certificate.getCertificate();
+        String alias = certificate.getAlias();
+        long certificateId = certificate.getId();
 
-        s_logger.debug("Uploading certificate: " + certificateVO.getAlias() + " to host " + hostId);
-        SetupDirectDownloadCertificateCommand cmd = new SetupDirectDownloadCertificateCommand(certificate, alias);
+        s_logger.debug("Uploading certificate: " + alias + " to host " + hostId);
+        SetupDirectDownloadCertificateCommand cmd = new SetupDirectDownloadCertificateCommand(certificateStr, alias);
         Answer answer = agentManager.easySend(hostId, cmd);
+        Pair<Boolean, String> result;
         if (answer == null || !answer.getResult()) {
             String msg = "Certificate " + alias + " could not be added to host " + hostId;
             if (answer != null) {
                 msg += " due to: " + answer.getDetails();
             }
             s_logger.error(msg);
-            return false;
+            result = new Pair<>(false, msg);
+        } else {
+            result = new Pair<>(true, "OK");
         }
 
         s_logger.info("Certificate " + alias + " successfully uploaded to host: " + hostId);
@@ -528,8 +545,25 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
             DirectDownloadCertificateHostMapVO mapVO = new DirectDownloadCertificateHostMapVO(certificateId, hostId);
             directDownloadCertificateHostMapDao.persist(mapVO);
         }
+        return result;
+    }
+    /**
+     * Upload and import certificate to hostId on keystore
+     */
+    public Pair<Boolean, String> provisionCertificate(long certificateId, long hostId) {
+        DirectDownloadCertificateVO certificateVO = directDownloadCertificateDao.findById(certificateId);
+        if (certificateVO == null) {
+            throw new CloudRuntimeException("Could not find certificate with id " + certificateId + " to upload to host: " + hostId);
+        }
+        HostVO host = hostDao.findById(hostId);
+        if (host == null) {
+            throw new CloudRuntimeException("Cannot find a host with ID " + hostId);
+        }
+        if (host.getHypervisorType() != HypervisorType.KVM) {
+            throw new CloudRuntimeException("Cannot provision certificate to host " + host.getName() + " since it is not KVM");
+        }
 
-        return true;
+        return setupCertificateOnHost(certificateVO, hostId);
     }
 
     @Override
@@ -549,8 +583,9 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
             DirectDownloadCertificateHostMapVO mapping = directDownloadCertificateHostMapDao.findByCertificateAndHost(certificateVO.getId(), hostId);
             if (mapping == null) {
                 s_logger.debug("Syncing certificate " + certificateVO.getId() + " (" + certificateVO.getAlias() + ") on host: " + hostId + ", uploading it");
-                if (!uploadCertificate(certificateVO.getId(), hostId)) {
-                    String msg = "Could not sync certificate " + certificateVO.getId() + " (" + certificateVO.getAlias() + ") on host: " + hostId + ", upload failed";
+                Pair<Boolean, String> result = provisionCertificate(certificateVO.getId(), hostId);
+                if (!result.first()) {
+                    String msg = "Could not sync certificate " + certificateVO.getId() + " (" + certificateVO.getAlias() + ") on host: " + hostId + ", upload failed: " + result.second();
                     s_logger.error(msg);
                     syncCertificatesResult = false;
                 } else {
@@ -565,52 +600,127 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
         return syncCertificatesResult;
     }
 
-    @Override
-    public boolean revokeCertificateAlias(String certificateAlias, String hypervisor, Long zoneId, Long hostId) {
-        HypervisorType hypervisorType = HypervisorType.getType(hypervisor);
-        DirectDownloadCertificateVO certificateVO = directDownloadCertificateDao.findByAlias(certificateAlias, hypervisorType, zoneId);
-        if (certificateVO == null) {
-            throw new CloudRuntimeException("Certificate alias " + certificateAlias + " does not exist");
-        }
-
-        List<DirectDownloadCertificateHostMapVO> maps = null;
+    private List<DirectDownloadCertificateHostMapVO> getCertificateHostMappings(DirectDownloadCertificate certificate, Long hostId) {
+        List<DirectDownloadCertificateHostMapVO> maps;
         if (hostId == null) {
-             maps = directDownloadCertificateHostMapDao.listByCertificateId(certificateVO.getId());
+            maps = directDownloadCertificateHostMapDao.listByCertificateIdAndRevoked(certificate.getId(), false);
         } else {
-            DirectDownloadCertificateHostMapVO hostMap = directDownloadCertificateHostMapDao.findByCertificateAndHost(certificateVO.getId(), hostId);
+            DirectDownloadCertificateHostMapVO hostMap = directDownloadCertificateHostMapDao.findByCertificateAndHost(certificate.getId(), hostId);
             if (hostMap == null) {
-                s_logger.info("Certificate " + certificateAlias + " cannot be revoked from host " + hostId + " as it is not available on the host");
-                return false;
+                String msg = "Certificate " + certificate.getAlias() + " cannot be revoked from host " + hostId + " as it is not available on the host";
+                s_logger.error(msg);
+                throw new CloudRuntimeException(msg);
+            } else if (hostMap.isRevoked()) {
+                s_logger.debug("Certificate " + certificate.getAlias() + " was already revoked from host " + hostId + " skipping it");
+                return new LinkedList<>();
             }
             maps = Collections.singletonList(hostMap);
         }
+        return maps;
+    }
+
+    @Override
+    public DirectDownloadCertificate findDirectDownloadCertificateByIdOrHypervisorAndAlias(Long id, String alias, String hypervisor, Long zoneId) {
+        DirectDownloadCertificateVO certificateVO;
+        if (id != null) {
+            certificateVO = directDownloadCertificateDao.findById(id);
+        } else if (StringUtils.isNotBlank(alias) && StringUtils.isNotBlank(hypervisor)) {
+            certificateVO = directDownloadCertificateDao.findByAlias(alias, HypervisorType.getType(hypervisor), zoneId);
+        } else {
+            throw new CloudRuntimeException("Please provide a hypervisor and certificate alias or certificate ID");
+        }
+        if (certificateVO == null) {
+            throw new CloudRuntimeException("Could not find certificate " +
+                    (id != null ? "with ID " + id : "with alias " + alias + " and hypervisor " + hypervisor));
+        }
+        return certificateVO;
+    }
+
+    @Override
+    public List<HostCertificateStatus> revokeCertificate(DirectDownloadCertificate certificate, Long zoneId, Long hostId) {
+        String certificateAlias = certificate.getAlias();
+        if (!certificate.getZoneId().equals(zoneId)) {
+            throw new CloudRuntimeException("The certificate with alias " + certificateAlias + " was uploaded " +
+                    " to the zone with ID=" + certificate.getZoneId() + " instead of the zone with ID=" + zoneId);
+        }
 
+        List<HostCertificateStatus> hostsList = new ArrayList<>();
+        List<DirectDownloadCertificateHostMapVO> maps = getCertificateHostMappings(certificate, hostId);
+        if (CollectionUtils.isEmpty(maps)) {
+            return hostsList;
+        }
+
+        int success = 0;
+        int failed = 0;
+        int skipped = 0;
         s_logger.info("Attempting to revoke certificate alias: " + certificateAlias + " from " + maps.size() + " hosts");
-        if (CollectionUtils.isNotEmpty(maps)) {
-            for (DirectDownloadCertificateHostMapVO map : maps) {
-                Long mappingHostId = map.getHostId();
-                if (!revokeCertificateAliasFromHost(certificateAlias, mappingHostId)) {
-                    String msg = "Could not revoke certificate from host: " + mappingHostId;
-                    s_logger.error(msg);
-                    throw new CloudRuntimeException(msg);
+        for (DirectDownloadCertificateHostMapVO map : maps) {
+            Long mappingHostId = map.getHostId();
+            HostVO host = hostDao.findById(mappingHostId);
+            HostCertificateStatus hostStatus;
+            if (host == null || host.getDataCenterId() != zoneId || host.getHypervisorType() != HypervisorType.KVM) {
+                if (host != null) {
+                    String reason = host.getDataCenterId() != zoneId ? "Host is not in the zone " + zoneId : "Host hypervisor is not KVM";
+                    s_logger.debug("Skipping host " + host.getName() + ": " + reason);
+                    hostStatus = new HostCertificateStatus(CertificateStatus.SKIPPED, host, reason);
+                    hostsList.add(hostStatus);
                 }
+                skipped++;
+                continue;
+            }
+            Pair<Boolean, String> result = revokeCertificateAliasFromHost(certificateAlias, mappingHostId);
+            if (!result.first()) {
+                String msg = "Could not revoke certificate from host: " + mappingHostId + ": " + result.second();
+                s_logger.error(msg);
+                hostStatus = new HostCertificateStatus(CertificateStatus.FAILED, host, result.second());
+                failed++;
+            } else {
                 s_logger.info("Certificate " + certificateAlias + " revoked from host " + mappingHostId);
                 map.setRevoked(true);
+                hostStatus = new HostCertificateStatus(CertificateStatus.REVOKED, host, null);
+                success++;
                 directDownloadCertificateHostMapDao.update(map.getId(), map);
             }
+            hostsList.add(hostStatus);
         }
-        return true;
+        s_logger.info(String.format("Certificate alias %s revoked from: %d hosts, %d failed, %d skipped",
+                certificateAlias, success, failed, skipped));
+        return hostsList;
+    }
+
+    @Override
+    public List<DirectDownloadCertificate> listDirectDownloadCertificates(Long certificateId, Long zoneId) {
+        if (zoneId != null && dataCenterDao.findById(zoneId) == null) {
+            throw new InvalidParameterValueException("Cannot find a zone with ID = " + zoneId);
+        }
+        List<DirectDownloadCertificate> certificates = new LinkedList<>();
+        if (certificateId != null) {
+            certificates.add(directDownloadCertificateDao.findById(certificateId));
+        } else if (zoneId != null) {
+            certificates.addAll(directDownloadCertificateDao.listByZone(zoneId));
+        } else {
+            certificates.addAll(directDownloadCertificateDao.listAll());
+        }
+        return certificates;
+    }
+
+    @Override
+    public List<DirectDownloadCertificateHostMap> getCertificateHostsMapping(Long certificateId) {
+        if (certificateId == null) {
+            throw new InvalidParameterValueException("Please specify a certificate ID");
+        }
+        return new LinkedList<>(directDownloadCertificateHostMapDao.listByCertificateId(certificateId));
     }
 
-    protected boolean revokeCertificateAliasFromHost(String alias, Long hostId) {
+    protected Pair<Boolean, String> revokeCertificateAliasFromHost(String alias, Long hostId) {
         RevokeDirectDownloadCertificateCommand cmd = new RevokeDirectDownloadCertificateCommand(alias);
         try {
             Answer answer = agentManager.send(hostId, cmd);
-            return answer != null && answer.getResult();
+            return new Pair<>(answer != null && answer.getResult(), answer != null ? answer.getDetails() : "");
         } catch (AgentUnavailableException | OperationTimedoutException e) {
             s_logger.error("Error revoking certificate " + alias + " from host " + hostId, e);
+            return new Pair<>(false, e.getMessage());
         }
-        return false;
     }
 
     @Override
@@ -692,10 +802,13 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
                                         s_logger.debug("Certificate " + certificateVO.getId() +
                                                 " (" + certificateVO.getAlias() + ") was not uploaded to host: " + hostVO.getId() +
                                                 " uploading it");
-                                        boolean result = directDownloadManager.uploadCertificate(certificateVO.getId(), hostVO.getId());
+                                        Pair<Boolean, String> result = directDownloadManager.provisionCertificate(certificateVO.getId(), hostVO.getId());
                                         s_logger.debug("Certificate " + certificateVO.getAlias() + " " +
-                                                (result ? "uploaded" : "could not be uploaded") +
+                                                (result.first() ? "uploaded" : "could not be uploaded") +
                                                 " to host " + hostVO.getId());
+                                        if (!result.first()) {
+                                            s_logger.error("Certificate " + certificateVO.getAlias() + " failed: " + result.second());
+                                        }
                                     }
                                 }
                             }
diff --git a/server/src/test/java/com/cloud/api/ApiResponseHelperTest.java b/server/src/test/java/com/cloud/api/ApiResponseHelperTest.java
index 69ac86de876..6dc96cfee1f 100644
--- a/server/src/test/java/com/cloud/api/ApiResponseHelperTest.java
+++ b/server/src/test/java/com/cloud/api/ApiResponseHelperTest.java
@@ -27,6 +27,7 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.TimeZone;
 
+import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
 import org.apache.cloudstack.api.response.NicSecondaryIpResponse;
 import org.apache.cloudstack.api.response.UsageRecordResponse;
 import org.apache.cloudstack.usage.UsageService;
@@ -141,4 +142,46 @@ public class ApiResponseHelperTest {
         assertTrue(response.getIpAddr().equals("ipv6"));
     }
 
+    @Test
+    public void testHandleCertificateResponse() {
+        String certStr = "-----BEGIN CERTIFICATE-----\n" +
+                "MIIGLTCCBRWgAwIBAgIQOHZRhOAYLowYNcopBvxCdjANBgkqhkiG9w0BAQsFADCB\n" +
+                "jzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\n" +
+                "A1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQD\n" +
+                "Ey5TZWN0aWdvIFJTQSBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENB\n" +
+                "MB4XDTIxMDYxNTAwMDAwMFoXDTIyMDcxNjIzNTk1OVowFzEVMBMGA1UEAwwMKi5h\n" +
+                "cGFjaGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4UoHCmK5\n" +
+                "XdbyZ++d2BGuX35zZcESvr4K1Hw7ZTbyzMC+uokBKJcng1Hf5ctjUFKCoz7AlWRq\n" +
+                "JH5U3vU0y515C0aEE+j0lUHlxMGQD2ut+sJ6BZqcTBl5d8ns1TSckEH31DBDN3Fw\n" +
+                "uMLqEWBOjwt1MMT3Z+kR7ekuheJYbYHbJ2VtnKQd4jHmLly+/p+UqaQ6dIvQxq82\n" +
+                "ggZIUNWjGKwXS2vKl6O9EDu/QaAX9e059pf3UxAxGtJjeKXWJvt1e96T53+2+kXp\n" +
+                "j0/PuyT6F0o+grY08tCJnw7kTB4sE2qfALdwSblvyjBDOYtS4Xj5nycMpd+4Qse4\n" +
+                "2+irNBdZ63pqqQIDAQABo4IC+jCCAvYwHwYDVR0jBBgwFoAUjYxexFStiuF36Zv5\n" +
+                "mwXhuAGNYeEwHQYDVR0OBBYEFH+9CNXAwWW4+jyizee51r8x4ofHMA4GA1UdDwEB\n" +
+                "/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\n" +
+                "BQcDAjBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgIHMCUwIwYIKwYBBQUHAgEWF2h0\n" +
+                "dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBhAYIKwYBBQUHAQEEeDB2\n" +
+                "ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29SU0FE\n" +
+                "b21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0MCMGCCsGAQUFBzABhhdo\n" +
+                "dHRwOi8vb2NzcC5zZWN0aWdvLmNvbTAjBgNVHREEHDAaggwqLmFwYWNoZS5vcmeC\n" +
+                "CmFwYWNoZS5vcmcwggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB2AEalVet1+pEg\n" +
+                "MLWiiWn0830RLEF0vv1JuIWr8vxw/m1HAAABehHLqfgAAAQDAEcwRQIgINH3CquJ\n" +
+                "zTAprwjdo2cEWkMzpaNoP1SOI4xGl68PF2oCIQC77eD7K6Smx4Fv/z/sTKk21Psb\n" +
+                "ZhmVq5YoqhwRKuMgVAB2AEHIyrHfIkZKEMahOglCh15OMYsbA+vrS8do8JBilgb2\n" +
+                "AAABehHLqcEAAAQDAEcwRQIhANh++zJa9AE4U0DsHIFq6bW40b1OfGfH8uUdmjEZ\n" +
+                "s1jzAiBIRtJeFVmobSnbFKlOr8BGfD2L/hg1rkAgJlKY5oFShgB2ACl5vvCeOTkh\n" +
+                "8FZzn2Old+W+V32cYAr4+U1dJlwlXceEAAABehHLqZ4AAAQDAEcwRQIhAOZDfvU8\n" +
+                "Hz80I6Iyj2rv8+yWBVq1XVixI8bMykdCO6ADAiAWj8cJ9g1zxko4dJu8ouJf+Pwl\n" +
+                "0bbhhuJHhy/f5kiaszANBgkqhkiG9w0BAQsFAAOCAQEAlkdB7FZtVQz39TDNKR4u\n" +
+                "I8VQsTH5n4Kg+zVc0pptI7HGUWtp5PjBAEsvJ/G/NQXsjVflQaNPRRd7KNZycZL1\n" +
+                "jls6GdVoWVno6O5aLS7cCnb0tTlb8srhb9vdLZkSoCVCZLVjik5s2TLfpLsBKrTP\n" +
+                "leVY3n9TBZH+vyKLHt4WHR23Z+74xDsuXunoPGXQVV8ymqTtfohaoM19jP99vjY7\n" +
+                "DL/289XjMSfyPFqlpU4JDM7lY/kJSKB/C4eQglT8Sgm0h/kj5hdT2uMJBIQZIJVv\n" +
+                "241fAVUPgrYAESOMm2TVA9r1OzeoUNlKw+e3+vjTR6sfDDp/iRKcEVQX4u9+CxZp\n" +
+                "9g==\n-----END CERTIFICATE-----";
+        DirectDownloadCertificateResponse response = new DirectDownloadCertificateResponse();
+        helper.handleCertificateResponse(certStr, response);
+        assertEquals("3", response.getVersion());
+        assertEquals("CN=*.apache.org", response.getSubject());
+    }
 }
diff --git a/test/integration/smoke/test_direct_download.py b/test/integration/smoke/test_direct_download.py
index b894f08934b..6570bb9f0b3 100644
--- a/test/integration/smoke/test_direct_download.py
+++ b/test/integration/smoke/test_direct_download.py
@@ -18,7 +18,7 @@
 """
 # Import Local Modules
 from marvin.cloudstackTestCase import cloudstackTestCase
-from marvin.lib.utils import (cleanup_resources)
+from marvin.lib.utils import (cleanup_resources, validateList)
 from marvin.lib.base import (ServiceOffering,
                              NetworkOffering,
                              Network,
@@ -28,7 +28,8 @@ from marvin.lib.base import (ServiceOffering,
 from marvin.lib.common import (get_pod,
                                get_zone)
 from nose.plugins.attrib import attr
-from marvin.cloudstackAPI import (uploadTemplateDirectDownloadCertificate, revokeTemplateDirectDownloadCertificate)
+from marvin.cloudstackAPI import (uploadTemplateDirectDownloadCertificate, revokeTemplateDirectDownloadCertificate,
+                                  listTemplateDirectDownloadCertificates)
 from marvin.lib.decoratorGenerators import skipTestIf
 import uuid
 
@@ -136,9 +137,13 @@ class TestUploadDirectDownloadCertificates(cloudstackTestCase):
         except Exception as e:
             self.fail("Valid certificate must be uploaded")
 
+        cmd = listTemplateDirectDownloadCertificates.listTemplateDirectDownloadCertificatesCmd()
+        certs = self.apiclient.listTemplateDirectDownloadCertificates(cmd)
+        validateList(certs)
+        cert = certs[0]
+
         revokecmd = revokeTemplateDirectDownloadCertificate.revokeTemplateDirectDownloadCertificateCmd()
-        revokecmd.hypervisor = self.hypervisor
-        revokecmd.name = cmd.name
+        revokecmd.id = cert.id
         revokecmd.zoneid = self.zone.id
 
         try:
@@ -149,6 +154,7 @@ class TestUploadDirectDownloadCertificates(cloudstackTestCase):
         return
 
 
+
 class TestDirectDownloadTemplates(cloudstackTestCase):
 
     @classmethod