| // |
| // 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 com.cloud.agent.AgentManager; |
| import com.cloud.agent.api.Answer; |
| import com.cloud.event.ActionEventUtils; |
| import com.cloud.event.EventTypes; |
| import com.cloud.event.EventVO; |
| 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.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.exception.CloudRuntimeException; |
| |
| 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.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.stream.Collectors; |
| import javax.inject.Inject; |
| |
| import com.cloud.utils.security.CertificateHelper; |
| import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand; |
| import org.apache.cloudstack.agent.directdownload.DirectDownloadAnswer; |
| import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand.DownloadProtocol; |
| 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.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.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 sun.security.x509.X509CertImpl; |
| |
| public class DirectDownloadManagerImpl extends ManagerBase implements DirectDownloadManager { |
| |
| private static final Logger s_logger = Logger.getLogger(DirectDownloadManagerImpl.class); |
| 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; |
| |
| @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 Arrays.asList(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 hosts to retry download having hostId as the first element |
| */ |
| protected Long[] getHostsToRetryOn(Long clusterId, long dataCenterId, HypervisorType hypervisorType, long hostId) { |
| List<Long> hostIds = getRunningHostIdsInTheSameCluster(clusterId, dataCenterId, hypervisorType, hostId); |
| return createHostIdsList(hostIds, hostId); |
| } |
| |
| @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); |
| |
| Answer answer = sendDirectDownloadCommand(cmd, template, poolId, host); |
| |
| VMTemplateStoragePoolVO sPoolRef = vmTemplatePoolDao.findByPoolTemplate(poolId, templateId); |
| if (sPoolRef == null) { |
| if (s_logger.isDebugEnabled()) { |
| s_logger.debug("Not found (templateId:" + templateId + " poolId: " + poolId + ") in template_spool_ref, persisting it"); |
| } |
| DirectDownloadAnswer ans = (DirectDownloadAnswer) answer; |
| sPoolRef = new VMTemplateStoragePoolVO(poolId, templateId); |
| 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); |
| } |
| } |
| |
| /** |
| * 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; |
| Long[] hostsToRetry = getHostsToRetryOn(host.getClusterId(), host.getDataCenterId(), host.getHypervisorType(), host.getId()); |
| int hostIndex = 0; |
| Answer answer = null; |
| Long hostToSendDownloadCmd = hostsToRetry[hostIndex]; |
| boolean continueRetrying = true; |
| while (!downloaded && retry > 0 && continueRetrying) { |
| s_logger.debug("Sending Direct download command to host " + hostToSendDownloadCmd); |
| answer = agentManager.easySend(hostToSendDownloadCmd, cmd); |
| if (answer != null) { |
| DirectDownloadAnswer ans = (DirectDownloadAnswer)answer; |
| downloaded = answer.getResult(); |
| continueRetrying = ans.isRetryOnOtherHosts(); |
| } |
| hostToSendDownloadCmd = hostsToRetry[(hostIndex + 1) % hostsToRetry.length]; |
| retry --; |
| } |
| if (!downloaded) { |
| logUsageEvent(template, poolId); |
| throw new CloudRuntimeException("Template " + template.getId() + " could not be downloaded on pool " + poolId + ", failing after trying on several hosts"); |
| } |
| 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"; |
| s_logger.error(description); |
| ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), template.getAccountId(), EventVO.LEVEL_INFO, event, description, 0); |
| } |
| |
| /** |
| * Return DirectDownloadCommand according to the protocol |
| */ |
| private DirectDownloadCommand getDirectDownloadCommandFromProtocol(DownloadProtocol protocol, String url, Long templateId, PrimaryDataStoreTO destPool, |
| String checksum, Map<String, String> httpHeaders) { |
| if (protocol.equals(DownloadProtocol.HTTP)) { |
| return new HttpDirectDownloadCommand(url, templateId, destPool, checksum, httpHeaders); |
| } else if (protocol.equals(DownloadProtocol.HTTPS)) { |
| return new HttpsDirectDownloadCommand(url, templateId, destPool, checksum, httpHeaders); |
| } 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); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * 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()); |
| } |
| |
| /** |
| * 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(); |
| s_logger.error(msg); |
| throw new CloudRuntimeException(msg); |
| } |
| if (x509Cert.getSubjectDN() != null) { |
| s_logger.debug("Valid certificate for domain name: " + x509Cert.getSubjectDN().getName()); |
| } |
| } |
| } |
| |
| @Override |
| public boolean uploadCertificateToHosts(String certificateCer, String alias, String hypervisor) { |
| if (alias != null && (alias.equalsIgnoreCase("cloud") || alias.startsWith("cloudca"))) { |
| throw new CloudRuntimeException("Please provide a different alias name for the certificate"); |
| } |
| |
| HypervisorType hypervisorType = HypervisorType.getType(hypervisor); |
| List<HostVO> hosts = getRunningHostsToUploadCertificate(hypervisorType); |
| |
| String certificatePem = getPretifiedCertificate(certificateCer); |
| certificateSanity(certificatePem); |
| |
| s_logger.info("Attempting to upload certificate: " + alias + " to " + hosts.size() + " hosts"); |
| int hostCount = 0; |
| if (CollectionUtils.isNotEmpty(hosts)) { |
| for (HostVO host : hosts) { |
| if (!uploadCertificate(certificatePem, alias, host.getId())) { |
| String msg = "Could not upload certificate " + alias + " on host: " + host.getName() + " (" + host.getUuid() + ")"; |
| s_logger.error(msg); |
| throw new CloudRuntimeException(msg); |
| } |
| hostCount++; |
| } |
| } |
| s_logger.info("Certificate was successfully uploaded to " + hostCount + " hosts"); |
| return true; |
| } |
| |
| /** |
| * 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); |
| Answer answer = agentManager.easySend(hostId, cmd); |
| if (answer == null || !answer.getResult()) { |
| String msg = "Certificate " + certificateName + " 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); |
| return true; |
| } |
| |
| } |