From a1a9fb8977532f9d43c78b785d0bf3e9c53fc66e Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Thu, 18 Jul 2019 02:39:00 -0300 Subject: [PATCH] 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 --- ...eTemplateDirectDownloadCertificateCmd.java | 98 ++++++++ ...dTemplateDirectDownloadCertificateCmd.java | 12 +- .../download/DirectDownloadCertificate.java | 29 +++ .../download/DirectDownloadManager.java | 16 +- ...evokeDirectDownloadCertificateCommand.java | 39 +++ .../main/java/com/cloud/host/dao/HostDao.java | 2 + .../java/com/cloud/host/dao/HostDaoImpl.java | 10 + .../DirectDownloadCertificateDao.java | 27 ++ .../DirectDownloadCertificateDaoImpl.java | 53 ++++ .../DirectDownloadCertificateHostMapDao.java | 26 ++ ...rectDownloadCertificateHostMapDaoImpl.java | 48 ++++ .../DirectDownloadCertificateHostMapVO.java | 84 +++++++ .../download/DirectDownloadCertificateVO.java | 119 +++++++++ ...spring-engine-schema-core-daos-context.xml | 2 + .../META-INF/db/schema-41200to41300.sql | 27 ++ .../download/DirectDownloadService.java | 7 +- ...evokeDirectDownloadCertificateWrapper.java | 106 ++++++++ .../cloud/server/ManagementServerImpl.java | 2 + .../download/DirectDownloadManagerImpl.java | 232 ++++++++++++++++-- .../integration/smoke/test_direct_download.py | 15 +- 20 files changed, 933 insertions(+), 21 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java create mode 100644 core/src/main/java/org/apache/cloudstack/agent/directdownload/RevokeDirectDownloadCertificateCommand.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateVO.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevokeDirectDownloadCertificateWrapper.java 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 new file mode 100644 index 00000000000..ef9fa8b1fa2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/direct/download/RevokeTemplateDirectDownloadCertificateCmd.java @@ -0,0 +1,98 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +package org.apache.cloudstack.api.command.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 c93fca2d300..223f20b5bb0 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/DirectDownloadCertificate.java b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java new file mode 100644 index 00000000000..6227c26ceab --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificate.java @@ -0,0 +1,29 @@ +// 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 org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface DirectDownloadCertificate extends InternalIdentity, Identity { + + 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 b3f0841a6e8..d627ffa69d1 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 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/core/src/main/java/org/apache/cloudstack/agent/directdownload/RevokeDirectDownloadCertificateCommand.java b/core/src/main/java/org/apache/cloudstack/agent/directdownload/RevokeDirectDownloadCertificateCommand.java new file mode 100644 index 00000000000..b0eb98647dc --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/agent/directdownload/RevokeDirectDownloadCertificateCommand.java @@ -0,0 +1,39 @@ +// +// 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.agent.directdownload; + +import com.cloud.agent.api.Command; + +public class RevokeDirectDownloadCertificateCommand extends Command { + + private String certificateAlias; + + public RevokeDirectDownloadCertificateCommand(final String alias) { + this.certificateAlias = alias; + } + + public String getCertificateAlias() { + return certificateAlias; + } + + @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 1fca86ca319..dd45c0987ad 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, StateDao 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 8c8c082ed8f..71f0aef39d6 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 implements HostDao } } + @Override + public List 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/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDao.java b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDao.java new file mode 100644 index 00000000000..69f79e0448e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateDao.java @@ -0,0 +1,27 @@ +// 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.GenericDao; + +import java.util.List; + +public interface DirectDownloadCertificateDao extends GenericDao { + DirectDownloadCertificateVO findByAlias(String alias, Hypervisor.HypervisorType hypervisorType, long zoneId); + List 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 00000000000..a936cbb4490 --- /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 implements DirectDownloadCertificateDao { + + private final SearchBuilder 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 sc = certificateSearchBuilder.create(); + sc.setParameters("alias", alias); + sc.setParameters("hypervisor_type", hypervisorType); + sc.setParameters("zone_id", zoneId); + return findOneBy(sc); + } + + @Override + public List listByZone(long zoneId) { + SearchCriteria sc = certificateSearchBuilder.create(); + sc.setParameters("zone_id", zoneId); + return listBy(sc); + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..e119b1d491e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/direct/download/DirectDownloadCertificateHostMapDao.java @@ -0,0 +1,26 @@ +// 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.GenericDao; + +import java.util.List; + +public interface DirectDownloadCertificateHostMapDao extends GenericDao { + DirectDownloadCertificateHostMapVO findByCertificateAndHost(long certificateId, long hostId); + List 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 00000000000..7a0b732bbfd --- /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 implements DirectDownloadCertificateHostMapDao { + private final SearchBuilder 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 sc = mapSearchBuilder.create(); + sc.setParameters("certificate_id", certificateId); + sc.setParameters("host_id", hostId); + return findOneBy(sc); + } + + @Override + public List listByCertificateId(long certificateId) { + SearchCriteria 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 00000000000..db5faf669ff --- /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 00000000000..0b147d7a227 --- /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 fc2a752307d..1cea7aa4b11 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 @@ + + 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 8b60592f275..904e76e8df0 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 f3153e3470e..ed7bbd76a35 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 00000000000..e942dcbad00 --- /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 { + + 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 4beddcc6e02..0066a95a6b6 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 99860934cd5..2eb6d36b9bf 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> 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 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 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 hosts; + DirectDownloadCertificateVO certificateVO; HypervisorType hypervisorType = HypervisorType.getType(hypervisor); - List hosts = getRunningHostsToUploadCertificate(hypervisorType); - String certificatePem = getPretifiedCertificate(certificateCer); - certificateSanity(certificatePem); + if (hostId == null) { + hosts = getRunningHostsToUploadCertificate(zoneId, hypervisorType); - s_logger.info("Attempting to upload certificate: " + alias + " to " + hosts.size() + " hosts"); + 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 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 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 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 enabledZones = dataCenterDao.listEnabledZones(); + for (DataCenterVO zone : enabledZones) { + List zoneCertificates = directDownloadCertificateDao.listByZone(zone.getId()); + if (CollectionUtils.isNotEmpty(zoneCertificates)) { + for (DirectDownloadCertificateVO certificateVO : zoneCertificates) { + List 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 65117f97f8c..132deb4f698 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