This is an automated email from the ASF dual-hosted git repository. rohit pushed a commit to branch 4.19 in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.19 by this push: new 70b634fff21 Linstor: add HA support and small cleanups (#8407) 70b634fff21 is described below commit 70b634fff21bd01a634fd16df073289c2a6cfedd Author: Rene Peinthor <rene.peint...@linbit.com> AuthorDate: Tue Feb 13 06:46:12 2024 +0100 Linstor: add HA support and small cleanups (#8407) * linstor: Outline get storagepools from resourcegroup into function * linstor: move getHostname() to kvm/Pool and reimplement * linstor: implement CloudStack HA support --- .../kvm/storage/LinstorStorageAdaptor.java | 73 +----------- .../hypervisor/kvm/storage/LinstorStoragePool.java | 132 +++++++++++++++++++-- .../driver/LinstorPrimaryDataStoreDriverImpl.java | 2 +- .../datastore/provider/LinstorHostListener.java | 32 +++++ .../LinstorPrimaryDatastoreProviderImpl.java | 2 +- .../storage/datastore/util/LinstorUtil.java | 27 +++-- 6 files changed, 178 insertions(+), 90 deletions(-) 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 57acbace183..101e8d3597e 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,18 +16,16 @@ // under the License. package com.cloud.hypervisor.kvm.storage; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.StringJoiner; 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; @@ -35,8 +33,6 @@ import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.apache.log4j.Logger; import org.libvirt.LibvirtException; -import com.cloud.storage.Storage; -import com.cloud.utils.exception.CloudRuntimeException; import com.linbit.linstor.api.ApiClient; import com.linbit.linstor.api.ApiException; import com.linbit.linstor.api.Configuration; @@ -47,7 +43,6 @@ import com.linbit.linstor.api.model.Properties; import com.linbit.linstor.api.model.ProviderKind; import com.linbit.linstor.api.model.ResourceDefinition; import com.linbit.linstor.api.model.ResourceDefinitionModify; -import com.linbit.linstor.api.model.ResourceGroup; import com.linbit.linstor.api.model.ResourceGroupSpawn; import com.linbit.linstor.api.model.ResourceMakeAvailable; import com.linbit.linstor.api.model.ResourceWithVolumes; @@ -70,28 +65,6 @@ public class LinstorStorageAdaptor implements StorageAdaptor { return LinstorUtil.RSC_PREFIX + name; } - private String getHostname() { - // either there is already some function for that in the agent or a better way. - ProcessBuilder pb = new ProcessBuilder("hostname"); - try - { - String result; - Process p = pb.start(); - final BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); - - StringJoiner sj = new StringJoiner(System.getProperty("line.separator")); - reader.lines().iterator().forEachRemaining(sj::add); - result = sj.toString(); - - p.waitFor(); - p.destroy(); - return result.trim(); - } catch (IOException | InterruptedException exc) { - Thread.currentThread().interrupt(); - throw new CloudRuntimeException("Unable to run 'hostname' command."); - } - } - private void logLinstorAnswer(@Nonnull ApiCallRc answer) { if (answer.isError()) { s_logger.error(answer.getMessage()); @@ -122,7 +95,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { } public LinstorStorageAdaptor() { - localNodeName = getHostname(); + localNodeName = LinstorStoragePool.getHostname(); } @Override @@ -511,25 +484,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { DevelopersApi linstorApi = getLinstorAPI(pool); final String rscGroupName = pool.getResourceGroup(); try { - List<ResourceGroup> rscGrps = linstorApi.resourceGroupList( - Collections.singletonList(rscGroupName), - null, - null, - null); - - if (rscGrps.isEmpty()) { - final String errMsg = String.format("Linstor: Resource group '%s' not found", rscGroupName); - s_logger.error(errMsg); - throw new CloudRuntimeException(errMsg); - } - - List<StoragePool> storagePools = linstorApi.viewStoragePools( - Collections.emptyList(), - rscGrps.get(0).getSelectFilter().getStoragePoolList(), - null, - null, - null - ); + List<StoragePool> storagePools = LinstorUtil.getRscGroupStoragePools(linstorApi, rscGroupName); final long free = storagePools.stream() .filter(sp -> sp.getProviderKind() != ProviderKind.DISKLESS) @@ -547,25 +502,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { DevelopersApi linstorApi = getLinstorAPI(pool); final String rscGroupName = pool.getResourceGroup(); try { - List<ResourceGroup> rscGrps = linstorApi.resourceGroupList( - Collections.singletonList(rscGroupName), - null, - null, - null); - - if (rscGrps.isEmpty()) { - final String errMsg = String.format("Linstor: Resource group '%s' not found", rscGroupName); - s_logger.error(errMsg); - throw new CloudRuntimeException(errMsg); - } - - List<StoragePool> storagePools = linstorApi.viewStoragePools( - Collections.emptyList(), - rscGrps.get(0).getSelectFilter().getStoragePoolList(), - null, - null, - null - ); + List<StoragePool> storagePools = LinstorUtil.getRscGroupStoragePools(linstorApi, rscGroupName); final long used = storagePools.stream() .filter(sp -> sp.getProviderKind() != ProviderKind.DISKLESS) diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java index d0309521874..4077d5dadfd 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java @@ -19,20 +19,33 @@ package com.cloud.hypervisor.kvm.storage; import java.util.List; import java.util.Map; -import org.apache.cloudstack.utils.qemu.QemuImg; -import org.joda.time.Duration; - import com.cloud.agent.api.to.HostTO; +import com.cloud.agent.properties.AgentProperties; +import com.cloud.agent.properties.AgentPropertiesFileHandler; import com.cloud.hypervisor.kvm.resource.KVMHABase.HAStoragePool; import com.cloud.storage.Storage; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.log4j.Logger; +import org.joda.time.Duration; public class LinstorStoragePool implements KVMStoragePool { + private static final Logger s_logger = Logger.getLogger(LinstorStoragePool.class); private final String _uuid; private final String _sourceHost; private final int _sourcePort; private final Storage.StoragePoolType _storagePoolType; private final StorageAdaptor _storageAdaptor; private final String _resourceGroup; + private final String localNodeName; public LinstorStoragePool(String uuid, String host, int port, String resourceGroup, Storage.StoragePoolType storagePoolType, StorageAdaptor storageAdaptor) { @@ -42,6 +55,7 @@ public class LinstorStoragePool implements KVMStoragePool { _storagePoolType = storagePoolType; _storageAdaptor = storageAdaptor; _resourceGroup = resourceGroup; + localNodeName = getHostname(); } @Override @@ -200,32 +214,132 @@ public class LinstorStoragePool implements KVMStoragePool { @Override public boolean isPoolSupportHA() { - return false; + return true; } @Override public String getHearthBeatPath() { - return null; + String kvmScriptsDir = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_SCRIPTS_DIR); + return Script.findScript(kvmScriptsDir, "kvmspheartbeat.sh"); } @Override - public String createHeartBeatCommand(HAStoragePool primaryStoragePool, String hostPrivateIp, + public String createHeartBeatCommand(HAStoragePool pool, String hostPrivateIp, boolean hostValidation) { - return null; + s_logger.trace(String.format("Linstor.createHeartBeatCommand: %s, %s, %b", pool.getPoolIp(), hostPrivateIp, hostValidation)); + boolean isStorageNodeUp = checkingHeartBeat(pool, null); + if (!isStorageNodeUp && !hostValidation) { + //restart the host + s_logger.debug(String.format("The host [%s] will be restarted because the health check failed for the storage pool [%s]", hostPrivateIp, pool.getPool().getType())); + Script cmd = new Script(pool.getPool().getHearthBeatPath(), Duration.millis(HeartBeatUpdateTimeout), s_logger); + cmd.add("-c"); + cmd.execute(); + return "Down"; + } + return isStorageNodeUp ? null : "Down"; } @Override public String getStorageNodeId() { + // only called by storpool return null; } + static String getHostname() { + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + Script sc = new Script("hostname", Duration.millis(10000L), s_logger); + String res = sc.execute(parser); + if (res != null) { + throw new CloudRuntimeException(String.format("Unable to run 'hostname' command: %s", res)); + } + String response = parser.getLines(); + return response.trim(); + } + @Override public Boolean checkingHeartBeat(HAStoragePool pool, HostTO host) { - return null; + String hostName; + if (host == null) { + hostName = localNodeName; + } else { + hostName = host.getParent(); + if (hostName == null) { + s_logger.error("No hostname set in host.getParent()"); + return false; + } + } + + return checkHostUpToDateAndConnected(hostName); + } + + private String executeDrbdSetupStatus(OutputInterpreter.AllLinesParser parser) { + Script sc = new Script("drbdsetup", Duration.millis(HeartBeatUpdateTimeout), s_logger); + sc.add("status"); + sc.add("--json"); + return sc.execute(parser); + } + + private boolean checkDrbdSetupStatusOutput(String output, String otherNodeName) { + JsonParser jsonParser = new JsonParser(); + JsonArray jResources = (JsonArray) jsonParser.parse(output); + for (JsonElement jElem : jResources) { + JsonObject jRes = (JsonObject) jElem; + JsonArray jConnections = jRes.getAsJsonArray("connections"); + for (JsonElement jConElem : jConnections) { + JsonObject jConn = (JsonObject) jConElem; + if (jConn.getAsJsonPrimitive("name").getAsString().equals(otherNodeName) + && jConn.getAsJsonPrimitive("connection-state").getAsString().equalsIgnoreCase("Connected")) { + return true; + } + } + } + s_logger.warn(String.format("checkDrbdSetupStatusOutput: no resource connected to %s.", otherNodeName)); + return false; + } + + private String executeDrbdEventsNow(OutputInterpreter.AllLinesParser parser) { + Script sc = new Script("drbdsetup", Duration.millis(HeartBeatUpdateTimeout), s_logger); + sc.add("events2"); + sc.add("--now"); + return sc.execute(parser); + } + + private boolean checkDrbdEventsNowOutput(String output) { + boolean healthy = output.lines().noneMatch(line -> line.matches(".*role:Primary .* promotion_score:0.*")); + if (!healthy) { + s_logger.warn("checkDrbdEventsNowOutput: primary resource with promotion score==0; HA false"); + } + return healthy; + } + + private boolean checkHostUpToDateAndConnected(String hostName) { + s_logger.trace(String.format("checkHostUpToDateAndConnected: %s/%s", localNodeName, hostName)); + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + + if (localNodeName.equalsIgnoreCase(hostName)) { + String res = executeDrbdEventsNow(parser); + if (res != null) { + return false; + } + return checkDrbdEventsNowOutput(parser.getLines()); + } else { + // check drbd connections + String res = executeDrbdSetupStatus(parser); + if (res != null) { + return false; + } + try { + return checkDrbdSetupStatusOutput(parser.getLines(), hostName); + } catch (JsonIOException | JsonSyntaxException e) { + s_logger.error("Error parsing drbdsetup status --json", e); + } + } + return false; } @Override public Boolean vmActivityCheck(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration) { - return null; + s_logger.trace(String.format("Linstor.vmActivityCheck: %s, %s", pool.getPoolIp(), host.getPrivateNetwork().getIp())); + return checkingHeartBeat(pool, host); } } diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java index 9b493ff01b9..ca98dc3dd46 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java @@ -1241,7 +1241,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver @Override public boolean isStorageSupportHA(StoragePoolType type) { - return false; + return true; } @Override diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorHostListener.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorHostListener.java new file mode 100644 index 00000000000..534431ed681 --- /dev/null +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorHostListener.java @@ -0,0 +1,32 @@ +// 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.storage.datastore.provider; + +import com.cloud.exception.StorageConflictException; +import com.cloud.host.HostVO; + +public class LinstorHostListener extends DefaultHostListener { + @Override + public boolean hostConnect(long hostId, long poolId) throws StorageConflictException { + HostVO host = hostDao.findById(hostId); + if (host.getParent() == null) { + host.setParent(host.getName()); + hostDao.update(host.getId(), host); + } + return super.hostConnect(hostId, poolId); + } +} diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java index 563f542db37..962c84fffb1 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java @@ -48,7 +48,7 @@ public class LinstorPrimaryDatastoreProviderImpl implements PrimaryDataStoreProv public boolean configure(Map<String, Object> params) { lifecycle = ComponentContext.inject(LinstorPrimaryDataStoreLifeCycleImpl.class); driver = ComponentContext.inject(LinstorPrimaryDataStoreDriverImpl.class); - listener = ComponentContext.inject(DefaultHostListener.class); + listener = ComponentContext.inject(LinstorHostListener.class); return true; } 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 b6904b90b29..e953c94db22 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 @@ -136,28 +136,33 @@ public class LinstorUtil { return path; } - public static long getCapacityBytes(String linstorUrl, String rscGroupName) { - DevelopersApi linstorApi = getLinstorAPI(linstorUrl); - try { - List<ResourceGroup> rscGrps = linstorApi.resourceGroupList( + public static List<StoragePool> getRscGroupStoragePools(DevelopersApi api, String rscGroupName) + throws ApiException { + List<ResourceGroup> rscGrps = api.resourceGroupList( Collections.singletonList(rscGroupName), null, null, null); - if (rscGrps.isEmpty()) { - final String errMsg = String.format("Linstor: Resource group '%s' not found", rscGroupName); - s_logger.error(errMsg); - throw new CloudRuntimeException(errMsg); - } + if (rscGrps.isEmpty()) { + final String errMsg = String.format("Linstor: Resource group '%s' not found", rscGroupName); + s_logger.error(errMsg); + throw new CloudRuntimeException(errMsg); + } - List<StoragePool> storagePools = linstorApi.viewStoragePools( + return api.viewStoragePools( Collections.emptyList(), rscGrps.get(0).getSelectFilter().getStoragePoolList(), null, null, null - ); + ); + } + + public static long getCapacityBytes(String linstorUrl, String rscGroupName) { + DevelopersApi linstorApi = getLinstorAPI(linstorUrl); + try { + List<StoragePool> storagePools = getRscGroupStoragePools(linstorApi, rscGroupName); return storagePools.stream() .filter(sp -> sp.getProviderKind() != ProviderKind.DISKLESS)