Skip to content

Commit 7d6bce2

Browse files
authored
Merge pull request #1848 from luissimas/linstor-remove-snapshots
Add `linstor.remove_snapshots` config option
2 parents 90a09b1 + cd8d372 commit 7d6bce2

File tree

4 files changed

+92
-11
lines changed

4 files changed

+92
-11
lines changed

doc/reference/storage_linstor.md

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ Restoring from older snapshots
5757
This method makes it possible to confirm whether a specific snapshot contains what you need.
5858
After determining the correct snapshot, you can {ref}`remove the newer snapshots <storage-edit-snapshots>` so that the snapshot you need is the latest one and you can restore it.
5959

60+
Alternatively, you can configure Incus to automatically discard the newer snapshots during restore.
61+
To do so, set the [`linstor.remove_snapshots`](storage-linstor-vol-config) configuration for the volume (or the corresponding `volume.linstor.remove_snapshots` configuration on the storage pool for all volumes in the pool).
62+
6063
## Configuration options
6164

6265
The following configuration options are available for storage pools that use the `linstor` driver and for storage volumes in these pools.
@@ -96,6 +99,7 @@ Key | Type | Condition
9699
`drbd.on_no_quorum` | string | | - | The DRBD policy to use on resources when quorum is lost (applied to the resource definition)
97100
`drbd.auto_diskful` | string | | - | A duration string describing the time after which a primary diskless resource can be converted to diskful if storage is available on the node (applied to the resource definition)
98101
`drbd.auto_add_quorum_tiebreaker` | bool | | `true` | Whether to allow LINSTOR to automatically create diskless resources to act as quorum tiebreakers if needed (applied to the resource definition)
102+
`linstor.remove_snapshots` | bool | | same as `volume.zfs.remove_snapshots` or `false` | Remove snapshots as needed
99103

100104
[^*]: {{snapshot_pattern_detail}}
101105

internal/server/storage/drivers/driver_linstor_utils.go

+22
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ const DrbdAutoDiskfulConfigKey = "drbd.auto_diskful"
5454
// DrbdAutoAddQuorumTiebreakerConfigKey represents the config key that describes whether DRBD will automatically create tiebreaker resources.
5555
const DrbdAutoAddQuorumTiebreakerConfigKey = "drbd.auto_add_quorum_tiebreaker"
5656

57+
// LinstorRemoveSnapshotsConfigKey represents the config key that describes whether snapshots should be automatically removed with volumes.
58+
const LinstorRemoveSnapshotsConfigKey = "linstor.remove_snapshots"
59+
5760
// LinstorAuxSnapshotPrefix represents the AuxProp prefix to map Incus and LINSTOR snapshots.
5861
const LinstorAuxSnapshotPrefix = "Aux/Incus/snapshot-name/"
5962

@@ -1076,6 +1079,22 @@ func (d *linstor) drbdPropsFromConfig(config map[string]string) (map[string]stri
10761079
return props, nil
10771080
}
10781081

1082+
// getSnapshots retrieves all snapshots for a given resource definition name.
1083+
func (d *linstor) getSnapshots(resourceDefinitionName string) ([]linstorClient.Snapshot, error) {
1084+
linstor, err := d.state.Linstor()
1085+
if err != nil {
1086+
return []linstorClient.Snapshot{}, err
1087+
}
1088+
1089+
snapshots, err := linstor.Client.Resources.GetSnapshots(context.TODO(), resourceDefinitionName)
1090+
if err != nil {
1091+
return snapshots, fmt.Errorf("Unable to get snapshots: %w", err)
1092+
}
1093+
1094+
return snapshots, nil
1095+
}
1096+
1097+
// rsyncMigrationType returns the migration types to use for a given content type.
10791098
func (d *linstor) rsyncMigrationType(contentType ContentType) localMigration.Type {
10801099
var rsyncTransportType migration.MigrationFSType
10811100
var rsyncFeatures []string
@@ -1100,6 +1119,7 @@ func (d *linstor) rsyncMigrationType(contentType ContentType) localMigration.Typ
11001119
}
11011120
}
11021121

1122+
// parseVolumeType parses a string into a volume type.
11031123
func (d *linstor) parseVolumeType(s string) (*VolumeType, bool) {
11041124
for _, volType := range d.Info().VolumeTypes {
11051125
if s == string(volType) {
@@ -1110,6 +1130,7 @@ func (d *linstor) parseVolumeType(s string) (*VolumeType, bool) {
11101130
return nil, false
11111131
}
11121132

1133+
// parseContentType parses a string into a volume type.
11131134
func (d *linstor) parseContentType(s string) (*ContentType, bool) {
11141135
for _, contentType := range []ContentType{ContentTypeFS, ContentTypeBlock, ContentTypeISO} {
11151136
if s == string(contentType) {
@@ -1120,6 +1141,7 @@ func (d *linstor) parseContentType(s string) (*ContentType, bool) {
11201141
return nil, false
11211142
}
11221143

1144+
// generateUUIDWithPrefix generates a new UUID (without "-") and appends it to the configured volume prefix.
11231145
func (d *linstor) generateUUIDWithPrefix() string {
11241146
return d.config[LinstorVolumePrefixConfigKey] + strings.ReplaceAll(uuid.NewString(), "-", "")
11251147
}

internal/server/storage/drivers/driver_linstor_volumes.go

+54-10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"io/fs"
99
"os"
10+
"slices"
1011
"strings"
1112
"time"
1213

@@ -74,6 +75,7 @@ func (d *linstor) commonVolumeRules() map[string]func(value string) error {
7475
DrbdOnNoQuorumConfigKey: validate.Optional(validate.IsOneOf("io-error", "suspend-io")),
7576
DrbdAutoDiskfulConfigKey: validate.Optional(validate.IsMinimumDuration(time.Minute)),
7677
DrbdAutoAddQuorumTiebreakerConfigKey: validate.Optional(validate.IsBool),
78+
LinstorRemoveSnapshotsConfigKey: validate.Optional(validate.IsBool),
7779
}
7880
}
7981

@@ -774,9 +776,56 @@ func (d *linstor) RestoreVolume(vol Volume, snapshotName string, op *operations.
774776
return err
775777
}
776778

777-
// TODO: check if more recent snapshots exist and delete them if the user
778-
// configure the storage pool to allow for such deletions. Otherwise, return
779-
// a graceful error
779+
resourceDefinition, err := d.getResourceDefinition(vol, false)
780+
if err != nil {
781+
return err
782+
}
783+
784+
// Since LINSTOR only supports rolling back a resource definition to its latest
785+
// snapshot, we need to check if the given snapshot is the latest one.
786+
existingSnapshots, err := d.getSnapshots(resourceDefinition.Name)
787+
if err != nil {
788+
return err
789+
}
790+
791+
// Sort all snapshots by creation date in descending order.
792+
slices.SortFunc(existingSnapshots, func(a linstorClient.Snapshot, b linstorClient.Snapshot) int {
793+
return a.Snapshots[0].CreateTimestamp.Compare(b.Snapshots[0].CreateTimestamp.Time) * -1
794+
})
795+
796+
linstorSnapshotName, ok := resourceDefinition.Props[LinstorAuxSnapshotPrefix+snapshotName]
797+
if !ok {
798+
return fmt.Errorf("Could not find snapshot name mapping for volume %s", vol.Name())
799+
}
800+
801+
snapshotMap, err := d.getSnapshotMap(vol)
802+
if err != nil {
803+
return err
804+
}
805+
806+
// Get all snapshots taken after the one we're trying to restore.
807+
snapshots := []string{}
808+
for _, s := range existingSnapshots {
809+
if s.Name == linstorSnapshotName {
810+
break
811+
}
812+
813+
snapshots = append(snapshots, snapshotMap[s.Name])
814+
}
815+
816+
// Check if snapshot removal is allowed.
817+
if len(snapshots) > 0 {
818+
if util.IsFalseOrEmpty(vol.ExpandedConfig(LinstorRemoveSnapshotsConfigKey)) {
819+
return fmt.Errorf("Snapshot %q cannot be restored due to subsequent snapshot(s). Set %s to override", snapshotName, LinstorRemoveSnapshotsConfigKey)
820+
}
821+
822+
// Setup custom error to tell the backend what to delete.
823+
err := ErrDeleteSnapshots{}
824+
err.Snapshots = snapshots
825+
return err
826+
}
827+
828+
// Restore the snapshot.
780829
err = d.restoreVolume(vol, snapVol)
781830
if err != nil {
782831
return err
@@ -1151,11 +1200,6 @@ func (d *linstor) CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser,
11511200
func (d *linstor) VolumeSnapshots(vol Volume, op *operations.Operation) ([]string, error) {
11521201
var snapshots []string
11531202

1154-
linstor, err := d.state.Linstor()
1155-
if err != nil {
1156-
return snapshots, err
1157-
}
1158-
11591203
resourceDefinition, err := d.getResourceDefinition(vol, false)
11601204
if err != nil {
11611205
return snapshots, err
@@ -1167,9 +1211,9 @@ func (d *linstor) VolumeSnapshots(vol Volume, op *operations.Operation) ([]strin
11671211
}
11681212

11691213
// Get the snapshots.
1170-
linstorSnapshots, err := linstor.Client.Resources.GetSnapshots(context.TODO(), resourceDefinition.Name)
1214+
linstorSnapshots, err := d.getSnapshots(resourceDefinition.Name)
11711215
if err != nil {
1172-
return snapshots, fmt.Errorf("Unable to get snapshots: %w", err)
1216+
return snapshots, err
11731217
}
11741218

11751219
for _, snapshot := range linstorSnapshots {

test/suites/storage_driver_linstor.sh

+12-1
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,20 @@ test_storage_driver_linstor() {
9292
incus storage volume set "incustest-$(basename "${INCUS_DIR}")-pool1" c1 size 500MiB
9393
incus storage volume unset "incustest-$(basename "${INCUS_DIR}")-pool1" c1 size
9494

95+
# Validate that we can restore to previous snapshots given that linstor.remove_snapshots is set
96+
incus storage volume create "incustest-$(basename "${INCUS_DIR}")-pool1" c3
97+
incus storage volume snapshot create "incustest-$(basename "${INCUS_DIR}")-pool1" c3 snap0
98+
incus storage volume snapshot create "incustest-$(basename "${INCUS_DIR}")-pool1" c3 snap1
99+
! incus storage volume snapshot restore "incustest-$(basename "${INCUS_DIR}")-pool1" c3 snap0 || false
100+
incus storage volume set "incustest-$(basename "${INCUS_DIR}")-pool1" c3 linstor.remove_snapshots=true
101+
incus storage volume snapshot restore "incustest-$(basename "${INCUS_DIR}")-pool1" c3 snap0 || false
102+
incus storage volume list "incustest-$(basename "${INCUS_DIR}")-pool1" | grep snap0
103+
! incus storage volume list "incustest-$(basename "${INCUS_DIR}")-pool1" | grep snap1 || false
104+
105+
# Cleanup
95106
incus storage volume delete "incustest-$(basename "${INCUS_DIR}")-pool1" c1
96107
incus storage volume delete "incustest-$(basename "${INCUS_DIR}")-pool1" c2
97-
108+
incus storage volume delete "incustest-$(basename "${INCUS_DIR}")-pool1" c3
98109
incus image delete testimage
99110
incus profile device remove default root
100111
incus storage delete "incustest-$(basename "${INCUS_DIR}")-pool1"

0 commit comments

Comments
 (0)