From 39c8c4dbae98ec5695465d39c9ca1b6afba72354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20De=20Marco=20Gon=C3=A7alves?= Date: Sat, 5 Jul 2025 10:20:08 -0300 Subject: [PATCH 1/5] Normalize naming of Kubernetes clusters (#10778) --- ...esClusterResourceModifierActionWorker.java | 38 +++-- ...usterResourceModifierActionWorkerTest.java | 138 ++++++++++++++++++ .../java/com/cloud/utils/net/NetUtils.java | 22 ++- 3 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorkerTest.java diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java index 60cd9a2dff4..667bc00605b 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java @@ -153,6 +153,8 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu protected String kubernetesClusterNodeNamePrefix; + private static final int MAX_CLUSTER_PREFIX_LENGTH = 43; + protected KubernetesClusterResourceModifierActionWorker(final KubernetesCluster kubernetesCluster, final KubernetesClusterManagerImpl clusterManager) { super(kubernetesCluster, clusterManager); } @@ -772,19 +774,35 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu } } + /** + * Generates a valid name prefix for Kubernetes cluster nodes. + * + *

The prefix must comply with Kubernetes naming constraints: + *

+ * + *

The generated prefix is limited to 43 characters to accommodate the full node naming pattern: + *

{'prefix'}-{'control' | 'node'}-{'11-digit-hash'}
+ * + * @return A valid node name prefix, truncated if necessary + * @see Kubernetes "Object Names and IDs" documentation + */ protected String getKubernetesClusterNodeNamePrefix() { - String prefix = kubernetesCluster.getName(); - if (!NetUtils.verifyDomainNameLabel(prefix, true)) { - prefix = prefix.replaceAll("[^a-zA-Z0-9-]", ""); - if (prefix.length() == 0) { - prefix = kubernetesCluster.getUuid(); - } - prefix = "k8s-" + prefix; + String prefix = kubernetesCluster.getName().toLowerCase(); + + if (NetUtils.verifyDomainNameLabel(prefix, true)) { + return StringUtils.truncate(prefix, MAX_CLUSTER_PREFIX_LENGTH); } - if (prefix.length() > 40) { - prefix = prefix.substring(0, 40); + + prefix = prefix.replaceAll("[^a-z0-9-]", ""); + if (prefix.isEmpty()) { + prefix = kubernetesCluster.getUuid(); } - return prefix; + return StringUtils.truncate("k8s-" + prefix, MAX_CLUSTER_PREFIX_LENGTH); } protected KubernetesClusterVO updateKubernetesClusterEntry(final Long cores, final Long memory, final Long size, diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorkerTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorkerTest.java new file mode 100644 index 00000000000..c220a3468af --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorkerTest.java @@ -0,0 +1,138 @@ +// 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.kubernetes.cluster.actionworkers; + +import com.cloud.kubernetes.cluster.KubernetesCluster; +import com.cloud.kubernetes.cluster.KubernetesClusterManagerImpl; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterDetailsDao; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; +import com.cloud.kubernetes.version.dao.KubernetesSupportedVersionDao; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class KubernetesClusterResourceModifierActionWorkerTest { + @Mock + private KubernetesClusterDao kubernetesClusterDaoMock; + + @Mock + private KubernetesClusterDetailsDao kubernetesClusterDetailsDaoMock; + + @Mock + private KubernetesClusterVmMapDao kubernetesClusterVmMapDaoMock; + + @Mock + private KubernetesSupportedVersionDao kubernetesSupportedVersionDaoMock; + + @Mock + private KubernetesClusterManagerImpl kubernetesClusterManagerMock; + + @Mock + private KubernetesCluster kubernetesClusterMock; + + private KubernetesClusterResourceModifierActionWorker kubernetesClusterResourceModifierActionWorker; + + @Before + public void setUp() { + kubernetesClusterManagerMock.kubernetesClusterDao = kubernetesClusterDaoMock; + kubernetesClusterManagerMock.kubernetesSupportedVersionDao = kubernetesSupportedVersionDaoMock; + kubernetesClusterManagerMock.kubernetesClusterDetailsDao = kubernetesClusterDetailsDaoMock; + kubernetesClusterManagerMock.kubernetesClusterVmMapDao = kubernetesClusterVmMapDaoMock; + + kubernetesClusterResourceModifierActionWorker = new KubernetesClusterResourceModifierActionWorker(kubernetesClusterMock, kubernetesClusterManagerMock); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestReturnOriginalPrefixWhenNamingAllRequirementsAreMet() { + String originalPrefix = "k8s-cluster-01"; + String expectedPrefix = "k8s-cluster-01"; + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldOnlyContainLowerCaseCharacters() { + String originalPrefix = "k8s-CLUSTER-01"; + String expectedPrefix = "k8s-cluster-01"; + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldBeTruncatedWhenRequired() { + int maxPrefixLength = 43; + + String originalPrefix = "c".repeat(maxPrefixLength + 1); + String expectedPrefix = "c".repeat(maxPrefixLength); + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + String normalizedPrefix = kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix(); + Assert.assertEquals(expectedPrefix, normalizedPrefix); + Assert.assertEquals(maxPrefixLength, normalizedPrefix.length()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldBeTruncatedWhenRequiredAndWhenOriginalPrefixIsInvalid() { + int maxPrefixLength = 43; + + String originalPrefix = "1!" + "c".repeat(maxPrefixLength); + String expectedPrefix = "k8s-1" + "c".repeat(maxPrefixLength - 5); + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + String normalizedPrefix = kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix(); + Assert.assertEquals(expectedPrefix, normalizedPrefix); + Assert.assertEquals(maxPrefixLength, normalizedPrefix.length()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldOnlyIncludeAlphanumericCharactersAndHyphen() { + String originalPrefix = "Cluster!@#$%^&*()_+?.-01|<>"; + String expectedPrefix = "k8s-cluster-01"; + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldContainClusterUuidWhenAllCharactersAreInvalid() { + String clusterUuid = "2699b547-cb56-4a59-a2c6-331cfb21d2e4"; + String originalPrefix = "!@#$%^&*()_+?.|<>"; + String expectedPrefix = "k8s-" + clusterUuid; + + Mockito.when(kubernetesClusterMock.getUuid()).thenReturn(clusterUuid); + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldNotStartWithADigit() { + String originalPrefix = "1 cluster"; + String expectedPrefix = "k8s-1cluster"; + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } +} diff --git a/utils/src/main/java/com/cloud/utils/net/NetUtils.java b/utils/src/main/java/com/cloud/utils/net/NetUtils.java index 47c35eaada1..802b84f6a1c 100644 --- a/utils/src/main/java/com/cloud/utils/net/NetUtils.java +++ b/utils/src/main/java/com/cloud/utils/net/NetUtils.java @@ -1055,13 +1055,23 @@ public class NetUtils { return Integer.toString(portRange[0]) + ":" + Integer.toString(portRange[1]); } + /** + * Validates a domain name. + * + *

Domain names must satisfy the following constraints: + *

+ * + * @param hostName The domain name to validate + * @param isHostName If true, verifies whether the domain name starts with a digit + * @return true if the domain name is valid, false otherwise + */ public static boolean verifyDomainNameLabel(final String hostName, final boolean isHostName) { - // must be between 1 and 63 characters long and may contain only the ASCII letters 'a' through 'z' (in a - // case-insensitive manner), - // the digits '0' through '9', and the hyphen ('-'). - // Can not start with a hyphen and digit, and must not end with a hyphen - // If it's a host name, don't allow to start with digit - if (hostName.length() > 63 || hostName.length() < 1) { s_logger.warn("Domain name label must be between 1 and 63 characters long"); return false; From e47b78b2bbb4842625f7bf311a045815ed0badaa Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 7 Jul 2025 13:36:16 +0200 Subject: [PATCH 2/5] directdownload: fix keytool importcert (#11113) * directdownload: fix keytool importcert ``` $ /usr/bin/keytool -importcert file /etc/cloudstack/agent/CSCERTIFICATE-full -keystore /etc/cloudstack/agent/cloud.jks -alias full -storepass DAWsfkJeeGrmhta6 Illegal option: file keytool -importcert [OPTION]... Imports a certificate or a certificate chain Options: -noprompt do not prompt -trustcacerts trust certificates from cacerts -protected password through protected mechanism -alias alias name of the entry to process -file input file name -keypass key password -keystore keystore name -cacerts access the cacerts keystore -storepass keystore password -storetype keystore type -providername provider name -addprovider add security provider by name (e.g. SunPKCS11) [-providerarg ] configure argument for -addprovider -providerclass add security provider by fully-qualified class name [-providerarg ] configure argument for -providerclass -providerpath provider classpath -v verbose output Use "keytool -?, -h, or --help" for this help message ``` * DirectDownload: drop HttpsMultiTrustManager --- .../HttpsDirectTemplateDownloader.java | 11 +- .../download/HttpsMultiTrustManager.java | 102 ------------------ ...rectDownloadCertificateCommandWrapper.java | 2 +- 3 files changed, 6 insertions(+), 109 deletions(-) delete mode 100644 core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java index 3a48ade4cd8..e3c74213d74 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java @@ -39,9 +39,7 @@ import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import org.apache.cloudstack.utils.security.SSLUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.io.IOUtils; @@ -55,6 +53,7 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import com.cloud.utils.Pair; @@ -120,10 +119,10 @@ public class HttpsDirectTemplateDownloader extends DirectTemplateDownloaderImpl String password = "changeit"; defaultKeystore.load(is, password.toCharArray()); } - TrustManager[] tm = HttpsMultiTrustManager.getTrustManagersFromKeyStores(customKeystore, defaultKeystore); - SSLContext sslContext = SSLUtils.getSSLContext(); - sslContext.init(null, tm, null); - return sslContext; + return SSLContexts.custom() + .loadTrustMaterial(customKeystore, null) + .loadTrustMaterial(defaultKeystore, null) + .build(); } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | KeyManagementException e) { s_logger.error(String.format("Failure getting SSL context for HTTPS downloader, using default SSL context: %s", e.getMessage()), e); try { diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java deleted file mode 100644 index fe47847c36c..00000000000 --- a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java +++ /dev/null @@ -1,102 +0,0 @@ -// 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 java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; - -public class HttpsMultiTrustManager implements X509TrustManager { - - private final List trustManagers; - - public HttpsMultiTrustManager(KeyStore... keystores) { - List trustManagers = new ArrayList<>(); - trustManagers.add(getTrustManager(null)); - for (KeyStore keystore : keystores) { - trustManagers.add(getTrustManager(keystore)); - } - this.trustManagers = ImmutableList.copyOf(trustManagers); - } - - public static TrustManager[] getTrustManagersFromKeyStores(KeyStore... keyStore) { - return new TrustManager[] { new HttpsMultiTrustManager(keyStore) }; - - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - for (X509TrustManager trustManager : trustManagers) { - try { - trustManager.checkClientTrusted(chain, authType); - return; - } catch (CertificateException ignored) {} - } - throw new CertificateException("None of the TrustManagers trust this certificate chain"); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - for (X509TrustManager trustManager : trustManagers) { - try { - trustManager.checkServerTrusted(chain, authType); - return; - } catch (CertificateException ignored) {} - } - throw new CertificateException("None of the TrustManagers trust this certificate chain"); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - ImmutableList.Builder certificates = ImmutableList.builder(); - for (X509TrustManager trustManager : trustManagers) { - for (X509Certificate cert : trustManager.getAcceptedIssuers()) { - certificates.add(cert); - } - } - return Iterables.toArray(certificates.build(), X509Certificate.class); - } - - public X509TrustManager getTrustManager(KeyStore keystore) { - return getTrustManager(TrustManagerFactory.getDefaultAlgorithm(), keystore); - } - - public X509TrustManager getTrustManager(String algorithm, KeyStore keystore) { - TrustManagerFactory factory; - try { - factory = TrustManagerFactory.getInstance(algorithm); - factory.init(keystore); - return Iterables.getFirst(Iterables.filter( - Arrays.asList(factory.getTrustManagers()), X509TrustManager.class), null); - } catch (NoSuchAlgorithmException | KeyStoreException e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java index d2b69412a72..1a8de7a8c5b 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java @@ -86,7 +86,7 @@ public class LibvirtSetupDirectDownloadCertificateCommandWrapper extends Command private void importCertificate(String tempCerFilePath, String keyStoreFile, String certificateName, String privatePassword) { s_logger.debug("Importing certificate from temporary file to keystore"); String keyToolPath = Script.getExecutableAbsolutePath("keytool"); - int result = Script.executeCommandForExitValue(keyToolPath, "-importcert", "file", tempCerFilePath, + int result = Script.executeCommandForExitValue(keyToolPath, "-importcert", "-file", tempCerFilePath, "-keystore", keyStoreFile, "-alias", sanitizeBashCommandArgument(certificateName), "-storepass", privatePassword, "-noprompt"); if (result != 0) { From c782835f01241be8ffdca4d6afa9896990e994e6 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Wed, 9 Jul 2025 04:41:34 -0300 Subject: [PATCH 3/5] [Vmware to KVM Migration] Fix issue with vCenter Standalone hosts for VM listing (#11091) --- .../cloud/hypervisor/vmware/mo/BaseMO.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/BaseMO.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/BaseMO.java index 0d380bd7ff7..3f27f6c80fe 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/BaseMO.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/BaseMO.java @@ -251,8 +251,9 @@ public class BaseMO { hostClusterPair = hostClusterNamesMap.get(hostMorValue); } else { HostMO hostMO = new HostMO(_context, hostMor); - ClusterMO clusterMO = new ClusterMO(_context, hostMO.getHyperHostCluster()); - hostClusterPair = new Pair<>(hostMO.getHostName(), clusterMO.getName()); + String hostName = hostMO.getHostName(); + String clusterName = getClusterNameFromHostIncludingStandaloneHosts(hostMO, hostName); + hostClusterPair = new Pair<>(hostName, clusterName); hostClusterNamesMap.put(hostMorValue, hostClusterPair); } vm.setHostName(hostClusterPair.first()); @@ -260,4 +261,20 @@ public class BaseMO { } } + /** + * Return the cluster name of the host on the vCenter + * @return null in case the host is standalone (doesn't belong to a cluster), cluster name otherwise + */ + private String getClusterNameFromHostIncludingStandaloneHosts(HostMO hostMO, String hostName) { + try { + ClusterMO clusterMO = new ClusterMO(_context, hostMO.getHyperHostCluster()); + return clusterMO.getName(); + } catch (Exception e) { + String msg = String.format("Cannot find a cluster for host %s, assuming standalone host, " + + "setting its cluster name as empty", hostName); + s_logger.info(msg); + return null; + } + } + } From 67a1ea35f4e40f8f81acdc99a6624f0c8590b564 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 10 Jul 2025 03:44:46 +0200 Subject: [PATCH 4/5] .github: restrict codecov in UI build to apache/cloudstack repo (#11158) --- .github/workflows/ui.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index 476526aff32..1c2d2c07fc0 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -56,6 +56,7 @@ jobs: npm run test:unit - uses: codecov/codecov-action@v4 + if: github.repository == 'apache/cloudstack' with: working-directory: ui files: ./coverage/lcov.info From 7715b3dc29876e6a414b9269a0255fb3c3ceff1f Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Thu, 10 Jul 2025 13:09:13 +0530 Subject: [PATCH 5/5] Improve the error throws when a template to owned by a non root-admin is registered for all zones. (#11170) --- .../src/main/java/com/cloud/template/TemplateAdapterBase.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/cloud/template/TemplateAdapterBase.java b/server/src/main/java/com/cloud/template/TemplateAdapterBase.java index 74347d1c057..905f4f6124d 100644 --- a/server/src/main/java/com/cloud/template/TemplateAdapterBase.java +++ b/server/src/main/java/com/cloud/template/TemplateAdapterBase.java @@ -192,7 +192,8 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat if (!isAdmin && zoneIdList == null && !isRegionStore ) { // domain admin and user should also be able to register template on a region store - throw new InvalidParameterValueException("Please specify a valid zone Id. Only admins can create templates in all zones."); + throw new InvalidParameterValueException("Template registered for 'All zones' can only be owned a Root Admin account. " + + "Please select specific zone(s)."); } // check for the url format only when url is not null. url can be null incase of form based upload