diff --git a/frontend/common/volumes.go b/frontend/common/volumes.go index ca9efd0cb..9d87c3aad 100644 --- a/frontend/common/volumes.go +++ b/frontend/common/volumes.go @@ -138,6 +138,17 @@ func GetVolumeConfig( snapshotDir = snapDirFormatted } + // If preserveUnlink is provided, ensure it is lower case + preserveUnlink := collection.GetV(opts, "preserveUnlink", "") + if preserveUnlink != "" { + preserveUnlinkFormatted, err := convert.ToFormattedBool(preserveUnlink) + if err != nil { + Logc(ctx).WithError(err).Errorf("Invalid boolean value for preserveUnlink: %v.", preserveUnlink) + return nil, err + } + preserveUnlink = preserveUnlinkFormatted + } + cfg := &storage.VolumeConfig{ Name: name, Size: fmt.Sprintf("%d", sizeBytes), @@ -150,6 +161,7 @@ func GetVolumeConfig( SplitOnClone: collection.GetV(opts, "splitOnClone", ""), SnapshotPolicy: collection.GetV(opts, "snapshotPolicy", ""), SnapshotReserve: collection.GetV(opts, "snapshotReserve", ""), + PreserveUnlink: preserveUnlink, SnapshotDir: snapshotDir, ExportPolicy: collection.GetV(opts, "exportPolicy", ""), UnixPermissions: collection.GetV(opts, "unixPermissions", ""), diff --git a/frontend/csi/controller_helpers/kubernetes/config.go b/frontend/csi/controller_helpers/kubernetes/config.go index 327d8ae11..a4c87fead 100644 --- a/frontend/csi/controller_helpers/kubernetes/config.go +++ b/frontend/csi/controller_helpers/kubernetes/config.go @@ -49,6 +49,7 @@ const ( AnnSnapshotPolicy = prefix + "/snapshotPolicy" AnnSnapshotReserve = prefix + "/snapshotReserve" AnnSnapshotDir = prefix + "/snapshotDirectory" + AnnPreserveUnlink = prefix + "/preserveUnlink" AnnUnixPermissions = prefix + "/unixPermissions" AnnExportPolicy = prefix + "/exportPolicy" AnnBlockSize = prefix + "/blockSize" diff --git a/frontend/csi/controller_helpers/kubernetes/helper.go b/frontend/csi/controller_helpers/kubernetes/helper.go index 87b8bfc26..59ec0b67e 100644 --- a/frontend/csi/controller_helpers/kubernetes/helper.go +++ b/frontend/csi/controller_helpers/kubernetes/helper.go @@ -841,6 +841,16 @@ func getVolumeConfig( snapshotDirAnn = snapDirFormatted } + // If preserveUnlink annotation is provided, ensure it is lower case + preserveUnlinkAnn := getAnnotation(annotations, AnnPreserveUnlink) + if preserveUnlinkAnn != "" { + preserveUnlinkFormatted, err := convert.ToFormattedBool(preserveUnlinkAnn) + if err != nil { + Logc(ctx).WithError(err).Errorf("Invalid boolean value for preserveUnlink annotation: %v.", preserveUnlinkAnn) + } + preserveUnlinkAnn = preserveUnlinkFormatted + } + if getAnnotation(annotations, AnnReadOnlyClone) == "" { annotations[AnnReadOnlyClone] = "false" } @@ -886,6 +896,7 @@ func getVolumeConfig( SnapshotPolicy: getAnnotation(annotations, AnnSnapshotPolicy), SnapshotReserve: getAnnotation(annotations, AnnSnapshotReserve), SnapshotDir: snapshotDirAnn, + PreserveUnlink: preserveUnlinkAnn, ExportPolicy: getAnnotation(annotations, AnnExportPolicy), UnixPermissions: getAnnotation(annotations, AnnUnixPermissions), StorageClass: storageClass.Name, diff --git a/storage/volume.go b/storage/volume.go index cf02342d5..3e698d28a 100644 --- a/storage/volume.go +++ b/storage/volume.go @@ -24,6 +24,7 @@ type VolumeConfig struct { SecurityStyle string `json:"securityStyle"` SnapshotPolicy string `json:"snapshotPolicy,omitempty"` SnapshotReserve string `json:"snapshotReserve,omitempty"` + PreserveUnlink string `json:"preserveUnlink,omitempty"` SnapshotDir string `json:"snapshotDirectory,omitempty"` ExportPolicy string `json:"exportPolicy,omitempty"` UnixPermissions string `json:"unixPermissions,omitempty"` diff --git a/storage_drivers/ontap/api/abstraction.go b/storage_drivers/ontap/api/abstraction.go index 6a6295042..e27d6257e 100644 --- a/storage_drivers/ontap/api/abstraction.go +++ b/storage_drivers/ontap/api/abstraction.go @@ -233,6 +233,7 @@ type OntapAPI interface { ) (string, error) VolumeRecoveryQueuePurge(ctx context.Context, recoveryQueueVolumeName string) error VolumeRecoveryQueueGetName(ctx context.Context, name string) (string, error) + PreserveUnlinkSet(ctx context.Context, volumeName string) error SMBShareCreate(ctx context.Context, shareName, path string) error SMBShareExists(ctx context.Context, shareName string) (bool, error) SMBShareDestroy(ctx context.Context, shareName string) error diff --git a/storage_drivers/ontap/api/abstraction_rest.go b/storage_drivers/ontap/api/abstraction_rest.go index 8e03e48a5..07a2633a7 100644 --- a/storage_drivers/ontap/api/abstraction_rest.go +++ b/storage_drivers/ontap/api/abstraction_rest.go @@ -220,6 +220,14 @@ func (d OntapAPIREST) VolumeRecoveryQueueGetName(ctx context.Context, name strin return recoveryQueueVolumeName, nil } +func (d OntapAPIREST) PreserveUnlinkSet(ctx context.Context, volumeName string) error { + preserveUnlinkErr := d.api.PreserveUnlinkSet(ctx, volumeName) + if preserveUnlinkErr != nil { + return fmt.Errorf("error setting preserveUnlink %v: %v", volumeName, preserveUnlinkErr) + } + return nil +} + func (d OntapAPIREST) VolumeInfo(ctx context.Context, name string) (*Volume, error) { fields := []string{ "type", "size", "comment", "aggregates", "nas", "guarantee", diff --git a/storage_drivers/ontap/api/abstraction_zapi.go b/storage_drivers/ontap/api/abstraction_zapi.go index 0307c1e35..b0b2cb7ed 100644 --- a/storage_drivers/ontap/api/abstraction_zapi.go +++ b/storage_drivers/ontap/api/abstraction_zapi.go @@ -125,6 +125,12 @@ func (d OntapAPIZAPI) VolumeDestroy(ctx context.Context, name string, force, ski return nil } +// The -is-preserve-unlink-enabled flag is only supported with newer ONTAP versions that use REST. +func (d OntapAPIZAPI) PreserveUnlinkSet(ctx context.Context, volumeName string) error { + Logc(ctx).WithField("volume", volumeName).Warn("preserveUnlink cannot be set with ZAPI.") + return nil +} + func (d OntapAPIZAPI) VolumeRecoveryQueuePurge(ctx context.Context, recoveryQueueVolumeName string) error { volRecoveryQueuePurgeResponse, err := d.api.VolumeRecoveryQueuePurge(recoveryQueueVolumeName) if err != nil { diff --git a/storage_drivers/ontap/api/ontap_rest.go b/storage_drivers/ontap/api/ontap_rest.go index 37f8bc1a2..dd7284858 100644 --- a/storage_drivers/ontap/api/ontap_rest.go +++ b/storage_drivers/ontap/api/ontap_rest.go @@ -1770,6 +1770,67 @@ func (c *RestClient) VolumeRecoveryQueueGetName(ctx context.Context, name string return responseObject.Records[0].Volume, nil } +// PreserveUnlinkSet uses the cli passthrough REST API to set the is-preserve-unlink-enabled option on a volume. +// This volume option was introduced in 9.12.1 but not made visible via the /storage/volumes REST API endpoint. +func (c *RestClient) PreserveUnlinkSet(ctx context.Context, volumeName string) error { + fields := LogFields{ + "Method": "PreserveUnlinkSet", + "Type": "ontap_rest", + "volumeName": volumeName, + "vserver": c.svmName, + } + Logd(ctx, c.driverName, c.config.DebugTraceFlags["method"]).WithFields(fields). + Trace(">>>> PreserveUnlinkSet") + defer Logd(ctx, c.driverName, c.config.DebugTraceFlags["method"]).WithFields(fields). + Trace("<<<< PrserveUnlinkSet") + + requestUrl := fmt.Sprintf("https://%s/api/private/cli/volume?vserver=%s&volume=%s", + c.config.ManagementLIF, c.svmName, volumeName) + + requestContent := map[string]string{ + "is-preserve-unlink-enabled": "true", + } + requestBodyBytes, err := json.Marshal(requestContent) + if err != nil { + return err + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPatch, requestUrl, bytes.NewReader(requestBodyBytes)) + if err != nil { + return err + } + + response, err := c.sendPassThroughCliCommand(ctx, request) + if err != nil { + return err + } + + defer func() { _ = response.Body.Close() }() + + var responseBodyBytes []byte + var responseBodyString string + if response.Body != nil { + responseBodyBytes, _ = io.ReadAll(response.Body) + responseBodyString = string(responseBodyBytes) + } else { + Logc(ctx).WithField("statusCode", response.StatusCode).Error( + "no response when trying to set preserveUnlink.") + return fmt.Errorf("no response with status code: %v", response.StatusCode) + } + if response.StatusCode != http.StatusOK { + Logc(ctx).WithField("statusCode", response.StatusCode).Error( + "failed to set preserveUnlink: %s", responseBodyString) + return fmt.Errorf("unexpected response status code: %v", response.StatusCode) + } + if !strings.Contains(responseBodyString, "modify successful") { + Logc(ctx).WithField("statusCode", response.StatusCode).Error( + "unexpected response when setting preserveUnlink: %s", responseBodyString) + return fmt.Errorf("unexpected response boday with status code: %v", response.StatusCode) + } + + return nil +} + func (c *RestClient) sendPassThroughCliCommand(ctx context.Context, request *http.Request) (*http.Response, error) { request.Header.Set("Content-Type", "application/json") diff --git a/storage_drivers/ontap/api/ontap_rest_interface.go b/storage_drivers/ontap/api/ontap_rest_interface.go index b20927a59..b63a002e6 100644 --- a/storage_drivers/ontap/api/ontap_rest_interface.go +++ b/storage_drivers/ontap/api/ontap_rest_interface.go @@ -81,6 +81,8 @@ type RestClientInterface interface { // directly purge the volume from the recovery queue. VolumeRecoveryQueuePurge(ctx context.Context, recoveryQueueVolumeName string) error VolumeRecoveryQueueGetName(ctx context.Context, name string) (string, error) + // PreserveUnlinkSet uses the cli passthrough REST API to to enable the volume is-preserve-unlink-enabled option. + PreserveUnlinkSet(ctx context.Context, volumeName string) error // ConsistencyGroupCreateAndWait creates a CG and waits on the job to complete ConsistencyGroupCreateAndWait(ctx context.Context, cgName string, flexVols []string) error // ConsistencyGroupCreate creates a consistency group diff --git a/storage_drivers/ontap/api/types.go b/storage_drivers/ontap/api/types.go index 7ca16af92..a00838903 100644 --- a/storage_drivers/ontap/api/types.go +++ b/storage_drivers/ontap/api/types.go @@ -20,6 +20,7 @@ type Volume struct { SnapshotDir *bool SnapshotPolicy string SnapshotReserve int + PreserveUnlink *bool SnapshotSpaceUsed int SpaceReserve string TieringPolicy string diff --git a/storage_drivers/ontap/ontap_common.go b/storage_drivers/ontap/ontap_common.go index 6221928fc..14c6a0b74 100644 --- a/storage_drivers/ontap/ontap_common.go +++ b/storage_drivers/ontap/ontap_common.go @@ -92,6 +92,7 @@ const ( SpaceReserve = "spaceReserve" SnapshotPolicy = "snapshotPolicy" SnapshotReserve = "snapshotReserve" + PreserveUnlink = "preserveUnlink" UnixPermissions = "unixPermissions" ExportPolicy = "exportPolicy" SecurityStyle = "securityStyle" @@ -1748,6 +1749,7 @@ const ( DefaultSpaceReserve = "none" DefaultSnapshotPolicy = "none" DefaultSnapshotReserve = "5" + DefaultPreserveUnlink = "false" DefaultUnixPermissions = "---rwxrwxrwx" DefaultSnapshotDir = "false" DefaultExportPolicy = "default" @@ -1812,6 +1814,10 @@ func PopulateConfigurationDefaults(ctx context.Context, config *drivers.OntapSto } } + if config.PreserveUnlink == "" { + config.PreserveUnlink = DefaultPreserveUnlink + } + // If snapshotDir is provided, ensure it is lower case snapDir := DefaultSnapshotDir if config.SnapshotDir != "" { @@ -1822,6 +1828,16 @@ func PopulateConfigurationDefaults(ctx context.Context, config *drivers.OntapSto } config.SnapshotDir = snapDir + // If preserveUnlink is provided, ensure it is lower case + preserveUnlink := DefaultPreserveUnlink + if config.PreserveUnlink != "" { + if preserveUnlink, err = convert.ToFormattedBool(config.PreserveUnlink); err != nil { + Logc(ctx).WithError(err).Errorf("Invalid boolean value for preserveUnlink: %v.", config.PreserveUnlink) + return fmt.Errorf("invalid boolean value for preserveUnlink: %v", err) + } + } + config.PreserveUnlink = preserveUnlink + if config.DenyNewVolumePools == "" { config.DenyNewVolumePools = DefaultDenyNewVolumePools } else { @@ -1950,6 +1966,7 @@ func PopulateConfigurationDefaults(ctx context.Context, config *drivers.OntapSto "SpaceReserve": config.SpaceReserve, "SnapshotPolicy": config.SnapshotPolicy, "SnapshotReserve": config.SnapshotReserve, + "PreserveUnlink": config.PreserveUnlink, "UnixPermissions": config.UnixPermissions, "SnapshotDir": config.SnapshotDir, "ExportPolicy": config.ExportPolicy, @@ -2615,6 +2632,10 @@ func getVolumeExternalCommon( if volume.SnapshotDir != nil { snapshotDir = *volume.SnapshotDir } + preserveUnlink := false + if volume.PreserveUnlink != nil { + preserveUnlink = *volume.PreserveUnlink + } volumeConfig := &storage.VolumeConfig{ Version: tridentconfig.OrchestratorAPIVersion, Name: name, @@ -2625,6 +2646,7 @@ func getVolumeExternalCommon( SnapshotReserve: strconv.Itoa(volume.SnapshotReserve), ExportPolicy: volume.ExportPolicy, SnapshotDir: strconv.FormatBool(snapshotDir), + PreserveUnlink: strconv.FormatBool(preserveUnlink), UnixPermissions: volume.UnixPermissions, StorageClass: "", AccessMode: tridentconfig.ReadWriteMany, @@ -2901,6 +2923,7 @@ func setStoragePoolAttributes( pool.InternalAttributes()[SpaceReserve] = config.SpaceReserve pool.InternalAttributes()[SnapshotPolicy] = config.SnapshotPolicy pool.InternalAttributes()[SnapshotReserve] = config.SnapshotReserve + pool.InternalAttributes()[PreserveUnlink] = config.PreserveUnlink pool.InternalAttributes()[SplitOnClone] = config.SplitOnClone pool.InternalAttributes()[Encryption] = config.Encryption pool.InternalAttributes()[LUKSEncryption] = config.LUKSEncryption @@ -3023,6 +3046,11 @@ func initializeVirtualPools( snapshotReserve = vpool.SnapshotReserve } + preserveUnlink := config.PreserveUnlink + if vpool.PreserveUnlink != "" { + preserveUnlink = vpool.PreserveUnlink + } + splitOnClone := config.SplitOnClone if vpool.SplitOnClone != "" { splitOnClone = vpool.SplitOnClone @@ -3155,6 +3183,7 @@ func initializeVirtualPools( pool.InternalAttributes()[SpaceReserve] = spaceReserve pool.InternalAttributes()[SnapshotPolicy] = snapshotPolicy pool.InternalAttributes()[SnapshotReserve] = snapshotReserve + pool.InternalAttributes()[PreserveUnlink] = preserveUnlink pool.InternalAttributes()[SplitOnClone] = splitOnClone pool.InternalAttributes()[UnixPermissions] = unixPermissions pool.InternalAttributes()[SnapshotDir] = snapshotDir @@ -3696,6 +3725,16 @@ func getVolumeOptsCommon( opts["snapshotDir"] = snapshotDirFormatted } + // If preserveUnlink is provided, ensure it is lower case + if volConfig.PreserveUnlink != "" { + preserveUnlinkFormatted, err := convert.ToFormattedBool(volConfig.PreserveUnlink) + if err != nil { + Logc(ctx).WithError(err).Errorf( + "Invalid boolean value for volume '%v' preserveUnlink: %v.", volConfig.Name, volConfig.PreserveUnlink) + } + opts["preserveUnlink"] = preserveUnlinkFormatted + } + // If skipRecoveryQueue is provided, ensure it is lower case if volConfig.SkipRecoveryQueue != "" { skipRecoveryQueueFormatted, err := convert.ToFormattedBool(volConfig.SkipRecoveryQueue) @@ -5474,6 +5513,14 @@ func purgeRecoveryQueueVolume(ctx context.Context, api api.OntapAPI, volumeName } } +// setPreserveUnlink has to be handled specially +func setPresrveUnlink(ctx context.Context, api api.OntapAPI, volumeName string) { + if err := api.PreserveUnlinkSet(ctx, volumeName); err != nil { + Logc(ctx).WithField("volume", + volumeName).Errorf("error setting preserveUnlink: %v", err) + } +} + // getSMBShareNamePath constructs the SMB share name and path based on the provided parameters. func getSMBShareNamePath(flexvol, name string, secureSMBEnabled bool) (string, string) { if secureSMBEnabled { diff --git a/storage_drivers/ontap/ontap_nas.go b/storage_drivers/ontap/ontap_nas.go index d09921425..05d1575f5 100644 --- a/storage_drivers/ontap/ontap_nas.go +++ b/storage_drivers/ontap/ontap_nas.go @@ -269,6 +269,7 @@ func (d *NASStorageDriver) Create( spaceReserve = collection.GetV(opts, "spaceReserve", storagePool.InternalAttributes()[SpaceReserve]) snapshotPolicy = collection.GetV(opts, "snapshotPolicy", storagePool.InternalAttributes()[SnapshotPolicy]) snapshotReserve = collection.GetV(opts, "snapshotReserve", storagePool.InternalAttributes()[SnapshotReserve]) + preserveUnlink = collection.GetV(opts, "preserveUnlink", storagePool.InternalAttributes()[PreserveUnlink]) unixPermissions = collection.GetV(opts, "unixPermissions", storagePool.InternalAttributes()[UnixPermissions]) snapshotDir = collection.GetV(opts, "snapshotDir", storagePool.InternalAttributes()[SnapshotDir]) exportPolicy = collection.GetV(opts, "exportPolicy", storagePool.InternalAttributes()[ExportPolicy]) @@ -280,7 +281,7 @@ func (d *NASStorageDriver) Create( qosPolicy = storagePool.InternalAttributes()[QosPolicy] adaptiveQosPolicy = storagePool.InternalAttributes()[AdaptiveQosPolicy] ) - + snapshotReserveInt, err := GetSnapshotReserve(snapshotPolicy, snapshotReserve) if err != nil { return fmt.Errorf("invalid value for snapshotReserve: %v", err) @@ -313,6 +314,11 @@ func (d *NASStorageDriver) Create( return fmt.Errorf("invalid boolean value for snapshotDir: %v", err) } + enablePreserveUnlink, err := strconv.ParseBool(preserveUnlink) + if err != nil { + return fmt.Errorf("invalid boolean value for preserveUnlink: %v", err) + } + enableEncryption, configEncryption, err := GetEncryptionValue(encryption) if err != nil { return fmt.Errorf("invalid boolean value for encryption: %v", err) @@ -355,6 +361,7 @@ func (d *NASStorageDriver) Create( volConfig.SpaceReserve = spaceReserve volConfig.SnapshotPolicy = snapshotPolicy volConfig.SnapshotReserve = snapshotReserve + volConfig.PreserveUnlink = preserveUnlink volConfig.UnixPermissions = unixPermissions volConfig.SnapshotDir = snapshotDir volConfig.ExportPolicy = exportPolicy @@ -370,6 +377,7 @@ func (d *NASStorageDriver) Create( "spaceReserve": spaceReserve, "snapshotPolicy": snapshotPolicy, "snapshotReserve": snapshotReserveInt, + "preserveUnlink": enablePreserveUnlink, "unixPermissions": unixPermissions, "snapshotDir": enableSnapshotDir, "exportPolicy": exportPolicy, @@ -463,6 +471,11 @@ func (d *NASStorageDriver) Create( } } + // Set is-unlink-preserve-enabled if required + if enablePreserveUnlink { + setPresrveUnlink(ctx, d.API, volConfig.InternalName) + } + return nil } diff --git a/storage_drivers/types.go b/storage_drivers/types.go index ade6ccbbc..a9cfae82e 100644 --- a/storage_drivers/types.go +++ b/storage_drivers/types.go @@ -198,6 +198,7 @@ type OntapStorageDriverConfigDefaults struct { SpaceReserve string `json:"spaceReserve"` SnapshotPolicy string `json:"snapshotPolicy"` SnapshotReserve string `json:"snapshotReserve"` + PreserveUnlink string `json:"preserveUnlink"` SnapshotDir string `json:"snapshotDir"` UnixPermissions string `json:"unixPermissions"` ExportPolicy string `json:"exportPolicy"`