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
This commit is contained in:
Nicolas Vazquez 2019-07-18 02:39:00 -03:00 committed by GitHub
parent a1faf09a2c
commit a1a9fb8977
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 933 additions and 21 deletions

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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));

View File

@ -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, Long> {
DirectDownloadCertificateVO findByAlias(String alias, Hypervisor.HypervisorType hypervisorType, long zoneId);
List<DirectDownloadCertificateVO> listByZone(long zoneId);
}

View File

@ -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);
}
}

View File

@ -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, Long> {
DirectDownloadCertificateHostMapVO findByCertificateAndHost(long certificateId, long hostId);
List<DirectDownloadCertificateHostMapVO> listByCertificateId(long certificateId);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);
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<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);
}
}
}
}

View File

@ -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