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 2019/07/18 05:39:13 UTC

[cloudstack] branch master updated: KVM: Enhancements for direct download feature (#3374)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new a1a9fb8  KVM: Enhancements for direct download feature (#3374)
a1a9fb8 is described below

commit a1a9fb8977532f9d43c78b785d0bf3e9c53fc66e
Author: Nicolas Vazquez <ni...@gmail.com>
AuthorDate: Thu Jul 18 02:39:00 2019 -0300

    KVM: Enhancements for direct download feature (#3374)
    
    * Add revoke certificates API
    
    * Add background task to sync certificates
    
    * Fix marvin test and revoke certificate
    
    * Fix certificate sent to hypervisor was missing headers
    
    * Fix background task for uploading certificates to hosts
---
 ...evokeTemplateDirectDownloadCertificateCmd.java} | 185 ++++++++--------
 ...UploadTemplateDirectDownloadCertificateCmd.java |  12 +-
 ...Manager.java => DirectDownloadCertificate.java} |  14 +-
 .../direct/download/DirectDownloadManager.java     |  16 +-
 .../RevokeDirectDownloadCertificateCommand.java    |  30 ++-
 .../src/main/java/com/cloud/host/dao/HostDao.java  |   2 +
 .../main/java/com/cloud/host/dao/HostDaoImpl.java  |  10 +
 .../download/DirectDownloadCertificateDao.java     |  12 +-
 .../download/DirectDownloadCertificateDaoImpl.java |  53 +++++
 .../DirectDownloadCertificateHostMapDao.java       |  11 +-
 .../DirectDownloadCertificateHostMapDaoImpl.java   |  48 +++++
 .../DirectDownloadCertificateHostMapVO.java        |  84 ++++++++
 .../download/DirectDownloadCertificateVO.java      | 119 +++++++++++
 .../spring-engine-schema-core-daos-context.xml     |   2 +
 .../resources/META-INF/db/schema-41200to41300.sql  |  27 +++
 .../direct/download/DirectDownloadService.java     |   7 +-
 ...virtRevokeDirectDownloadCertificateWrapper.java | 106 ++++++++++
 .../com/cloud/server/ManagementServerImpl.java     |   2 +
 .../direct/download/DirectDownloadManagerImpl.java | 232 +++++++++++++++++++--
 test/integration/smoke/test_direct_download.py     |  15 +-
 20 files changed, 853 insertions(+), 134 deletions(-)

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/RevokeTemplateDirectDownloadCertificateCmd.java
old mode 100755
new mode 100644
similarity index 56%
copy from api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/UploadTemplateDirectDownloadCertificateCmd.java
copy to api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java
index c93fca2..ef9fa8b
--- 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/RevokeTemplateDirectDownloadCertificateCmd.java
@@ -1,87 +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.admin.direct.download;
-
-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.Parameter;
-import org.apache.cloudstack.api.ServerApiException;
-import org.apache.cloudstack.api.ApiErrorCode;
-import org.apache.cloudstack.api.response.SuccessResponse;
-import org.apache.cloudstack.context.CallContext;
-import org.apache.cloudstack.direct.download.DirectDownloadManager;
-import org.apache.log4j.Logger;
-
-import javax.inject.Inject;
-
-@APICommand(name = UploadTemplateDirectDownloadCertificateCmd.APINAME,
-        description = "Upload a certificate for HTTPS direct template download on KVM hosts",
-        responseObject = SuccessResponse.class,
-        requestHasSensitiveInfo = true,
-        responseHasSensitiveInfo = true,
-        since = "4.11.0",
-        authorized = {RoleType.Admin})
-public class UploadTemplateDirectDownloadCertificateCmd extends BaseCmd {
-
-    @Inject
-    DirectDownloadManager directDownloadManager;
-
-    private static final Logger LOG = Logger.getLogger(UploadTemplateDirectDownloadCertificateCmd.class);
-    public static final String APINAME = "uploadTemplateDirectDownloadCertificate";
-
-    @Parameter(name = ApiConstants.CERTIFICATE, type = BaseCmd.CommandType.STRING, required = true, length = 65535,
-            description = "SSL certificate")
-    private String certificate;
-
-    @Parameter(name = ApiConstants.NAME , type = BaseCmd.CommandType.STRING, required = true,
-            description = "Name for the uploaded certificate")
-    private String name;
-
-    @Parameter(name = ApiConstants.HYPERVISOR, type = BaseCmd.CommandType.STRING, required = true, description = "Hypervisor type")
-    private String hypervisor;
-
-    @Override
-    public void execute() {
-        if (!hypervisor.equalsIgnoreCase("kvm")) {
-            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Currently supporting KVM hosts only");
-        }
-
-        try {
-            LOG.debug("Uploading certificate " + name + " to agents for Direct Download");
-            boolean result = directDownloadManager.uploadCertificateToHosts(certificate, name, hypervisor);
-            SuccessResponse response = new SuccessResponse(getCommandName());
-            response.setSuccess(result);
-            setResponseObject(response);
-        } catch (Exception e) {
-            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
-        }
-    }
-
-    @Override
-    public String getCommandName() {
-        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
-    }
-
-    @Override
-    public long getEntityOwnerId() {
-        return CallContext.current().getCallingAccount().getId();
-    }
-}
-
-
+//
+// 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.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.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",
+        authorized = {RoleType.Admin})
+public class RevokeTemplateDirectDownloadCertificateCmd extends BaseCmd {
+
+    @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.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")) {
+            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());
+        }
+    }
+
+    @Override
+    public String getCommandName() {
+        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+    }
+
+    @Override
+    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/UploadTemplateDirectDownloadCertificateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/UploadTemplateDirectDownloadCertificateCmd.java
index c93fca2..223f20b 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
@@ -23,7 +23,9 @@ 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.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;
@@ -56,6 +58,14 @@ public class UploadTemplateDirectDownloadCertificateCmd extends BaseCmd {
     @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 upload 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() {
         if (!hypervisor.equalsIgnoreCase("kvm")) {
@@ -64,7 +74,7 @@ public class UploadTemplateDirectDownloadCertificateCmd extends BaseCmd {
 
         try {
             LOG.debug("Uploading certificate " + name + " to agents for Direct Download");
-            boolean result = directDownloadManager.uploadCertificateToHosts(certificate, name, hypervisor);
+            boolean result = directDownloadManager.uploadCertificateToHosts(certificate, name, hypervisor, zoneId, hostId);
             SuccessResponse response = new SuccessResponse(getCommandName());
             response.setSuccess(result);
             setResponseObject(response);
diff --git a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java
similarity index 72%
copy from api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
copy to api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java
index b3f0841..6227c26 100644
--- a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
+++ b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java
@@ -14,12 +14,16 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-
 package org.apache.cloudstack.direct.download;
 
-import com.cloud.utils.component.PluggableService;
-import org.apache.cloudstack.framework.agent.direct.download.DirectDownloadService;
+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 DirectDownloadManager extends DirectDownloadService, PluggableService {
+    String getCertificate();
+    String getAlias();
+    Hypervisor.HypervisorType getHypervisorType();
 
-}
+}
\ No newline at end of file
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 b3f0841..d627ffa 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
@@ -19,7 +19,21 @@ package org.apache.cloudstack.direct.download;
 
 import com.cloud.utils.component.PluggableService;
 import org.apache.cloudstack.framework.agent.direct.download.DirectDownloadService;
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.framework.config.Configurable;
 
-public interface DirectDownloadManager extends DirectDownloadService, PluggableService {
+public interface DirectDownloadManager extends DirectDownloadService, PluggableService, Configurable {
 
+    ConfigKey<Long> DirectDownloadCertificateUploadInterval = new ConfigKey<>("Advanced", Long.class,
+            "direct.download.certificate.background.task.interval",
+            "0",
+            "This interval (in hours) controls a background task to sync hosts within enabled zones " +
+                    "missing uploaded certificates for direct download." +
+                    "Only certificates which have not been revoked from hosts are uploaded",
+            false);
+
+    /**
+     * Revoke direct download certificate with alias 'alias' from hosts of hypervisor type 'hypervisor'
+     */
+    boolean revokeCertificateAlias(String certificateAlias, String hypervisor, Long zoneId, Long hostId);
 }
diff --git a/framework/direct-download/src/main/java/org/apache/cloudstack/framework/agent/direct/download/DirectDownloadService.java b/core/src/main/java/org/apache/cloudstack/agent/directdownload/RevokeDirectDownloadCertificateCommand.java
similarity index 62%
copy from framework/direct-download/src/main/java/org/apache/cloudstack/framework/agent/direct/download/DirectDownloadService.java
copy to core/src/main/java/org/apache/cloudstack/agent/directdownload/RevokeDirectDownloadCertificateCommand.java
index f3153e3..b0eb986 100644
--- a/framework/direct-download/src/main/java/org/apache/cloudstack/framework/agent/direct/download/DirectDownloadService.java
+++ b/core/src/main/java/org/apache/cloudstack/agent/directdownload/RevokeDirectDownloadCertificateCommand.java
@@ -1,3 +1,4 @@
+//
 // 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
@@ -14,18 +15,25 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
+//
+package org.apache.cloudstack.agent.directdownload;
+
+import com.cloud.agent.api.Command;
+
+public class RevokeDirectDownloadCertificateCommand extends Command {
 
-package org.apache.cloudstack.framework.agent.direct.download;
+    private String certificateAlias;
 
-public interface DirectDownloadService {
+    public RevokeDirectDownloadCertificateCommand(final String alias) {
+        this.certificateAlias = alias;
+    }
 
-    /**
-     * Download template/ISO into poolId bypassing secondary storage. Download performed by hostId
-     */
-    void downloadTemplate(long templateId, long poolId, long hostId);
+    public String getCertificateAlias() {
+        return certificateAlias;
+    }
 
-    /**
-     * Upload client certificate to each running host
-     */
-    boolean uploadCertificateToHosts(String certificateCer, String certificateName, String hypervisor);
-}
+    @Override
+    public boolean executeInSequence() {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java
index 1fca86c..dd45c09 100644
--- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java
+++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java
@@ -107,4 +107,6 @@ public interface HostDao extends GenericDao<HostVO, Long>, StateDao<Status, Stat
      * Side note: this method is currently only used in XenServerGuru; therefore, it was designed to meet XenServer deployment scenarios requirements.
      */
     HostVO findHostInZoneToExecuteCommand(long zoneId, HypervisorType hypervisorType);
+
+    List<HostVO> listAllHostsUpByZoneAndHypervisor(long zoneId, HypervisorType hypervisorType);
 }
diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java
index 8c8c082..71f0aef 100644
--- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.TimeZone;
+import java.util.stream.Collectors;
 
 import javax.annotation.PostConstruct;
 import javax.inject.Inject;
@@ -1190,6 +1191,15 @@ public class HostDaoImpl extends GenericDaoBase<HostVO, Long> implements HostDao
         }
     }
 
+    @Override
+    public List<HostVO> listAllHostsUpByZoneAndHypervisor(long zoneId, HypervisorType hypervisorType) {
+        return listByDataCenterIdAndHypervisorType(zoneId, hypervisorType)
+                .stream()
+                .filter(x -> x.getStatus().equals(Status.Up) &&
+                        x.getType() == Host.Type.Routing)
+                .collect(Collectors.toList());
+    }
+
     private ResultSet executeSqlGetResultsetForMethodFindHostInZoneToExecuteCommand(HypervisorType hypervisorType, long zoneId, TransactionLegacy tx, String sql) throws SQLException {
         PreparedStatement pstmt = tx.prepareAutoCloseStatement(sql);
         pstmt.setString(1, Objects.toString(hypervisorType));
diff --git a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDao.java
similarity index 69%
copy from api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
copy to engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDao.java
index b3f0841..69f79e0 100644
--- a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDao.java
@@ -14,12 +14,14 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-
 package org.apache.cloudstack.direct.download;
 
-import com.cloud.utils.component.PluggableService;
-import org.apache.cloudstack.framework.agent.direct.download.DirectDownloadService;
+import com.cloud.hypervisor.Hypervisor;
+import com.cloud.utils.db.GenericDao;
 
-public interface DirectDownloadManager extends DirectDownloadService, PluggableService {
+import java.util.List;
 
-}
+public interface DirectDownloadCertificateDao extends GenericDao<DirectDownloadCertificateVO, Long> {
+    DirectDownloadCertificateVO findByAlias(String alias, Hypervisor.HypervisorType hypervisorType, long zoneId);
+    List<DirectDownloadCertificateVO> listByZone(long zoneId);
+}
\ No newline at end of file
diff --git a/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDaoImpl.java
new file mode 100644
index 0000000..a936cbb
--- /dev/null
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDaoImpl.java
@@ -0,0 +1,53 @@
+// 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.direct.download;
+
+import com.cloud.hypervisor.Hypervisor;
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+
+import java.util.List;
+
+public class DirectDownloadCertificateDaoImpl extends GenericDaoBase<DirectDownloadCertificateVO, Long> implements DirectDownloadCertificateDao {
+
+    private final SearchBuilder<DirectDownloadCertificateVO> certificateSearchBuilder;
+
+    public DirectDownloadCertificateDaoImpl() {
+        certificateSearchBuilder = createSearchBuilder();
+        certificateSearchBuilder.and("alias", certificateSearchBuilder.entity().getAlias(), SearchCriteria.Op.EQ);
+        certificateSearchBuilder.and("hypervisor_type", certificateSearchBuilder.entity().getHypervisorType(), SearchCriteria.Op.EQ);
+        certificateSearchBuilder.and("zone_id", certificateSearchBuilder.entity().getZoneId(), SearchCriteria.Op.EQ);
+        certificateSearchBuilder.done();
+    }
+
+    @Override
+    public DirectDownloadCertificateVO findByAlias(String alias, Hypervisor.HypervisorType hypervisorType, long zoneId) {
+        SearchCriteria<DirectDownloadCertificateVO> sc = certificateSearchBuilder.create();
+        sc.setParameters("alias", alias);
+        sc.setParameters("hypervisor_type", hypervisorType);
+        sc.setParameters("zone_id", zoneId);
+        return findOneBy(sc);
+    }
+
+    @Override
+    public List<DirectDownloadCertificateVO> listByZone(long zoneId) {
+        SearchCriteria<DirectDownloadCertificateVO> sc = certificateSearchBuilder.create();
+        sc.setParameters("zone_id", zoneId);
+        return listBy(sc);
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java
similarity index 70%
copy from api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
copy to engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java
index b3f0841..e119b1d 100644
--- a/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadManager.java
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java
@@ -14,12 +14,13 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-
 package org.apache.cloudstack.direct.download;
 
-import com.cloud.utils.component.PluggableService;
-import org.apache.cloudstack.framework.agent.direct.download.DirectDownloadService;
+import com.cloud.utils.db.GenericDao;
 
-public interface DirectDownloadManager extends DirectDownloadService, PluggableService {
+import java.util.List;
 
-}
+public interface DirectDownloadCertificateHostMapDao extends GenericDao<DirectDownloadCertificateHostMapVO, Long> {
+    DirectDownloadCertificateHostMapVO findByCertificateAndHost(long certificateId, long hostId);
+    List<DirectDownloadCertificateHostMapVO> listByCertificateId(long certificateId);
+}
\ 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
new file mode 100644
index 0000000..7a0b732
--- /dev/null
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDaoImpl.java
@@ -0,0 +1,48 @@
+// 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.direct.download;
+
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+
+import java.util.List;
+
+public class DirectDownloadCertificateHostMapDaoImpl extends GenericDaoBase<DirectDownloadCertificateHostMapVO, Long> implements DirectDownloadCertificateHostMapDao {
+    private final SearchBuilder<DirectDownloadCertificateHostMapVO> mapSearchBuilder;
+
+    public DirectDownloadCertificateHostMapDaoImpl() {
+        mapSearchBuilder = createSearchBuilder();
+        mapSearchBuilder.and("certificate_id", mapSearchBuilder.entity().getCertificateId(), SearchCriteria.Op.EQ);
+        mapSearchBuilder.and("host_id", mapSearchBuilder.entity().getHostId(), SearchCriteria.Op.EQ);
+        mapSearchBuilder.done();
+    }
+    @Override
+    public DirectDownloadCertificateHostMapVO findByCertificateAndHost(long certificateId, long hostId) {
+        SearchCriteria<DirectDownloadCertificateHostMapVO> sc = mapSearchBuilder.create();
+        sc.setParameters("certificate_id", certificateId);
+        sc.setParameters("host_id", hostId);
+        return findOneBy(sc);
+    }
+
+    @Override
+    public List<DirectDownloadCertificateHostMapVO> listByCertificateId(long certificateId) {
+        SearchCriteria<DirectDownloadCertificateHostMapVO> sc = mapSearchBuilder.create();
+        sc.setParameters("certificate_id", certificateId);
+        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
new file mode 100644
index 0000000..db5faf6
--- /dev/null
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapVO.java
@@ -0,0 +1,84 @@
+// 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.direct.download;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "direct_download_certificate_host_map")
+public class DirectDownloadCertificateHostMapVO {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Long id;
+
+    @Column(name = "host_id")
+    private Long hostId;
+
+    @Column(name = "certificate_id")
+    private Long certificateId;
+
+    @Column(name = "revoked")
+    private Boolean revoked;
+
+    public DirectDownloadCertificateHostMapVO() {
+    }
+
+    public DirectDownloadCertificateHostMapVO(Long certificateId, Long hostId) {
+        this.certificateId = certificateId;
+        this.hostId = hostId;
+        this.revoked = false;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getHostId() {
+        return hostId;
+    }
+
+    public void setHostId(Long hostId) {
+        this.hostId = hostId;
+    }
+
+    public Long getCertificateId() {
+        return certificateId;
+    }
+
+    public void setCertificateId(Long certificateId) {
+        this.certificateId = certificateId;
+    }
+
+    public Boolean isRevoked() {
+        return revoked;
+    }
+
+    public void setRevoked(Boolean revoked) {
+        this.revoked = revoked;
+    }
+}
\ No newline at end of file
diff --git a/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateVO.java b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateVO.java
new file mode 100644
index 0000000..0b147d7
--- /dev/null
+++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateVO.java
@@ -0,0 +1,119 @@
+// 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.direct.download;
+
+import com.cloud.hypervisor.Hypervisor;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.util.UUID;
+
+@Entity
+@Table(name = "direct_download_certificate")
+public class DirectDownloadCertificateVO implements DirectDownloadCertificate {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Long id;
+
+    @Column(name = "uuid")
+    private String uuid;
+
+    @Column(name = "alias")
+    private String alias;
+
+    @Column(name = "certificate", length = 65535)
+    private String certificate;
+
+    @Column(name = "hypervisor_type")
+    private Hypervisor.HypervisorType hypervisorType;
+
+    @Column(name = "zone_id")
+    private Long zoneId;
+
+    public DirectDownloadCertificateVO() {
+        this.uuid = UUID.randomUUID().toString();
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public void setUuid(String uuid) {
+        this.uuid = uuid;
+    }
+
+    public void setAlias(String alias) {
+        this.alias = alias;
+    }
+
+    public void setCertificate(String certificate) {
+        this.certificate = certificate;
+    }
+
+    public void setHypervisorType(Hypervisor.HypervisorType hypervisorType) {
+        this.hypervisorType = hypervisorType;
+    }
+
+    public DirectDownloadCertificateVO(String alias, String certificate,
+                                       Hypervisor.HypervisorType hypervisorType, Long zoneId) {
+        this();
+        this.alias = alias;
+        this.certificate = certificate;
+        this.hypervisorType = hypervisorType;
+        this.zoneId = zoneId;
+    }
+
+    @Override
+    public String getCertificate() {
+        return certificate;
+    }
+
+    @Override
+    public String getAlias() {
+        return alias;
+    }
+
+    @Override
+    public Hypervisor.HypervisorType getHypervisorType() {
+        return hypervisorType;
+    }
+
+    @Override
+    public long getId() {
+        return id;
+    }
+
+    @Override
+    public String getUuid() {
+        return uuid;
+    }
+
+    public Long getZoneId() {
+        return zoneId;
+    }
+
+    public void setZoneId(Long zoneId) {
+        this.zoneId = zoneId;
+    }
+
+}
\ No newline at end of file
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 fc2a752..1cea7aa 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
@@ -285,4 +285,6 @@
   <bean id="outOfBandManagementDaoImpl" class="org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDaoImpl" />
   <bean id="GuestOsDetailsDaoImpl" class="org.apache.cloudstack.resourcedetail.dao.GuestOsDetailsDaoImpl" />
   <bean id="annotationDaoImpl" class="org.apache.cloudstack.annotation.dao.AnnotationDaoImpl" />
+  <bean id="directDownloadCertificateDaoImpl" class="org.apache.cloudstack.direct.download.DirectDownloadCertificateDaoImpl" />
+  <bean id="directDownloadCertificateHostMapDaoImpl" class="org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMapDaoImpl" />
 </beans>
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41200to41300.sql b/engine/schema/src/main/resources/META-INF/db/schema-41200to41300.sql
index 8b60592..904e76e 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41200to41300.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41200to41300.sql
@@ -359,3 +359,30 @@ CREATE VIEW `cloud`.`project_view` AS
         `cloud`.`account` ON account.id = project_account.account_id
             left join
         `cloud`.`project_account` pacct ON projects.id = pacct.project_id;
+
+-- KVM: Add background task to upload certificates for direct download
+CREATE TABLE `cloud`.`direct_download_certificate` (
+  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+  `uuid` varchar(40) NOT NULL,
+  `alias` varchar(255) NOT NULL,
+  `certificate` text NOT NULL,
+  `hypervisor_type` varchar(45) NOT NULL,
+  `zone_id` bigint(20) unsigned NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `i_direct_download_certificate_alias` (`alias`),
+  KEY `fk_direct_download_certificate__zone_id` (`zone_id`),
+  CONSTRAINT `fk_direct_download_certificate__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE `cloud`.`direct_download_certificate_host_map` (
+  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+  `certificate_id` bigint(20) unsigned NOT NULL,
+  `host_id` bigint(20) unsigned NOT NULL,
+  `revoked` int(1) NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`),
+  KEY `fk_direct_download_certificate_host_map__host_id` (`host_id`),
+  KEY `fk_direct_download_certificate_host_map__certificate_id` (`certificate_id`),
+  CONSTRAINT `fk_direct_download_certificate_host_map__host_id` FOREIGN KEY (`host_id`) REFERENCES `host` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `fk_direct_download_certificate_host_map__certificate_id` FOREIGN KEY (`certificate_id`) REFERENCES `direct_download_certificate` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
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 f3153e3..ed7bbd7 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
@@ -27,5 +27,10 @@ public interface DirectDownloadService {
     /**
      * Upload client certificate to each running host
      */
-    boolean uploadCertificateToHosts(String certificateCer, String certificateName, String hypervisor);
+    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);
 }
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevokeDirectDownloadCertificateWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevokeDirectDownloadCertificateWrapper.java
new file mode 100644
index 0000000..e942dcb
--- /dev/null
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevokeDirectDownloadCertificateWrapper.java
@@ -0,0 +1,106 @@
+//
+// 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.hypervisor.kvm.resource.wrapper;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+import com.cloud.utils.PropertiesUtil;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.utils.script.Script;
+import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand;
+import org.apache.cloudstack.utils.security.KeyStoreUtils;
+import org.apache.log4j.Logger;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import static org.apache.commons.lang.StringUtils.isBlank;
+
+@ResourceWrapper(handles =  RevokeDirectDownloadCertificateCommand.class)
+public class LibvirtRevokeDirectDownloadCertificateWrapper extends CommandWrapper<RevokeDirectDownloadCertificateCommand, Answer, LibvirtComputingResource> {
+
+    private static final Logger s_logger = Logger.getLogger(LibvirtRevokeDirectDownloadCertificateWrapper.class);
+
+    /**
+     * Retrieve agent.properties file
+     */
+    private File getAgentPropertiesFile() throws FileNotFoundException {
+        final File agentFile = PropertiesUtil.findConfigFile("agent.properties");
+        if (agentFile == null) {
+            throw new FileNotFoundException("Failed to find agent.properties file");
+        }
+        return agentFile;
+    }
+
+    /**
+     * Get the property 'keystore.passphrase' value from agent.properties file
+     */
+    private String getKeystorePassword(File agentFile) {
+        String pass = null;
+        if (agentFile != null) {
+            try {
+                pass = PropertiesUtil.loadFromFile(agentFile).getProperty(KeyStoreUtils.KS_PASSPHRASE_PROPERTY);
+            } catch (IOException e) {
+                s_logger.error("Could not get 'keystore.passphrase' property value due to: " + e.getMessage());
+            }
+        }
+        return pass;
+    }
+
+    /**
+     * Get keystore path
+     */
+    private String getKeyStoreFilePath(File agentFile) {
+        return agentFile.getParent() + "/" + KeyStoreUtils.KS_FILENAME;
+    }
+
+    @Override
+    public Answer execute(RevokeDirectDownloadCertificateCommand command, LibvirtComputingResource serverResource) {
+        String certificateAlias = command.getCertificateAlias();
+        try {
+            File agentFile = getAgentPropertiesFile();
+            String privatePassword = getKeystorePassword(agentFile);
+            if (isBlank(privatePassword)) {
+                return new Answer(command, false, "No password found for keystore: " + KeyStoreUtils.KS_FILENAME);
+            }
+
+            final String keyStoreFile = getKeyStoreFilePath(agentFile);
+
+            String checkCmd = String.format("keytool -list -alias %s -keystore %s -storepass %s",
+                    certificateAlias, keyStoreFile, privatePassword);
+            int existsCmdResult = Script.runSimpleBashScriptForExitValue(checkCmd);
+            if (existsCmdResult == 1) {
+                s_logger.error("Certificate alias " + certificateAlias + " does not exist, no need to revoke it");
+            } else {
+                String revokeCmd = String.format("keytool -delete -alias %s -keystore %s -storepass %s",
+                        certificateAlias, keyStoreFile, privatePassword);
+                s_logger.debug("Revoking certificate alias " + certificateAlias + " from keystore " + keyStoreFile);
+                Script.runSimpleBashScriptForExitValue(revokeCmd);
+            }
+        } catch (FileNotFoundException | CloudRuntimeException e) {
+            s_logger.error("Error while setting up certificate " + certificateAlias, e);
+            return new Answer(command, false, e.getMessage());
+        }
+        return new Answer(command);
+    }
+}
diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
index 4beddcc..0066a95 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -67,6 +67,7 @@ import org.apache.cloudstack.api.command.admin.config.ListDeploymentPlannersCmd;
 import org.apache.cloudstack.api.command.admin.config.ListHypervisorCapabilitiesCmd;
 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.RevokeTemplateDirectDownloadCertificateCmd;
 import org.apache.cloudstack.api.command.admin.direct.download.UploadTemplateDirectDownloadCertificateCmd;
 import org.apache.cloudstack.api.command.admin.domain.CreateDomainCmd;
 import org.apache.cloudstack.api.command.admin.domain.DeleteDomainCmd;
@@ -3100,6 +3101,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
         cmdList.add(CreateManagementNetworkIpRangeCmd.class);
         cmdList.add(DeleteManagementNetworkIpRangeCmd.class);
         cmdList.add(UploadTemplateDirectDownloadCertificateCmd.class);
+        cmdList.add(RevokeTemplateDirectDownloadCertificateCmd.class);
         cmdList.add(ListMgmtsCmd.class);
         cmdList.add(GetUploadParamsForIsoCmd.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 9986093..2eb6d36 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
@@ -22,9 +22,13 @@ import static com.cloud.storage.Storage.ImageFormat;
 
 import com.cloud.agent.AgentManager;
 import com.cloud.agent.api.Answer;
+import com.cloud.dc.DataCenterVO;
+import com.cloud.dc.dao.DataCenterDao;
 import com.cloud.event.ActionEventUtils;
 import com.cloud.event.EventTypes;
 import com.cloud.event.EventVO;
+import com.cloud.exception.AgentUnavailableException;
+import com.cloud.exception.OperationTimedoutException;
 import com.cloud.host.Host;
 import com.cloud.host.HostVO;
 import com.cloud.host.Status;
@@ -37,6 +41,7 @@ import com.cloud.storage.VMTemplateVO;
 import com.cloud.storage.dao.VMTemplateDao;
 import com.cloud.storage.dao.VMTemplatePoolDao;
 import com.cloud.utils.component.ManagerBase;
+import com.cloud.utils.concurrency.NamedThreadFactory;
 import com.cloud.utils.exception.CloudRuntimeException;
 
 import java.net.URI;
@@ -51,8 +56,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import javax.inject.Inject;
+import javax.naming.ConfigurationException;
 
 import com.cloud.utils.security.CertificateHelper;
 import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand;
@@ -62,18 +71,24 @@ import org.apache.cloudstack.agent.directdownload.HttpDirectDownloadCommand;
 import org.apache.cloudstack.agent.directdownload.MetalinkDirectDownloadCommand;
 import org.apache.cloudstack.agent.directdownload.NfsDirectDownloadCommand;
 import org.apache.cloudstack.agent.directdownload.HttpsDirectDownloadCommand;
+import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand;
 import org.apache.cloudstack.agent.directdownload.SetupDirectDownloadCertificateCommand;
 import org.apache.cloudstack.context.CallContext;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
 import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
 import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.managed.context.ManagedContextRunnable;
+import org.apache.cloudstack.poll.BackgroundPollManager;
 import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
 import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.collections.MapUtils;
 import org.apache.log4j.Logger;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import sun.security.x509.X509CertImpl;
 
 public class DirectDownloadManagerImpl extends ManagerBase implements DirectDownloadManager {
@@ -96,6 +111,16 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
     private VMTemplatePoolDao vmTemplatePoolDao;
     @Inject
     private DataStoreManager dataStoreManager;
+    @Inject
+    private DirectDownloadCertificateDao directDownloadCertificateDao;
+    @Inject
+    private DirectDownloadCertificateHostMapDao directDownloadCertificateHostMapDao;
+    @Inject
+    private BackgroundPollManager backgroundPollManager;
+    @Inject
+    private DataCenterDao dataCenterDao;
+
+    protected ScheduledExecutorService executorService;
 
     @Override
     public List<Class<?>> getCommands() {
@@ -311,12 +336,8 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
     /**
      * Return the list of running hosts to which upload certificates for Direct Download
      */
-    private List<HostVO> getRunningHostsToUploadCertificate(HypervisorType hypervisorType) {
-        return hostDao.listAllHostsByType(Host.Type.Routing)
-                .stream()
-                .filter(x -> x.getStatus().equals(Status.Up) &&
-                        x.getHypervisorType().equals(hypervisorType))
-                .collect(Collectors.toList());
+    private List<HostVO> getRunningHostsToUploadCertificate(Long zoneId, HypervisorType hypervisorType) {
+        return hostDao.listAllHostsUpByZoneAndHypervisor(zoneId, hypervisorType);
     }
 
     /**
@@ -365,22 +386,42 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
     }
 
     @Override
-    public boolean uploadCertificateToHosts(String certificateCer, String alias, String hypervisor) {
+    public boolean 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");
         }
 
+        List<HostVO> hosts;
+        DirectDownloadCertificateVO certificateVO;
         HypervisorType hypervisorType = HypervisorType.getType(hypervisor);
-        List<HostVO> hosts = getRunningHostsToUploadCertificate(hypervisorType);
 
-        String certificatePem = getPretifiedCertificate(certificateCer);
-        certificateSanity(certificatePem);
+        if (hostId == null) {
+            hosts = getRunningHostsToUploadCertificate(zoneId, hypervisorType);
+
+            String certificatePem = getPretifiedCertificate(certificateCer);
+            certificateSanity(certificatePem);
+
+            certificateVO = directDownloadCertificateDao.findByAlias(alias, hypervisorType, zoneId);
+            if (certificateVO != null) {
+                throw new CloudRuntimeException("Certificate alias " + alias + " has been already created");
+            }
+            certificateVO = new DirectDownloadCertificateVO(alias, certificatePem, hypervisorType, zoneId);
+            directDownloadCertificateDao.persist(certificateVO);
+        } else {
+            HostVO host = hostDao.findById(hostId);
+            hosts = Collections.singletonList(host);
+            certificateVO = directDownloadCertificateDao.findByAlias(alias, hypervisorType, zoneId);
+            if (certificateVO == null) {
+                s_logger.info("Certificate must be uploaded on zone " + zoneId);
+                return false;
+            }
+        }
 
-        s_logger.info("Attempting to upload certificate: " + alias + " to " + hosts.size() + " hosts");
+        s_logger.info("Attempting to upload certificate: " + alias + " to " + hosts.size() + " hosts on zone " + zoneId);
         int hostCount = 0;
         if (CollectionUtils.isNotEmpty(hosts)) {
             for (HostVO host : hosts) {
-                if (!uploadCertificate(certificatePem, alias, host.getId())) {
+                if (!uploadCertificate(certificateVO.getId(), host.getId())) {
                     String msg = "Could not upload certificate " + alias + " on host: " + host.getName() + " (" + host.getUuid() + ")";
                     s_logger.error(msg);
                     throw new CloudRuntimeException(msg);
@@ -395,20 +436,177 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
     /**
      * Upload and import certificate to hostId on keystore
      */
-    protected boolean uploadCertificate(String certificate, String certificateName, long hostId) {
-        s_logger.debug("Uploading certificate: " + certificateName + " to host " + hostId);
-        SetupDirectDownloadCertificateCommand cmd = new SetupDirectDownloadCertificateCommand(certificate, certificateName);
+    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();
+
+        s_logger.debug("Uploading certificate: " + certificateVO.getAlias() + " to host " + hostId);
+        SetupDirectDownloadCertificateCommand cmd = new SetupDirectDownloadCertificateCommand(certificate, alias);
         Answer answer = agentManager.easySend(hostId, cmd);
         if (answer == null || !answer.getResult()) {
-            String msg = "Certificate " + certificateName + " could not be added to host " + hostId;
+            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;
         }
-        s_logger.info("Certificate " + certificateName + " successfully uploaded to host: " + hostId);
+
+        s_logger.info("Certificate " + alias + " successfully uploaded to host: " + hostId);
+        DirectDownloadCertificateHostMapVO map = directDownloadCertificateHostMapDao.findByCertificateAndHost(certificateId, hostId);
+        if (map != null) {
+            map.setRevoked(false);
+            directDownloadCertificateHostMapDao.update(map.getId(), map);
+        } else {
+            DirectDownloadCertificateHostMapVO mapVO = new DirectDownloadCertificateHostMapVO(certificateId, hostId);
+            directDownloadCertificateHostMapDao.persist(mapVO);
+        }
+
         return true;
     }
 
+    @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;
+        if (hostId == null) {
+             maps = directDownloadCertificateHostMapDao.listByCertificateId(certificateVO.getId());
+        } else {
+            DirectDownloadCertificateHostMapVO hostMap = directDownloadCertificateHostMapDao.findByCertificateAndHost(certificateVO.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;
+            }
+            maps = Collections.singletonList(hostMap);
+        }
+
+        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);
+                }
+                s_logger.info("Certificate " + certificateAlias + " revoked from host " + mappingHostId);
+                map.setRevoked(true);
+                directDownloadCertificateHostMapDao.update(map.getId(), map);
+            }
+        }
+        return true;
+    }
+
+    protected boolean revokeCertificateAliasFromHost(String alias, Long hostId) {
+        RevokeDirectDownloadCertificateCommand cmd = new RevokeDirectDownloadCertificateCommand(alias);
+        try {
+            Answer answer = agentManager.send(hostId, cmd);
+            return answer != null && answer.getResult();
+        } catch (AgentUnavailableException | OperationTimedoutException e) {
+            s_logger.error("Error revoking certificate " + alias + " from host " + hostId, e);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
+        executorService = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("DirectDownloadCertificateMonitor"));
+        return true;
+    }
+
+    @Override
+    public boolean stop() {
+        executorService.shutdownNow();
+        return true;
+    }
+
+    @Override
+    public boolean start() {
+        if (DirectDownloadCertificateUploadInterval.value() > 0L) {
+            executorService.scheduleWithFixedDelay(
+                    new DirectDownloadCertificateUploadBackgroundTask(this, hostDao, dataCenterDao,
+                            directDownloadCertificateDao, directDownloadCertificateHostMapDao),
+                    60L, DirectDownloadCertificateUploadInterval.value(), TimeUnit.HOURS);
+        }
+        return true;
+    }
+
+    @Override
+    public String getConfigComponentName() {
+        return DirectDownloadManager.class.getSimpleName();
+    }
+
+    @Override
+    public ConfigKey<?>[] getConfigKeys() {
+        return new ConfigKey<?>[]{
+                DirectDownloadCertificateUploadInterval
+        };
+    }
+
+    public static final class DirectDownloadCertificateUploadBackgroundTask extends ManagedContextRunnable {
+
+        private DirectDownloadManager directDownloadManager;
+        private HostDao hostDao;
+        private DirectDownloadCertificateDao directDownloadCertificateDao;
+        private DirectDownloadCertificateHostMapDao directDownloadCertificateHostMapDao;
+        private DataCenterDao dataCenterDao;
+
+        public DirectDownloadCertificateUploadBackgroundTask(
+                final DirectDownloadManager manager,
+                final HostDao hostDao,
+                final DataCenterDao dataCenterDao,
+                final DirectDownloadCertificateDao directDownloadCertificateDao,
+                final DirectDownloadCertificateHostMapDao directDownloadCertificateHostMapDao) {
+            this.directDownloadManager = manager;
+            this.hostDao = hostDao;
+            this.dataCenterDao = dataCenterDao;
+            this.directDownloadCertificateDao = directDownloadCertificateDao;
+            this.directDownloadCertificateHostMapDao = directDownloadCertificateHostMapDao;
+        }
+
+        @Override
+        protected void runInContext() {
+            try {
+                if (s_logger.isTraceEnabled()) {
+                    s_logger.trace("Direct Download Manager background task is running...");
+                }
+                final DateTime now = DateTime.now(DateTimeZone.UTC);
+                List<DataCenterVO> enabledZones = dataCenterDao.listEnabledZones();
+                for (DataCenterVO zone : enabledZones) {
+                    List<DirectDownloadCertificateVO> zoneCertificates = directDownloadCertificateDao.listByZone(zone.getId());
+                    if (CollectionUtils.isNotEmpty(zoneCertificates)) {
+                        for (DirectDownloadCertificateVO certificateVO : zoneCertificates) {
+                            List<HostVO> hostsToUpload = hostDao.listAllHostsUpByZoneAndHypervisor(certificateVO.getZoneId(), certificateVO.getHypervisorType());
+                            if (CollectionUtils.isNotEmpty(hostsToUpload)) {
+                                for (HostVO hostVO : hostsToUpload) {
+                                    DirectDownloadCertificateHostMapVO mapping = directDownloadCertificateHostMapDao.findByCertificateAndHost(certificateVO.getId(), hostVO.getId());
+                                    if (mapping == null) {
+                                        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());
+                                        s_logger.debug("Certificate " + certificateVO.getAlias() + " " +
+                                                (result ? "uploaded" : "could not be uploaded") +
+                                                " to host " + hostVO.getId());
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (final Throwable t) {
+                s_logger.error("Error trying to run Direct Download background task", t);
+            }
+        }
+    }
 }
diff --git a/test/integration/smoke/test_direct_download.py b/test/integration/smoke/test_direct_download.py
index 65117f9..132deb4 100644
--- a/test/integration/smoke/test_direct_download.py
+++ b/test/integration/smoke/test_direct_download.py
@@ -27,7 +27,7 @@ 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
+from marvin.cloudstackAPI import (uploadTemplateDirectDownloadCertificate, revokeTemplateDirectDownloadCertificate)
 from marvin.lib.decoratorGenerators import skipTestIf
 
 
@@ -92,6 +92,7 @@ class TestUploadDirectDownloadCertificates(cloudstackTestCase):
         cmd.hypervisor = self.hypervisor
         cmd.name = "marvin-test-verify-certs"
         cmd.certificate = self.certificates["invalid"]
+        cmd.zoneid = self.zone.id
 
         invalid_cert_uploadFails = False
         expired_cert_upload_fails = False
@@ -120,17 +121,29 @@ class TestUploadDirectDownloadCertificates(cloudstackTestCase):
 
         # Validate the following
         # 1. Valid certificates are uploaded to hosts
+        # 2. Revoke uploaded certificate from host
 
         cmd = uploadTemplateDirectDownloadCertificate.uploadTemplateDirectDownloadCertificateCmd()
         cmd.hypervisor = self.hypervisor
         cmd.name = "marvin-test-verify-certs"
         cmd.certificate = self.certificates["valid"]
+        cmd.zoneid = self.zone.id
 
         try:
             self.apiclient.uploadTemplateDirectDownloadCertificate(cmd)
         except Exception as e:
             self.fail("Valid certificate must be uploaded")
 
+        revokecmd = revokeTemplateDirectDownloadCertificate.revokeTemplateDirectDownloadCertificateCmd()
+        revokecmd.hypervisor = self.hypervisor
+        revokecmd.name = cmd.name
+        revokecmd.zoneid = self.zone.id
+
+        try:
+            self.apiclient.revokeTemplateDirectDownloadCertificate(revokecmd)
+        except Exception as e:
+            self.fail("Uploaded certificates should be revoked when needed")
+
         return