| // |
| // 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 static com.cloud.storage.Storage.ImageFormat; |
| import static org.apache.cloudstack.direct.download.DirectDownloadManager.HostCertificateStatus.CertificateStatus; |
| |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateExpiredException; |
| import java.security.cert.CertificateNotYetValidException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.ScheduledExecutorService; |
| 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.exception.InvalidParameterValueException; |
| import com.cloud.user.Account; |
| import com.cloud.utils.Pair; |
| import org.apache.cloudstack.agent.directdownload.DirectDownloadAnswer; |
| import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand; |
| import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand.DownloadProtocol; |
| import org.apache.cloudstack.agent.directdownload.HttpDirectDownloadCommand; |
| import org.apache.cloudstack.agent.directdownload.HttpsDirectDownloadCommand; |
| import org.apache.cloudstack.agent.directdownload.MetalinkDirectDownloadCommand; |
| import org.apache.cloudstack.agent.directdownload.NfsDirectDownloadCommand; |
| import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand; |
| import org.apache.cloudstack.agent.directdownload.SetupDirectDownloadCertificateCommand; |
| import org.apache.cloudstack.api.ApiCommandResourceType; |
| 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.engine.subsystem.api.storage.TemplateDataFactory; |
| import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; |
| import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; |
| import org.apache.cloudstack.framework.config.ConfigKey; |
| import org.apache.cloudstack.framework.config.dao.ConfigurationDao; |
| 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.cloudstack.storage.to.TemplateObjectTO; |
| import org.apache.commons.collections.CollectionUtils; |
| import org.apache.commons.collections.MapUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.joda.time.DateTime; |
| import org.joda.time.DateTimeZone; |
| |
| 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; |
| import com.cloud.host.dao.HostDao; |
| import com.cloud.hypervisor.Hypervisor.HypervisorType; |
| import com.cloud.storage.DataStoreRole; |
| import com.cloud.storage.ScopeType; |
| import com.cloud.storage.Storage; |
| import com.cloud.storage.StorageManager; |
| import com.cloud.storage.VMTemplateStoragePoolVO; |
| import com.cloud.storage.VMTemplateStorageResourceAssoc; |
| 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 com.cloud.utils.security.CertificateHelper; |
| |
| import sun.security.x509.X509CertImpl; |
| |
| public class DirectDownloadManagerImpl extends ManagerBase implements DirectDownloadManager { |
| |
| protected static final String httpHeaderDetailKey = "HTTP_HEADER"; |
| protected static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; |
| protected static final String END_CERT = "-----END CERTIFICATE-----"; |
| protected final static String LINE_SEPARATOR = "\n"; |
| |
| @Inject |
| private VMTemplateDao vmTemplateDao; |
| @Inject |
| private PrimaryDataStoreDao primaryDataStoreDao; |
| @Inject |
| private HostDao hostDao; |
| @Inject |
| private AgentManager agentManager; |
| @Inject |
| private VMTemplatePoolDao vmTemplatePoolDao; |
| @Inject |
| private DataStoreManager dataStoreManager; |
| @Inject |
| private DirectDownloadCertificateDao directDownloadCertificateDao; |
| @Inject |
| private DirectDownloadCertificateHostMapDao directDownloadCertificateHostMapDao; |
| @Inject |
| private BackgroundPollManager backgroundPollManager; |
| @Inject |
| private DataCenterDao dataCenterDao; |
| @Inject |
| private ConfigurationDao configDao; |
| @Inject |
| private TemplateDataFactory tmplFactory; |
| @Inject |
| private VolumeService volService; |
| |
| protected ScheduledExecutorService executorService; |
| |
| @Override |
| public List<Class<?>> getCommands() { |
| final List<Class<?>> cmdList = new ArrayList<Class<?>>(); |
| return cmdList; |
| } |
| |
| /** |
| * Return protocol to use from provided URL |
| * @param url |
| * @return |
| */ |
| public static DownloadProtocol getProtocolFromUrl(String url) { |
| URI uri; |
| try { |
| uri = new URI(url); |
| } catch (URISyntaxException e) { |
| throw new CloudRuntimeException("URI is incorrect: " + url); |
| } |
| if ((uri != null) && (uri.getScheme() != null)) { |
| if (uri.getPath().endsWith(".metalink")) { |
| return DownloadProtocol.METALINK; |
| } else if (uri.getScheme().equalsIgnoreCase("http")) { |
| return DownloadProtocol.HTTP; |
| } else if (uri.getScheme().equalsIgnoreCase("https")) { |
| return DownloadProtocol.HTTPS; |
| } else if (uri.getScheme().equalsIgnoreCase("nfs")) { |
| return DownloadProtocol.NFS; |
| } else { |
| throw new CloudRuntimeException("Scheme is not supported " + url); |
| } |
| } else { |
| throw new CloudRuntimeException("URI is incorrect: " + url); |
| } |
| } |
| |
| /** |
| * Return HTTP headers from template details |
| * @param templateDetails |
| * @return |
| */ |
| protected Map<String, String> getHeadersFromDetails(Map<String, String> templateDetails) { |
| if (MapUtils.isEmpty(templateDetails)) { |
| return new HashMap<>(); |
| } |
| Map<String, String> headers = new HashMap<>(); |
| for (String key : templateDetails.keySet()) { |
| if (key.startsWith(httpHeaderDetailKey)) { |
| String header = key.split(":")[1]; |
| String value = templateDetails.get(key); |
| headers.put(header, value); |
| } |
| } |
| return headers; |
| } |
| |
| /** |
| * Get running host IDs within the same hypervisor, cluster and datacenter than hostId. ID hostId is not included on the returned list |
| */ |
| protected List<Long> getRunningHostIdsInTheSameCluster(Long clusterId, long dataCenterId, HypervisorType hypervisorType, long hostId) { |
| List<Long> list = hostDao.listByDataCenterIdAndHypervisorType(dataCenterId, hypervisorType) |
| .stream() |
| .filter(x -> x.getHypervisorType().equals(hypervisorType) && x.getStatus().equals(Status.Up) && |
| x.getType().equals(Host.Type.Routing) && x.getClusterId().equals(clusterId) && |
| x.getId() != hostId) |
| .map(x -> x.getId()) |
| .collect(Collectors.toList()); |
| Collections.shuffle(list); |
| return list; |
| } |
| |
| /** |
| * Create host IDs array having hostId as the first element |
| */ |
| protected Long[] createHostIdsList(List<Long> hostIds, long hostId) { |
| if (CollectionUtils.isEmpty(hostIds)) { |
| return Collections.singletonList(hostId).toArray(new Long[1]); |
| } |
| Long[] ids = new Long[hostIds.size() + 1]; |
| ids[0] = hostId; |
| int i = 1; |
| for (Long id : hostIds) { |
| ids[i] = id; |
| i++; |
| } |
| return ids; |
| } |
| |
| /** |
| * Get alternative hosts to retry downloading a template. The planner have previously selected a host and a storage pool |
| * @return array of host ids which can access the storage pool |
| */ |
| protected Long[] getHostsToRetryOn(Host host, StoragePoolVO storagePool) { |
| List<Long> clusterHostIds = new ArrayList<>(); |
| if (storagePool.getPoolType() != Storage.StoragePoolType.Filesystem || storagePool.getScope() != ScopeType.HOST) { |
| clusterHostIds = getRunningHostIdsInTheSameCluster(host.getClusterId(), host.getDataCenterId(), host.getHypervisorType(), host.getId()); |
| } |
| return createHostIdsList(clusterHostIds, host.getId()); |
| } |
| |
| @Override |
| public void downloadTemplate(long templateId, long poolId, long hostId) { |
| VMTemplateVO template = vmTemplateDao.findById(templateId); |
| StoragePoolVO pool = primaryDataStoreDao.findById(poolId); |
| HostVO host = hostDao.findById(hostId); |
| if (pool == null) { |
| throw new CloudRuntimeException("Storage pool " + poolId + " could not be found"); |
| } |
| if (template == null) { |
| throw new CloudRuntimeException("Template " + templateId + " could not be found"); |
| } |
| if (host == null) { |
| throw new CloudRuntimeException("Host " + hostId + " could not be found"); |
| } |
| if (!template.isDirectDownload()) { |
| throw new CloudRuntimeException("Template " + templateId + " is not marked for direct download"); |
| } |
| Map<String, String> details = template.getDetails(); |
| String url = template.getUrl(); |
| String checksum = template.getChecksum(); |
| Map<String, String> headers = getHeadersFromDetails(details); |
| DataStore store = dataStoreManager.getDataStore(poolId, DataStoreRole.Primary); |
| if (store == null) { |
| throw new CloudRuntimeException("Data store " + poolId + " could not be found"); |
| } |
| PrimaryDataStore primaryDataStore = (PrimaryDataStore) store; |
| PrimaryDataStoreTO to = (PrimaryDataStoreTO) primaryDataStore.getTO(); |
| |
| DownloadProtocol protocol = getProtocolFromUrl(url); |
| DirectDownloadCommand cmd = getDirectDownloadCommandFromProtocol(protocol, url, templateId, to, checksum, |
| headers); |
| cmd.setTemplateSize(template.getSize()); |
| cmd.setFormat(template.getFormat()); |
| |
| if (tmplFactory.getTemplate(templateId, store) != null) { |
| cmd.setDestData((TemplateObjectTO) tmplFactory.getTemplate(templateId, store).getTO()); |
| } |
| |
| int cmdTimeOut = StorageManager.PRIMARY_STORAGE_DOWNLOAD_WAIT.value(); |
| cmd.setWait(cmdTimeOut); |
| |
| Answer answer = sendDirectDownloadCommand(cmd, template, poolId, host); |
| |
| VMTemplateStoragePoolVO sPoolRef = vmTemplatePoolDao.findByPoolTemplate(poolId, templateId, null); |
| if (sPoolRef == null) { |
| if (logger.isDebugEnabled()) { |
| logger.debug("Not found (templateId:" + templateId + " poolId: " + poolId + ") in template_spool_ref, persisting it"); |
| } |
| DirectDownloadAnswer ans = (DirectDownloadAnswer) answer; |
| sPoolRef = new VMTemplateStoragePoolVO(poolId, templateId, null); |
| sPoolRef.setDownloadPercent(100); |
| sPoolRef.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOADED); |
| sPoolRef.setState(ObjectInDataStoreStateMachine.State.Ready); |
| sPoolRef.setTemplateSize(ans.getTemplateSize()); |
| sPoolRef.setLocalDownloadPath(ans.getInstallPath()); |
| sPoolRef.setInstallPath(ans.getInstallPath()); |
| vmTemplatePoolDao.persist(sPoolRef); |
| } else { |
| // For managed storage, update after template downloaded and copied to the disk |
| DirectDownloadAnswer ans = (DirectDownloadAnswer) answer; |
| sPoolRef.setDownloadPercent(100); |
| sPoolRef.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOADED); |
| sPoolRef.setState(ObjectInDataStoreStateMachine.State.Ready); |
| sPoolRef.setTemplateSize(ans.getTemplateSize()); |
| sPoolRef.setLocalDownloadPath(ans.getInstallPath()); |
| sPoolRef.setInstallPath(ans.getInstallPath()); |
| vmTemplatePoolDao.update(sPoolRef.getId(), sPoolRef); |
| } |
| } |
| |
| /** |
| * Send direct download command for downloading template with ID templateId on storage pool with ID poolId.<br/> |
| * At first, cmd is sent to host, in case of failure it will retry on other hosts before failing |
| * @param cmd direct download command |
| * @param template template |
| * @param poolId pool id |
| * @param host first host to which send the command |
| * @return download answer from any host which could handle cmd |
| */ |
| private Answer sendDirectDownloadCommand(DirectDownloadCommand cmd, VMTemplateVO template, long poolId, HostVO host) { |
| boolean downloaded = false; |
| int retry = 3; |
| |
| StoragePoolVO storagePoolVO = primaryDataStoreDao.findById(poolId); |
| // TODO: Move the host retry attempts to upper layer |
| Long[] hostsToRetry = getHostsToRetryOn(host, storagePoolVO); |
| int hostIndex = 0; |
| Answer answer = null; |
| String answerDetails = ""; |
| String errorDetails = ""; |
| Long hostToSendDownloadCmd = hostsToRetry[hostIndex]; |
| boolean continueRetrying = true; |
| while (!downloaded && retry > 0 && continueRetrying) { |
| PrimaryDataStore primaryDataStore = null; |
| TemplateInfo templateOnPrimary = null; |
| |
| try { |
| if (hostToSendDownloadCmd != host.getId() && storagePoolVO.isManaged()) { |
| primaryDataStore = (PrimaryDataStore) dataStoreManager.getPrimaryDataStore(poolId); |
| templateOnPrimary = primaryDataStore.getTemplate(template.getId(), null); |
| if (templateOnPrimary != null) { |
| volService.grantAccess(templateOnPrimary, host, primaryDataStore); |
| } |
| } |
| |
| logger.debug("Sending Direct download command to host " + hostToSendDownloadCmd); |
| answer = agentManager.easySend(hostToSendDownloadCmd, cmd); |
| if (answer != null) { |
| DirectDownloadAnswer ans = (DirectDownloadAnswer)answer; |
| downloaded = answer.getResult(); |
| answerDetails = answer.getDetails(); |
| continueRetrying = ans.isRetryOnOtherHosts(); |
| } |
| hostToSendDownloadCmd = hostsToRetry[(hostIndex + 1) % hostsToRetry.length]; |
| } finally { |
| if (templateOnPrimary != null) { |
| volService.revokeAccess(templateOnPrimary, host, primaryDataStore); |
| } |
| } |
| |
| retry --; |
| } |
| if (!downloaded) { |
| logUsageEvent(template, poolId); |
| if (!answerDetails.isEmpty()){ |
| Account caller = CallContext.current().getCallingAccount(); |
| if (caller != null && caller.getType() == Account.Type.ADMIN){ |
| errorDetails = String.format(" Details: %s", answerDetails); |
| } |
| } |
| throw new CloudRuntimeException(String.format("Template %d could not be downloaded on pool %d, failing after trying on several hosts%s", template.getId(), poolId, errorDetails)); |
| } |
| return answer; |
| } |
| |
| /** |
| * Log and persist event for direct download failure |
| */ |
| private void logUsageEvent(VMTemplateVO template, long poolId) { |
| String event = EventTypes.EVENT_TEMPLATE_DIRECT_DOWNLOAD_FAILURE; |
| if (template.getFormat() == ImageFormat.ISO) { |
| event = EventTypes.EVENT_ISO_DIRECT_DOWNLOAD_FAILURE; |
| } |
| String description = "Direct Download for template Id: " + template.getId() + " on pool Id: " + poolId + " failed"; |
| logger.error(description); |
| ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), template.getAccountId(), EventVO.LEVEL_INFO, event, description, template.getId(), ApiCommandResourceType.Template.toString(), 0); |
| } |
| |
| /** |
| * Return DirectDownloadCommand according to the protocol |
| */ |
| private DirectDownloadCommand getDirectDownloadCommandFromProtocol(DownloadProtocol protocol, String url, |
| Long templateId, PrimaryDataStoreTO destPool, String checksum, Map<String, String> httpHeaders) { |
| int connectTimeout = DirectDownloadConnectTimeout.value(); |
| int soTimeout = DirectDownloadSocketTimeout.value(); |
| int connectionRequestTimeout = DirectDownloadConnectionRequestTimeout.value(); |
| boolean followRedirects = StorageManager.DataStoreDownloadFollowRedirects.value(); |
| if (protocol.equals(DownloadProtocol.HTTP)) { |
| return new HttpDirectDownloadCommand(url, templateId, destPool, checksum, httpHeaders, connectTimeout, |
| soTimeout, followRedirects); |
| } else if (protocol.equals(DownloadProtocol.HTTPS)) { |
| return new HttpsDirectDownloadCommand(url, templateId, destPool, checksum, httpHeaders, connectTimeout, |
| soTimeout, connectionRequestTimeout, followRedirects); |
| } else if (protocol.equals(DownloadProtocol.NFS)) { |
| return new NfsDirectDownloadCommand(url, templateId, destPool, checksum, httpHeaders); |
| } else if (protocol.equals(DownloadProtocol.METALINK)) { |
| return new MetalinkDirectDownloadCommand(url, templateId, destPool, checksum, httpHeaders, connectTimeout, |
| soTimeout, followRedirects); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Return the list of running hosts to which upload certificates for Direct Download |
| */ |
| private List<HostVO> getRunningHostsToUploadCertificate(Long zoneId, HypervisorType hypervisorType) { |
| return hostDao.listAllHostsUpByZoneAndHypervisor(zoneId, hypervisorType); |
| } |
| |
| /** |
| * Return pretified PEM certificate |
| */ |
| protected String getPretifiedCertificate(String certificateCer) { |
| String cert = certificateCer.replaceAll("(.{64})", "$1\n"); |
| if (!cert.startsWith(BEGIN_CERT) && !cert.endsWith(END_CERT)) { |
| cert = BEGIN_CERT + LINE_SEPARATOR + cert + LINE_SEPARATOR + END_CERT; |
| } |
| return cert; |
| } |
| |
| /** |
| * Generate and return certificate from the string |
| * @throws CloudRuntimeException if the certificate is not well formed |
| */ |
| private Certificate getCertificateFromString(String certificatePem) { |
| try { |
| return CertificateHelper.buildCertificate(certificatePem); |
| } catch (CertificateException e) { |
| e.printStackTrace(); |
| throw new CloudRuntimeException("Cannot parse the certificate provided, please provide a PEM certificate. Error: " + e.getMessage()); |
| } |
| } |
| |
| /** |
| * Perform sanity of string parsed certificate |
| */ |
| protected void certificateSanity(String certificatePem) { |
| Certificate certificate = getCertificateFromString(certificatePem); |
| |
| if (certificate instanceof X509CertImpl) { |
| X509CertImpl x509Cert = (X509CertImpl) certificate; |
| try { |
| x509Cert.checkValidity(); |
| } catch (CertificateExpiredException | CertificateNotYetValidException e) { |
| String msg = "Certificate is invalid. Please provide a valid certificate. Error: " + e.getMessage(); |
| logger.error(msg); |
| throw new CloudRuntimeException(msg); |
| } |
| if (x509Cert.getSubjectDN() != null) { |
| logger.debug("Valid certificate for domain name: " + x509Cert.getSubjectDN().getName()); |
| } |
| } |
| } |
| |
| @Override |
| public Pair<DirectDownloadCertificate, List<HostCertificateStatus>> uploadCertificateToHosts( |
| String certificateCer, String alias, String hypervisor, Long zoneId, Long hostId) { |
| if (alias != null && (alias.equalsIgnoreCase("cloud") || alias.startsWith("cloudca"))) { |
| throw new CloudRuntimeException("Please provide a different alias name for the certificate"); |
| } |
| |
| List<HostVO> hosts; |
| DirectDownloadCertificateVO certificateVO; |
| HypervisorType hypervisorType = HypervisorType.getType(hypervisor); |
| |
| if (hypervisorType != HypervisorType.KVM) { |
| throw new CloudRuntimeException("Direct download certificates only supported on KVM"); |
| } |
| |
| if (hostId == null) { |
| hosts = getRunningHostsToUploadCertificate(zoneId, hypervisorType); |
| |
| 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) { |
| logger.info("Certificate must be uploaded on zone " + zoneId); |
| return new Pair<>(certificateVO, new ArrayList<>()); |
| } |
| } |
| |
| logger.info("Attempting to upload certificate: " + alias + " to " + hosts.size() + " hosts on zone " + zoneId); |
| int success = 0; |
| int failed = 0; |
| List<HostCertificateStatus> results = new ArrayList<>(); |
| if (CollectionUtils.isNotEmpty(hosts)) { |
| for (HostVO host : hosts) { |
| if (host == null) { |
| continue; |
| } |
| HostCertificateStatus hostStatus; |
| Pair<Boolean, String> result = provisionCertificate(certificateVO.getId(), host.getId()); |
| if (!result.first()) { |
| String msg = "Could not upload certificate " + alias + " on host: " + host.getName() + " (" + host.getUuid() + "): " + result.second(); |
| logger.error(msg); |
| failed++; |
| hostStatus = new HostCertificateStatus(CertificateStatus.FAILED, host, result.second()); |
| } else { |
| success++; |
| hostStatus = new HostCertificateStatus(CertificateStatus.UPLOADED, host, ""); |
| } |
| results.add(hostStatus); |
| } |
| } |
| logger.info("Certificate was successfully uploaded to " + success + " hosts, " + failed + " failed"); |
| return new Pair<>(certificateVO, results); |
| } |
| |
| private Pair<Boolean, String> setupCertificateOnHost(DirectDownloadCertificate certificate, long hostId) { |
| String certificateStr = certificate.getCertificate(); |
| String alias = certificate.getAlias(); |
| long certificateId = certificate.getId(); |
| |
| logger.debug("Uploading certificate: " + alias + " to host " + hostId); |
| SetupDirectDownloadCertificateCommand cmd = new SetupDirectDownloadCertificateCommand(certificateStr, alias); |
| Answer answer = agentManager.easySend(hostId, cmd); |
| Pair<Boolean, String> result; |
| if (answer == null || !answer.getResult()) { |
| String msg = "Certificate " + alias + " could not be added to host " + hostId; |
| if (answer != null) { |
| msg += " due to: " + answer.getDetails(); |
| } |
| logger.error(msg); |
| result = new Pair<>(false, msg); |
| } else { |
| result = new Pair<>(true, "OK"); |
| } |
| |
| 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 result; |
| } |
| /** |
| * Upload and import certificate to hostId on keystore |
| */ |
| public Pair<Boolean, String> provisionCertificate(long certificateId, long hostId) { |
| DirectDownloadCertificateVO certificateVO = directDownloadCertificateDao.findById(certificateId); |
| if (certificateVO == null) { |
| throw new CloudRuntimeException("Could not find certificate with id " + certificateId + " to upload to host: " + hostId); |
| } |
| HostVO host = hostDao.findById(hostId); |
| if (host == null) { |
| throw new CloudRuntimeException("Cannot find a host with ID " + hostId); |
| } |
| if (host.getHypervisorType() != HypervisorType.KVM) { |
| throw new CloudRuntimeException("Cannot provision certificate to host " + host.getName() + " since it is not KVM"); |
| } |
| |
| return setupCertificateOnHost(certificateVO, hostId); |
| } |
| |
| @Override |
| public boolean syncCertificatesToHost(long hostId, long zoneId) { |
| List<DirectDownloadCertificateVO> zoneCertificates = directDownloadCertificateDao.listByZone(zoneId); |
| if (CollectionUtils.isEmpty(zoneCertificates)) { |
| if (logger.isTraceEnabled()) { |
| logger.trace("No certificates to sync on host: " + hostId); |
| } |
| return true; |
| } |
| |
| boolean syncCertificatesResult = true; |
| int certificatesSyncCount = 0; |
| logger.debug("Syncing certificates on host: " + hostId); |
| for (DirectDownloadCertificateVO certificateVO : zoneCertificates) { |
| DirectDownloadCertificateHostMapVO mapping = directDownloadCertificateHostMapDao.findByCertificateAndHost(certificateVO.getId(), hostId); |
| if (mapping == null) { |
| logger.debug("Syncing certificate " + certificateVO.getId() + " (" + certificateVO.getAlias() + ") on host: " + hostId + ", uploading it"); |
| Pair<Boolean, String> result = provisionCertificate(certificateVO.getId(), hostId); |
| if (!result.first()) { |
| String msg = "Could not sync certificate " + certificateVO.getId() + " (" + certificateVO.getAlias() + ") on host: " + hostId + ", upload failed: " + result.second(); |
| logger.error(msg); |
| syncCertificatesResult = false; |
| } else { |
| certificatesSyncCount++; |
| } |
| } else { |
| logger.debug("Certificate " + certificateVO.getId() + " (" + certificateVO.getAlias() + ") already synced on host: " + hostId); |
| } |
| } |
| |
| logger.debug("Synced " + certificatesSyncCount + " out of " + zoneCertificates.size() + " certificates on host: " + hostId); |
| return syncCertificatesResult; |
| } |
| |
| private List<DirectDownloadCertificateHostMapVO> getCertificateHostMappings(DirectDownloadCertificate certificate, Long hostId) { |
| List<DirectDownloadCertificateHostMapVO> maps; |
| if (hostId == null) { |
| maps = directDownloadCertificateHostMapDao.listByCertificateIdAndRevoked(certificate.getId(), false); |
| } else { |
| DirectDownloadCertificateHostMapVO hostMap = directDownloadCertificateHostMapDao.findByCertificateAndHost(certificate.getId(), hostId); |
| if (hostMap == null) { |
| String msg = "Certificate " + certificate.getAlias() + " cannot be revoked from host " + hostId + " as it is not available on the host"; |
| logger.error(msg); |
| throw new CloudRuntimeException(msg); |
| } else if (hostMap.isRevoked()) { |
| logger.debug("Certificate " + certificate.getAlias() + " was already revoked from host " + hostId + " skipping it"); |
| return new LinkedList<>(); |
| } |
| maps = Collections.singletonList(hostMap); |
| } |
| return maps; |
| } |
| |
| @Override |
| public DirectDownloadCertificate findDirectDownloadCertificateByIdOrHypervisorAndAlias(Long id, String alias, String hypervisor, Long zoneId) { |
| DirectDownloadCertificateVO certificateVO; |
| if (id != null) { |
| certificateVO = directDownloadCertificateDao.findById(id); |
| } else if (StringUtils.isNotBlank(alias) && StringUtils.isNotBlank(hypervisor)) { |
| certificateVO = directDownloadCertificateDao.findByAlias(alias, HypervisorType.getType(hypervisor), zoneId); |
| } else { |
| throw new CloudRuntimeException("Please provide a hypervisor and certificate alias or certificate ID"); |
| } |
| if (certificateVO == null) { |
| throw new CloudRuntimeException("Could not find certificate " + |
| (id != null ? "with ID " + id : "with alias " + alias + " and hypervisor " + hypervisor)); |
| } |
| return certificateVO; |
| } |
| |
| @Override |
| public List<HostCertificateStatus> revokeCertificate(DirectDownloadCertificate certificate, Long zoneId, Long hostId) { |
| String certificateAlias = certificate.getAlias(); |
| if (!certificate.getZoneId().equals(zoneId)) { |
| throw new CloudRuntimeException("The certificate with alias " + certificateAlias + " was uploaded " + |
| " to the zone with ID=" + certificate.getZoneId() + " instead of the zone with ID=" + zoneId); |
| } |
| |
| List<HostCertificateStatus> hostsList = new ArrayList<>(); |
| List<DirectDownloadCertificateHostMapVO> maps = getCertificateHostMappings(certificate, hostId); |
| if (CollectionUtils.isEmpty(maps)) { |
| return hostsList; |
| } |
| |
| int success = 0; |
| int failed = 0; |
| int skipped = 0; |
| logger.info("Attempting to revoke certificate alias: " + certificateAlias + " from " + maps.size() + " hosts"); |
| for (DirectDownloadCertificateHostMapVO map : maps) { |
| Long mappingHostId = map.getHostId(); |
| HostVO host = hostDao.findById(mappingHostId); |
| HostCertificateStatus hostStatus; |
| if (host == null || host.getDataCenterId() != zoneId || host.getHypervisorType() != HypervisorType.KVM) { |
| if (host != null) { |
| String reason = host.getDataCenterId() != zoneId ? "Host is not in the zone " + zoneId : "Host hypervisor is not KVM"; |
| logger.debug("Skipping host " + host.getName() + ": " + reason); |
| hostStatus = new HostCertificateStatus(CertificateStatus.SKIPPED, host, reason); |
| hostsList.add(hostStatus); |
| } |
| skipped++; |
| continue; |
| } |
| Pair<Boolean, String> result = revokeCertificateAliasFromHost(certificateAlias, mappingHostId); |
| if (!result.first()) { |
| String msg = "Could not revoke certificate from host: " + mappingHostId + ": " + result.second(); |
| logger.error(msg); |
| hostStatus = new HostCertificateStatus(CertificateStatus.FAILED, host, result.second()); |
| failed++; |
| } else { |
| logger.info("Certificate " + certificateAlias + " revoked from host " + mappingHostId); |
| map.setRevoked(true); |
| hostStatus = new HostCertificateStatus(CertificateStatus.REVOKED, host, null); |
| success++; |
| directDownloadCertificateHostMapDao.update(map.getId(), map); |
| } |
| hostsList.add(hostStatus); |
| } |
| logger.info(String.format("Certificate alias %s revoked from: %d hosts, %d failed, %d skipped", |
| certificateAlias, success, failed, skipped)); |
| return hostsList; |
| } |
| |
| @Override |
| public List<DirectDownloadCertificate> listDirectDownloadCertificates(Long certificateId, Long zoneId) { |
| if (zoneId != null && dataCenterDao.findById(zoneId) == null) { |
| throw new InvalidParameterValueException("Cannot find a zone with ID = " + zoneId); |
| } |
| List<DirectDownloadCertificate> certificates = new LinkedList<>(); |
| if (certificateId != null) { |
| certificates.add(directDownloadCertificateDao.findById(certificateId)); |
| } else if (zoneId != null) { |
| certificates.addAll(directDownloadCertificateDao.listByZone(zoneId)); |
| } else { |
| certificates.addAll(directDownloadCertificateDao.listAll()); |
| } |
| return certificates; |
| } |
| |
| @Override |
| public List<DirectDownloadCertificateHostMap> getCertificateHostsMapping(Long certificateId) { |
| if (certificateId == null) { |
| throw new InvalidParameterValueException("Please specify a certificate ID"); |
| } |
| return new LinkedList<>(directDownloadCertificateHostMapDao.listByCertificateId(certificateId)); |
| } |
| |
| protected Pair<Boolean, String> revokeCertificateAliasFromHost(String alias, Long hostId) { |
| RevokeDirectDownloadCertificateCommand cmd = new RevokeDirectDownloadCertificateCommand(alias); |
| try { |
| Answer answer = agentManager.send(hostId, cmd); |
| return new Pair<>(answer != null && answer.getResult(), answer != null ? answer.getDetails() : ""); |
| } catch (AgentUnavailableException | OperationTimedoutException e) { |
| logger.error("Error revoking certificate " + alias + " from host " + hostId, e); |
| return new Pair<>(false, e.getMessage()); |
| } |
| } |
| |
| @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), |
| 1L, DirectDownloadCertificateUploadInterval.value(), TimeUnit.HOURS); |
| } |
| return true; |
| } |
| |
| @Override |
| public String getConfigComponentName() { |
| return DirectDownloadManager.class.getSimpleName(); |
| } |
| |
| @Override |
| public ConfigKey<?>[] getConfigKeys() { |
| return new ConfigKey<?>[]{ |
| DirectDownloadCertificateUploadInterval, |
| DirectDownloadConnectTimeout, |
| DirectDownloadSocketTimeout, |
| DirectDownloadConnectionRequestTimeout |
| }; |
| } |
| |
| 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 (logger.isTraceEnabled()) { |
| 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) { |
| logger.debug("Certificate " + certificateVO.getId() + |
| " (" + certificateVO.getAlias() + ") was not uploaded to host: " + hostVO.getId() + |
| " uploading it"); |
| Pair<Boolean, String> result = directDownloadManager.provisionCertificate(certificateVO.getId(), hostVO.getId()); |
| logger.debug("Certificate " + certificateVO.getAlias() + " " + |
| (result.first() ? "uploaded" : "could not be uploaded") + |
| " to host " + hostVO.getId()); |
| if (!result.first()) { |
| logger.error("Certificate " + certificateVO.getAlias() + " failed: " + result.second()); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } catch (final Throwable t) { |
| logger.error("Error trying to run Direct Download background task", t); |
| } |
| } |
| } |
| } |