Skip to content

Commit ce06a0f

Browse files
committed
linstor: implement CloudStack HA support
1 parent 5337ae8 commit ce06a0f

File tree

4 files changed

+141
-11
lines changed

4 files changed

+141
-11
lines changed

plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,23 @@
1919
import java.util.List;
2020
import java.util.Map;
2121

22-
import org.apache.cloudstack.utils.qemu.QemuImg;
23-
import org.joda.time.Duration;
24-
2522
import com.cloud.agent.api.to.HostTO;
23+
import com.cloud.agent.properties.AgentProperties;
24+
import com.cloud.agent.properties.AgentPropertiesFileHandler;
2625
import com.cloud.hypervisor.kvm.resource.KVMHABase.HAStoragePool;
2726
import com.cloud.storage.Storage;
2827
import com.cloud.utils.exception.CloudRuntimeException;
2928
import com.cloud.utils.script.OutputInterpreter;
3029
import com.cloud.utils.script.Script;
30+
import com.google.gson.JsonArray;
31+
import com.google.gson.JsonElement;
32+
import com.google.gson.JsonIOException;
33+
import com.google.gson.JsonObject;
34+
import com.google.gson.JsonParser;
35+
import com.google.gson.JsonSyntaxException;
36+
import org.apache.cloudstack.utils.qemu.QemuImg;
3137
import org.apache.log4j.Logger;
38+
import org.joda.time.Duration;
3239

3340
public class LinstorStoragePool implements KVMStoragePool {
3441
private static final Logger s_logger = Logger.getLogger(LinstorStoragePool.class);
@@ -38,6 +45,7 @@ public class LinstorStoragePool implements KVMStoragePool {
3845
private final Storage.StoragePoolType _storagePoolType;
3946
private final StorageAdaptor _storageAdaptor;
4047
private final String _resourceGroup;
48+
private final String localNodeName;
4149

4250
public LinstorStoragePool(String uuid, String host, int port, String resourceGroup,
4351
Storage.StoragePoolType storagePoolType, StorageAdaptor storageAdaptor) {
@@ -47,6 +55,7 @@ public LinstorStoragePool(String uuid, String host, int port, String resourceGro
4755
_storagePoolType = storagePoolType;
4856
_storageAdaptor = storageAdaptor;
4957
_resourceGroup = resourceGroup;
58+
localNodeName = getHostname();
5059
}
5160

5261
@Override
@@ -205,22 +214,34 @@ public String getResourceGroup() {
205214

206215
@Override
207216
public boolean isPoolSupportHA() {
208-
return false;
217+
return true;
209218
}
210219

211220
@Override
212221
public String getHearthBeatPath() {
213-
return null;
222+
String kvmScriptsDir = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_SCRIPTS_DIR);
223+
return Script.findScript(kvmScriptsDir, "kvmspheartbeat.sh");
214224
}
215225

216226
@Override
217-
public String createHeartBeatCommand(HAStoragePool primaryStoragePool, String hostPrivateIp,
227+
public String createHeartBeatCommand(HAStoragePool pool, String hostPrivateIp,
218228
boolean hostValidation) {
219-
return null;
229+
s_logger.trace(String.format("Linstor.createHeartBeatCommand: %s, %s, %b", pool.getPoolIp(), hostPrivateIp, hostValidation));
230+
boolean isStorageNodeUp = checkingHeartBeat(pool, null);
231+
if (!isStorageNodeUp && !hostValidation) {
232+
//restart the host
233+
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()));
234+
Script cmd = new Script(pool.getPool().getHearthBeatPath(), Duration.millis(HeartBeatUpdateTimeout), s_logger);
235+
cmd.add("-c");
236+
cmd.execute();
237+
return "Down";
238+
}
239+
return isStorageNodeUp ? null : "Down";
220240
}
221241

222242
@Override
223243
public String getStorageNodeId() {
244+
// only called by storpool
224245
return null;
225246
}
226247

@@ -237,11 +258,88 @@ static String getHostname() {
237258

238259
@Override
239260
public Boolean checkingHeartBeat(HAStoragePool pool, HostTO host) {
240-
return null;
261+
String hostName;
262+
if (host == null) {
263+
hostName = localNodeName;
264+
} else {
265+
hostName = host.getParent();
266+
if (hostName == null) {
267+
s_logger.error("No hostname set in host.getParent()");
268+
return false;
269+
}
270+
}
271+
272+
return checkHostUpToDateAndConnected(hostName);
273+
}
274+
275+
private String executeDrbdSetupStatus(OutputInterpreter.AllLinesParser parser) {
276+
Script sc = new Script("drbdsetup", Duration.millis(HeartBeatUpdateTimeout), s_logger);
277+
sc.add("status");
278+
sc.add("--json");
279+
return sc.execute(parser);
280+
}
281+
282+
private boolean checkDrbdSetupStatusOutput(String output, String otherNodeName) {
283+
JsonParser jsonParser = new JsonParser();
284+
JsonArray jResources = (JsonArray) jsonParser.parse(output);
285+
for (JsonElement jElem : jResources) {
286+
JsonObject jRes = (JsonObject) jElem;
287+
JsonArray jConnections = jRes.getAsJsonArray("connections");
288+
for (JsonElement jConElem : jConnections) {
289+
JsonObject jConn = (JsonObject) jConElem;
290+
if (jConn.getAsJsonPrimitive("name").getAsString().equals(otherNodeName)
291+
&& jConn.getAsJsonPrimitive("connection-state").getAsString().equalsIgnoreCase("Connected")) {
292+
return true;
293+
}
294+
}
295+
}
296+
s_logger.warn(String.format("checkDrbdSetupStatusOutput: no resource connected to %s.", otherNodeName));
297+
return false;
298+
}
299+
300+
private String executeDrbdEventsNow(OutputInterpreter.AllLinesParser parser) {
301+
Script sc = new Script("drbdsetup", Duration.millis(HeartBeatUpdateTimeout), s_logger);
302+
sc.add("events2");
303+
sc.add("--now");
304+
return sc.execute(parser);
305+
}
306+
307+
private boolean checkDrbdEventsNowOutput(String output) {
308+
boolean healthy = output.lines().noneMatch(line -> line.matches(".*role:Primary .* promotion_score:0.*"));
309+
if (!healthy) {
310+
s_logger.warn("checkDrbdEventsNowOutput: primary resource with promotion score==0; HA false");
311+
}
312+
return healthy;
313+
}
314+
315+
private boolean checkHostUpToDateAndConnected(String hostName) {
316+
s_logger.trace(String.format("checkHostUpToDateAndConnected: %s/%s", localNodeName, hostName));
317+
OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser();
318+
319+
if (localNodeName.equalsIgnoreCase(hostName)) {
320+
String res = executeDrbdEventsNow(parser);
321+
if (res != null) {
322+
return false;
323+
}
324+
return checkDrbdEventsNowOutput(parser.getLines());
325+
} else {
326+
// check drbd connections
327+
String res = executeDrbdSetupStatus(parser);
328+
if (res != null) {
329+
return false;
330+
}
331+
try {
332+
return checkDrbdSetupStatusOutput(parser.getLines(), hostName);
333+
} catch (JsonIOException | JsonSyntaxException e) {
334+
s_logger.error("Error parsing drbdsetup status --json", e);
335+
}
336+
}
337+
return false;
241338
}
242339

243340
@Override
244341
public Boolean vmActivityCheck(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration) {
245-
return null;
342+
s_logger.trace(String.format("Linstor.vmActivityCheck: %s, %s", pool.getPoolIp(), host.getPrivateNetwork().getIp()));
343+
return checkingHeartBeat(pool, host);
246344
}
247345
}

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ public void provideVmTags(long vmId, long volumeId, String tagValue) {
12411241

12421242
@Override
12431243
public boolean isStorageSupportHA(StoragePoolType type) {
1244-
return false;
1244+
return true;
12451245
}
12461246

12471247
@Override
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.storage.datastore.provider;
18+
19+
import com.cloud.exception.StorageConflictException;
20+
import com.cloud.host.HostVO;
21+
22+
public class LinstorHostListener extends DefaultHostListener {
23+
@Override
24+
public boolean hostConnect(long hostId, long poolId) throws StorageConflictException {
25+
HostVO host = hostDao.findById(hostId);
26+
if (host.getParent() == null) {
27+
host.setParent(host.getName());
28+
hostDao.update(host.getId(), host);
29+
}
30+
return super.hostConnect(hostId, poolId);
31+
}
32+
}

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public DataStoreLifeCycle getDataStoreLifeCycle() {
4848
public boolean configure(Map<String, Object> params) {
4949
lifecycle = ComponentContext.inject(LinstorPrimaryDataStoreLifeCycleImpl.class);
5050
driver = ComponentContext.inject(LinstorPrimaryDataStoreDriverImpl.class);
51-
listener = ComponentContext.inject(DefaultHostListener.class);
51+
listener = ComponentContext.inject(LinstorHostListener.class);
5252
return true;
5353
}
5454

0 commit comments

Comments
 (0)