diff --git a/api/src/main/java/org/apache/cloudstack/api/response/StoragePoolResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/StoragePoolResponse.java index bd468a9201f..06d5103d731 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/StoragePoolResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/StoragePoolResponse.java @@ -141,6 +141,10 @@ public class StoragePoolResponse extends BaseResponseWithAnnotations { @Param(description = "the storage pool capabilities") private Map caps; + @SerializedName(ApiConstants.MANAGED) + @Param(description = "whether this pool is managed or not") + private Boolean managed; + public Map getCaps() { return caps; } @@ -383,4 +387,12 @@ public class StoragePoolResponse extends BaseResponseWithAnnotations { public void setTagARule(Boolean tagARule) { isTagARule = tagARule; } + + public Boolean getManaged() { + return managed; + } + + public void setManaged(Boolean managed) { + this.managed = managed; + } } diff --git a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java index 39d4618f81c..518a3869833 100644 --- a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java +++ b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java @@ -533,11 +533,6 @@ public class TemplateServiceImpl implements TemplateService { logger.info("Skip downloading template " + tmplt.getUniqueName() + " since no url is specified."); continue; } - // if this is private template, skip sync to a new image store - if (isSkipTemplateStoreDownload(tmplt, zoneId)) { - logger.info("Skip sync downloading private template " + tmplt.getUniqueName() + " to a new image store"); - continue; - } // if this is a region store, and there is already an DOWNLOADED entry there without install_path information, which // means that this is a duplicate entry from migration of previous NFS to staging. diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md new file mode 100644 index 00000000000..30f0225b45e --- /dev/null +++ b/plugins/storage/volume/linstor/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to Linstor CloudStack plugin will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2024-08-27] + +### Changed + +- Allow two primaries(+protocol c) is now set on resource-connection level instead of rd diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java index 5d037d63433..cc411543c44 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.hypervisor.kvm.storage; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -26,6 +27,7 @@ import javax.annotation.Nonnull; import com.cloud.storage.Storage; import com.cloud.utils.exception.CloudRuntimeException; + import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; @@ -44,8 +46,8 @@ import com.linbit.linstor.api.model.ApiCallRcList; import com.linbit.linstor.api.model.Properties; import com.linbit.linstor.api.model.ProviderKind; import com.linbit.linstor.api.model.Resource; +import com.linbit.linstor.api.model.ResourceConnectionModify; import com.linbit.linstor.api.model.ResourceDefinition; -import com.linbit.linstor.api.model.ResourceDefinitionModify; import com.linbit.linstor.api.model.ResourceGroupSpawn; import com.linbit.linstor.api.model.ResourceMakeAvailable; import com.linbit.linstor.api.model.ResourceWithVolumes; @@ -242,15 +244,19 @@ public class LinstorStorageAdaptor implements StorageAdaptor { * @throws ApiException if any problem connecting to the Linstor controller */ private void allow2PrimariesIfInUse(DevelopersApi api, String rscName) throws ApiException { - if (LinstorUtil.isResourceInUse(api, rscName)) { + String inUseNode = LinstorUtil.isResourceInUse(api, rscName); + if (inUseNode != null && !inUseNode.equalsIgnoreCase(localNodeName)) { // allow 2 primaries for live migration, should be removed by disconnect on the other end - ResourceDefinitionModify rdm = new ResourceDefinitionModify(); + ResourceConnectionModify rcm = new ResourceConnectionModify(); Properties props = new Properties(); props.put("DrbdOptions/Net/allow-two-primaries", "yes"); - rdm.setOverrideProps(props); - ApiCallRcList answers = api.resourceDefinitionModify(rscName, rdm); + props.put("DrbdOptions/Net/protocol", "C"); + rcm.setOverrideProps(props); + ApiCallRcList answers = api.resourceConnectionModify(rscName, inUseNode, localNodeName, rcm); if (answers.hasError()) { - logger.error("Unable to set 'allow-two-primaries' on {} ", rscName); + logger.error(String.format( + "Unable to set protocol C and 'allow-two-primaries' on %s/%s/%s", + inUseNode, localNodeName, rscName)); // do not fail here as adding allow-two-primaries property is only a problem while live migrating } } @@ -290,6 +296,23 @@ public class LinstorStorageAdaptor implements StorageAdaptor { return true; } + private void removeTwoPrimariesRcProps(DevelopersApi api, String inUseNode, String rscName) throws ApiException { + ResourceConnectionModify rcm = new ResourceConnectionModify(); + List deleteProps = new ArrayList<>(); + deleteProps.add("DrbdOptions/Net/allow-two-primaries"); + deleteProps.add("DrbdOptions/Net/protocol"); + rcm.deleteProps(deleteProps); + ApiCallRcList answers = api.resourceConnectionModify(rscName, localNodeName, inUseNode, rcm); + if (answers.hasError()) { + logger.error( + String.format("Failed to remove 'protocol' and 'allow-two-primaries' on %s/%s/%s: %s", + localNodeName, + inUseNode, + rscName, LinstorUtil.getBestErrorMessage(answers))); + // do not fail here as removing allow-two-primaries property isn't fatal + } + } + private boolean tryDisconnectLinstor(String volumePath, KVMStoragePool pool) { if (volumePath == null) { @@ -319,9 +342,18 @@ public class LinstorStorageAdaptor implements StorageAdaptor { if (optRsc.isPresent()) { + Resource rsc = optRsc.get(); try { - Resource rsc = optRsc.get(); + String inUseNode = LinstorUtil.isResourceInUse(api, rsc.getName()); + if (inUseNode != null && !inUseNode.equalsIgnoreCase(localNodeName)) { + removeTwoPrimariesRcProps(api, inUseNode, rsc.getName()); + } + } catch (ApiException apiEx) { + logger.error(apiEx.getBestMessage()); + // do not fail here as removing allow-two-primaries property or deleting diskless isn't fatal + } + try { // if diskless resource remove it, in the worst case it will be transformed to a tiebreaker if (rsc.getFlags() != null && rsc.getFlags().contains(ApiConsts.FLAG_DRBD_DISKLESS) && @@ -329,17 +361,6 @@ public class LinstorStorageAdaptor implements StorageAdaptor { ApiCallRcList delAnswers = api.resourceDelete(rsc.getName(), localNodeName); logLinstorAnswers(delAnswers); } - - // remove allow-two-primaries - ResourceDefinitionModify rdm = new ResourceDefinitionModify(); - rdm.deleteProps(Collections.singletonList("DrbdOptions/Net/allow-two-primaries")); - ApiCallRcList answers = api.resourceDefinitionModify(rsc.getName(), rdm); - if (answers.hasError()) { - logger.error( - String.format("Failed to remove 'allow-two-primaries' on %s: %s", - rsc.getName(), LinstorUtil.getBestErrorMessage(answers))); - // do not fail here as removing allow-two-primaries property isn't fatal - } } catch (ApiException apiEx) { logger.error(apiEx.getBestMessage()); // do not fail here as removing allow-two-primaries property or deleting diskless isn't fatal diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java index 8fdba049603..49b8655ed63 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java @@ -192,17 +192,20 @@ public class LinstorUtil { * * @param api developer api object to use * @param rscName resource name to check in use state. - * @return True if a resource found that is in use(primary) state, else false. + * @return NodeName where the resource is inUse, if not in use `null` * @throws ApiException forwards api errors */ - public static boolean isResourceInUse(DevelopersApi api, String rscName) throws ApiException { + public static String isResourceInUse(DevelopersApi api, String rscName) throws ApiException { List rscs = api.resourceList(rscName, null, null); if (rscs != null) { return rscs.stream() - .anyMatch(rsc -> rsc.getState() != null && Boolean.TRUE.equals(rsc.getState().isInUse())); + .filter(rsc -> rsc.getState() != null && Boolean.TRUE.equals(rsc.getState().isInUse())) + .map(Resource::getNodeName) + .findFirst() + .orElse(null); } LOGGER.error("isResourceInUse: null returned from resourceList"); - return false; + return null; } /** diff --git a/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java index e5c116463c2..c401cabe233 100644 --- a/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java @@ -172,6 +172,7 @@ public class StoragePoolJoinDaoImpl extends GenericDaoBase
* Possible secondary storage VM state transition cases:
@@ -399,6 +401,9 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar String[] cidrs = _allowedInternalSites.split(","); for (String cidr : cidrs) { if (NetUtils.isValidIp4Cidr(cidr) || NetUtils.isValidIp4(cidr) || !cidr.startsWith("0.0.0.0")) { + if (NetUtils.getCleanIp4Cidr(cidr).equals(cidr)) { + logger.warn(String.format("Invalid CIDR %s in %s", cidr, SecStorageAllowedInternalDownloadSites.key())); + } allowedCidrs.add(cidr); } } diff --git a/systemvm/debian/opt/cloud/bin/cs/CsFile.py b/systemvm/debian/opt/cloud/bin/cs/CsFile.py index bad9cd9537a..b33dbcff1c2 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsFile.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsFile.py @@ -153,7 +153,6 @@ class CsFile: logging.debug("Searching for %s string " % search) for index, line in enumerate(self.new_config): - print(' line = ' + line) if line.lstrip().startswith(ignoreLinesStartWith): continue if search in line: diff --git a/systemvm/debian/opt/cloud/bin/cs_vpnusers.py b/systemvm/debian/opt/cloud/bin/cs_vpnusers.py index 4a29cccdefb..e4d1906562d 100755 --- a/systemvm/debian/opt/cloud/bin/cs_vpnusers.py +++ b/systemvm/debian/opt/cloud/bin/cs_vpnusers.py @@ -22,8 +22,6 @@ import copy def merge(dbag, data): dbagc = copy.deepcopy(dbag) - print(dbag) - print(data) if "vpn_users" not in data: return dbagc diff --git a/ui/src/views/setting/ConfigurationValue.vue b/ui/src/views/setting/ConfigurationValue.vue index e6129f1a1d7..24dd0385741 100644 --- a/ui/src/views/setting/ConfigurationValue.vue +++ b/ui/src/views/setting/ConfigurationValue.vue @@ -274,7 +274,7 @@ export default { this.$message.error(this.$t('message.error.save.setting')) this.$notification.error({ message: this.$t('label.error'), - description: this.$t('message.error.save.setting') + description: error?.response?.data?.updateconfigurationresponse?.errortext || this.$t('message.error.save.setting') }) }).finally(() => { this.valueLoading = false 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 c75dcce3d0f..5fd843d6aab 100644 --- a/utils/src/main/java/com/cloud/utils/net/NetUtils.java +++ b/utils/src/main/java/com/cloud/utils/net/NetUtils.java @@ -628,6 +628,18 @@ public class NetUtils { return long2Ip(firstPart) + "/" + size; } + public static String getCleanIp4Cidr(final String cidr) { + if (!isValidIp4Cidr(cidr)) { + throw new CloudRuntimeException("Invalid CIDR: " + cidr); + } + String gateway = cidr.split("/")[0]; + Long netmaskSize = Long.parseLong(cidr.split("/")[1]); + final long ip = ip2Long(gateway); + final long startNetMask = ip2Long(getCidrNetmask(netmaskSize)); + final long start = (ip & startNetMask); + return String.format("%s/%s", long2Ip(start), netmaskSize); + } + public static String[] getIpRangeFromCidr(final String cidr, final long size) { assert size < MAX_CIDR : "You do know this is not for ipv6 right? Keep it smaller than 32 but you have " + size; final String[] result = new String[2]; diff --git a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java index ecb3b8ca8eb..e6a219b09a1 100644 --- a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java @@ -296,6 +296,17 @@ public class NetUtilsTest { assertTrue(NetUtils.isValidIp4Cidr(cidrThird));; } + @Test + public void testGetCleanIp4Cidr() throws Exception { + final String cidrFirst = "10.0.144.0/20"; + final String cidrSecond = "10.0.151.5/20"; + final String cidrThird = "10.0.144.10/21"; + + assertEquals(cidrFirst, NetUtils.getCleanIp4Cidr(cidrFirst)); + assertEquals("10.0.144.0/20", NetUtils.getCleanIp4Cidr(cidrSecond)); + assertEquals("10.0.144.0/21", NetUtils.getCleanIp4Cidr(cidrThird));; + } + @Test public void testIsValidCidrList() throws Exception { final String cidrFirst = "10.0.144.0/20,1.2.3.4/32,5.6.7.8/24";