Skip to content

Commit eaf97bd

Browse files
committed
linstor: delete backup scratch left behind when the agent starts
The agent deletes the LINSTOR scratch resource of a pull-mode backup once the backup has ended, also when the backup fails or the agent is stopped normally. If the agent dies or the host goes down during the backup, the resource stays behind and keeps its storage allocated. When the agent registers the first pool of a LINSTOR controller, delete every cs-bkscratch-* resource definition that has a resource on this node: a scratch always has one on the node whose agent created it, diskless if its data lives elsewhere. Pools are registered again on every reconnect to the management server, while backups may still be running, so this happens only once per controller and agent process, before any backup can use the pool. Failures are only logged and never keep the pool from being registered. After a hard crash the VM's pull-mode backup job can outlive the agent and still hold the scratch devices, so LINSTOR refuses the delete. The warning then says to end the job with virsh domjobabort and delete the resource definition manually.
1 parent 5b29945 commit eaf97bd

2 files changed

Lines changed: 115 additions & 2 deletions

File tree

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

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Optional;
27+
import java.util.Set;
2728
import java.util.UUID;
29+
import java.util.concurrent.ConcurrentHashMap;
2830
import java.util.stream.Collectors;
2931

3032
import javax.annotation.Nonnull;
@@ -81,6 +83,8 @@ public class LinstorStorageAdaptor implements StorageAdaptor {
8183
static final String BACKUP_SCRATCH_PREFIX = "cs-bkscratch-";
8284
static final String BACKUP_SCRATCH_VM_PROP = "Aux/cs-backup-vm";
8385
private static final long DEVICE_WAIT_MS = 10000;
86+
// Controllers this agent has already removed its leftover backup scratch from, see deleteLeftoverBackupScratch().
87+
private static final Set<String> BackupScratchSweptControllers = ConcurrentHashMap.newKeySet();
8488
private final String localNodeName;
8589

8690
DevelopersApi getLinstorAPI(KVMStoragePool pool) {
@@ -198,6 +202,11 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri
198202
: Boolean.parseBoolean(LinstorConfigurationManager.InsecureSsl.defaultValue());
199203
LinstorStoragePool storagePool = new LinstorStoragePool(name, host, port, userInfo, type, this, insecureSsl);
200204

205+
// Pools are registered again whenever the agent reconnects, but only the first registration of a
206+
// controller precedes every backup that could be using its scratch space.
207+
if (BackupScratchSweptControllers.add(host)) {
208+
deleteLeftoverBackupScratch(storagePool);
209+
}
201210
MapStorageUuidToStoragePool.put(name, storagePool);
202211

203212
return storagePool;
@@ -265,8 +274,9 @@ void waitForDevice(String device) {
265274
* node attaches diskless.
266275
*
267276
* The agent deletes the scratch once the backup has ended. If it never gets there, e.g. because the agent
268-
* crashed, the resource definition has to be deleted manually; its {@value #BACKUP_SCRATCH_VM_PROP}
269-
* property names the VM.
277+
* crashed or the host went down, it deletes the leftover the next time it starts, see
278+
* {@link #deleteLeftoverBackupScratch(KVMStoragePool)}; the {@value #BACKUP_SCRATCH_VM_PROP} property
279+
* names the VM.
270280
*/
271281
public Pair<String, List<String>> createBackupScratch(KVMStoragePool pool, String vmName, List<Long> sizes) {
272282
final LinstorStoragePool lpool = (LinstorStoragePool) pool;
@@ -371,6 +381,47 @@ private List<String> getBackupScratchDevices(DevelopersApi api, String rscName)
371381
return devices;
372382
}
373383

384+
/**
385+
* Deletes the backup scratch resources an earlier run of this agent left on this node, e.g. because it
386+
* crashed or was restarted during a backup. Called when the first pool of a controller is registered,
387+
* before any backup can use it, so none of them can belong to a running backup. A scratch always has a
388+
* resource on the node whose agent created it, diskless if its data lives on another node. Failures are
389+
* logged only: a leftover must not keep the pool from being registered.
390+
*/
391+
void deleteLeftoverBackupScratch(KVMStoragePool pool) {
392+
final DevelopersApi api = getLinstorAPI(pool);
393+
try {
394+
Map<String, ResourceDefinition> scratchRds = LinstorUtil.getRDListStartingWith(api, BACKUP_SCRATCH_PREFIX)
395+
.stream().collect(Collectors.toMap(ResourceDefinition::getName, rd -> rd));
396+
if (scratchRds.isEmpty()) {
397+
return;
398+
}
399+
List<String> leftovers = api.viewResources(Collections.singletonList(localNodeName),
400+
new ArrayList<>(scratchRds.keySet()), Collections.emptyList(), null, null, null)
401+
.stream().map(Resource::getName).distinct().collect(Collectors.toList());
402+
for (String rscName : leftovers) {
403+
Properties props = scratchRds.get(rscName).getProps();
404+
String vmName = props != null ? props.get(BACKUP_SCRATCH_VM_PROP) : null;
405+
logger.warn("Linstor: deleting backup scratch {} of VM {} left behind on {}", rscName, vmName,
406+
localNodeName);
407+
try {
408+
deleteBackupScratch(pool, rscName);
409+
} catch (CloudRuntimeException e) {
410+
// Typically after a crash during a backup: the VM's pull-mode backup job outlived the agent
411+
// and still holds the scratch devices.
412+
logger.warn("Linstor: unable to delete leftover backup scratch {}: {}. If VM {} still has a backup "
413+
+ "job, end it with 'virsh domjobabort {}', then delete the resource definition manually.",
414+
rscName, e.getMessage(), vmName, vmName);
415+
}
416+
}
417+
} catch (ApiException apiEx) {
418+
logger.warn("Linstor: unable to look for leftover backup scratch on {}: {}", localNodeName,
419+
apiEx.getBestMessage());
420+
} catch (CloudRuntimeException e) {
421+
logger.warn("Linstor: unable to look for leftover backup scratch on {}: {}", localNodeName, e.getMessage());
422+
}
423+
}
424+
374425
public void deleteBackupScratch(KVMStoragePool pool, String rscName) {
375426
logger.info("Linstor: deleting backup scratch {}", rscName);
376427
try {

‎plugins/storage/volume/linstor/src/test/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptorTest.java‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import com.linbit.linstor.api.model.AutoPlaceRequest;
5454
import com.linbit.linstor.api.model.DrbdResource;
5555
import com.linbit.linstor.api.model.LayerType;
56+
import com.linbit.linstor.api.model.Properties;
5657
import com.linbit.linstor.api.model.ResourceDefinition;
5758
import com.linbit.linstor.api.model.ResourceDefinitionCreate;
5859
import com.linbit.linstor.api.model.ResourceLayer;
@@ -279,4 +280,65 @@ public void connectPhysicalDiskFailsWithoutLocalVolume() throws ApiException {
279280
public void waitForDeviceReturnsForAnExistingDevice() {
280281
new LinstorStorageAdaptor().waitForDevice("/dev/null");
281282
}
283+
284+
private static ResourceDefinition scratchRd(String name, String vmName) {
285+
ResourceDefinition rd = new ResourceDefinition();
286+
rd.setName(name);
287+
Properties props = new Properties();
288+
props.put(LinstorStorageAdaptor.BACKUP_SCRATCH_VM_PROP, vmName);
289+
rd.setProps(props);
290+
return rd;
291+
}
292+
293+
private static ResourceWithVolumes resourceOf(String name) {
294+
ResourceWithVolumes rsc = new ResourceWithVolumes();
295+
rsc.setName(name);
296+
return rsc;
297+
}
298+
299+
// The controllers this agent has swept are remembered per process, so every test uses its own one.
300+
private KVMStoragePool registerPool(String controller) {
301+
return adaptor.createStoragePool("pool-" + controller, controller, 3370, "cloudstack", null,
302+
Storage.StoragePoolType.Linstor, null, true);
303+
}
304+
305+
@Test
306+
@SuppressWarnings("unchecked")
307+
public void createStoragePoolDeletesLeftoverScratchOfThisNode() throws ApiException {
308+
ResourceDefinition volume = new ResourceDefinition();
309+
volume.setName("cs-vol-1");
310+
when(api.resourceDefinitionList(null, false, null, null, null)).thenReturn(Arrays.asList(
311+
scratchRd("cs-bkscratch-here", "i-2-3-VM"), scratchRd("cs-bkscratch-elsewhere", "i-2-4-VM"), volume));
312+
ArgumentCaptor<List<String>> rscNames = ArgumentCaptor.forClass(List.class);
313+
when(api.viewResources(eq(Collections.singletonList(localNode)), rscNames.capture(), any(), any(), any(), any()))
314+
.thenReturn(Collections.singletonList(resourceOf("cs-bkscratch-here")));
315+
when(api.resourceDefinitionDelete("cs-bkscratch-here")).thenReturn(success());
316+
317+
Assert.assertNotNull(registerPool("http://sweep-1:3370"));
318+
319+
Assert.assertEquals(2, rscNames.getValue().size());
320+
Assert.assertFalse(rscNames.getValue().contains("cs-vol-1"));
321+
verify(api).resourceDefinitionDelete("cs-bkscratch-here");
322+
verify(api, never()).resourceDefinitionDelete("cs-bkscratch-elsewhere");
323+
}
324+
325+
@Test
326+
public void createStoragePoolSweepsEachControllerOnlyOnce() throws ApiException {
327+
when(api.resourceDefinitionList(null, false, null, null, null)).thenReturn(Collections.emptyList());
328+
329+
registerPool("http://sweep-2:3370");
330+
registerPool("http://sweep-2:3370");
331+
332+
verify(api, times(1)).resourceDefinitionList(null, false, null, null, null);
333+
}
334+
335+
@Test
336+
public void createStoragePoolRegistersPoolWhenControllerIsUnreachable() throws ApiException {
337+
when(api.resourceDefinitionList(null, false, null, null, null)).thenThrow(new ApiException("connection refused"));
338+
339+
KVMStoragePool registered = registerPool("http://sweep-3:3370");
340+
341+
Assert.assertSame(registered, adaptor.getStoragePool("pool-http://sweep-3:3370"));
342+
verify(api, never()).resourceDefinitionDelete(anyString());
343+
}
282344
}

0 commit comments

Comments
 (0)