diff --git a/go.mod b/go.mod index 7c6b4907..384d4ca3 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/dell/dell-csi-extensions/volumeGroupSnapshot v1.2.3-0.20230517135918-9920e636bff1 github.com/dell/gocsi v1.7.0 github.com/dell/gofsutil v1.12.0 - github.com/dell/goscaleio v1.11.1-0.20230724113841-f10386ad0bb4 + github.com/dell/goscaleio v1.11.1-0.20230724122254-8f8b41ad4aad github.com/fsnotify/fsnotify v1.5.1 github.com/golang/protobuf v1.5.3 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 69f8fe58..3339f237 100644 --- a/go.sum +++ b/go.sum @@ -114,12 +114,10 @@ github.com/dell/gocsi v1.7.0 h1:fMQO2zwAXCaIsUoPCcnnuPMwfQMoaI1/0aqkQVndlxU= github.com/dell/gocsi v1.7.0/go.mod h1:X/8Ll8qqKAKCenmd1gPJMUvUmgY8cK0LiS8Pck12UaU= github.com/dell/gofsutil v1.12.0 h1:oo2YHfGFKHvHS1urtqjOIKpaHwcdyqacwKHLXzUg33M= github.com/dell/gofsutil v1.12.0/go.mod h1:mGMN5grVDtHv2imNw5+gFr2RmCqeyYgBFBldUbHtV78= -github.com/dell/goscaleio v1.11.1-0.20230707063208-b67372e0f8d0 h1:3GqvTjqCAAG6y8FDUtKRR9q/Uj5lKD1hFD6w7FTzhww= -github.com/dell/goscaleio v1.11.1-0.20230707063208-b67372e0f8d0/go.mod h1:dMTrHnXSsPus+Kd9mrs0JuyrCndoKvFP/bbEdc21Bi8= -github.com/dell/goscaleio v1.11.1-0.20230722160317-b47d2ec46caa h1:3qX09PA+g/UjglChU7OLE7D25O9n0ZsF9ERVvhKy6dg= -github.com/dell/goscaleio v1.11.1-0.20230722160317-b47d2ec46caa/go.mod h1:dMTrHnXSsPus+Kd9mrs0JuyrCndoKvFP/bbEdc21Bi8= -github.com/dell/goscaleio v1.11.1-0.20230724113841-f10386ad0bb4 h1:6IDogX2aGiWQvybl3cWwtFdrLwiXE753sG6mDZr9Q3g= -github.com/dell/goscaleio v1.11.1-0.20230724113841-f10386ad0bb4/go.mod h1:dMTrHnXSsPus+Kd9mrs0JuyrCndoKvFP/bbEdc21Bi8= +github.com/dell/goscaleio v1.11.1-0.20230721055528-55caf6d14be6 h1:xsB1Ergjq3C4DrIhixHfBsoJ8lYj9uY/HsTVqDD9EN8= +github.com/dell/goscaleio v1.11.1-0.20230721055528-55caf6d14be6/go.mod h1:dMTrHnXSsPus+Kd9mrs0JuyrCndoKvFP/bbEdc21Bi8= +github.com/dell/goscaleio v1.11.1-0.20230724122254-8f8b41ad4aad h1:3yLaVslMtLDGrWBkR88F+9tKgJjEKWYrlj7f0aVQd9k= +github.com/dell/goscaleio v1.11.1-0.20230724122254-8f8b41ad4aad/go.mod h1:dMTrHnXSsPus+Kd9mrs0JuyrCndoKvFP/bbEdc21Bi8= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= diff --git a/helm/csi-vxflexos/templates/controller.yaml b/helm/csi-vxflexos/templates/controller.yaml index 00cca354..6cb19657 100644 --- a/helm/csi-vxflexos/templates/controller.yaml +++ b/helm/csi-vxflexos/templates/controller.yaml @@ -405,6 +405,12 @@ spec: - name: X_CSI_POWERFLEX_EXTERNAL_ACCESS value: "{{ .Values.externalAccess }}" {{- end }} + {{- if hasKey .Values "enableQuota" }} + {{- if eq .Values.enableQuota true}} + - name: X_CSI_QUOTA_ENABLED + value: "{{ .Values.enableQuota }}" + {{- end }} + {{- end }} volumeMounts: - name: socket-dir mountPath: /var/run/csi diff --git a/helm/csi-vxflexos/values.yaml b/helm/csi-vxflexos/values.yaml index de1e73e1..8856f057 100644 --- a/helm/csi-vxflexos/values.yaml +++ b/helm/csi-vxflexos/values.yaml @@ -66,6 +66,14 @@ imagePullPolicy: IfNotPresent # Default value: "0777" nfsAcls: "0777" +# enableQuota: a boolean that, when enabled, will set quota limit for a newly provisioned NFS volume. +# Allowed values: +# true: set quota for volume +# false: do not set quota for volume +# Optional: true +# Default value: none +enableQuota: "false" + # "enablesnapshotcgdelete"- a boolean that, when enabled, will delete all snapshots in a consistency group # everytime a snap in the group is deleted # Allowed values: true, false @@ -74,7 +82,7 @@ enablesnapshotcgdelete: "false" # "enablelistvolumesnapshot" - a boolean that, when enabled, will allow list volume operation to include snapshots (since creating a volume # from a snap actually results in a new snap) -# It is recommend this be false unless instructed otherwise. +# It is recommended this be false unless instructed otherwise. # Allowed values: true, false # Default value: none enablelistvolumesnapshot: "false" @@ -82,7 +90,7 @@ enablelistvolumesnapshot: "false" # Setting allowRWOMultiPodAccess to "true" will allow multiple pods on the same node # to access the same RWO volume. This behavior conflicts with the CSI specification version 1.3 # NodePublishVolume descrition that requires an error to be returned in this case. -# However some other CSI drivers support this behavior and some customers desire this behavior. +# However, some other CSI drivers support this behavior and some customers desire this behavior. # Kubernetes could make a change at their discretion that would preclude our ability to support this option. # Customers use this option at their own risk. # You should leave this set as "false" unless instructed to change it by Dell support. diff --git a/samples/storageclass/storageclass-nfs.yaml b/samples/storageclass/storageclass-nfs.yaml index 179fdc31..c2ad74f7 100644 --- a/samples/storageclass/storageclass-nfs.yaml +++ b/samples/storageclass/storageclass-nfs.yaml @@ -81,6 +81,28 @@ parameters: # Optional: true # Default value: "0777" # nfsAcls: "0777" + + # path: relative path to the root of the associated filesystem. + # Allowed values: string + # Optional: true + # Default value: None + # Examples: /fs, /csi + # path: /csi + + # softLimit: set soft limit to quota. + # Specified as a percentage + # Allowed values: int + # Optional: true + # Default value: 0, unlimited quota + # softLimit: "80" + + # gracePeriod: Grace period of tree quota, must be mentioned along with softLimit, in seconds. + # Soft Limit can be exceeded until the grace period. + # No hard limit when set to -1. + # Allowed values: int + # Optional: true + # Default value : 0 + # gracePeriod: "86400" # volumeBindingMode determines how volume binding and dynamic provisioning should occur # Allowed values: diff --git a/service/controller.go b/service/controller.go index 5b2cd9d5..175e6250 100644 --- a/service/controller.go +++ b/service/controller.go @@ -73,10 +73,23 @@ const ( // NFSExportLocalPath is the local path for NFSExport NFSExportLocalPath = "/" + // NFSExportNamePrefix is the prefix used for nfs exports created using // csi-powerflex driver NFSExportNamePrefix = "csishare-" + // KeyPath is the key used to get path of the associated filesystem + // from the volume create parameters map + KeyPath = "path" + + // KeySoftLimit is the key used to get the soft limit of the filesystem + // from the volume create parameters map + KeySoftLimit = "softLimit" + + // KeyGracePeriod is the key used to get the grace period from the + // volume create parameters map + KeyGracePeriod = "gracePeriod" + // DefaultVolumeSizeKiB is default volume sgolang/protobuf/blob/master/ptypesize // to create on a scaleIO cluster when no size is given, expressed in KiB DefaultVolumeSizeKiB = 16 * kiBytesInGiB @@ -361,9 +374,47 @@ func (s *service) CreateVolume( return nil, status.Errorf(codes.Unknown, "Create Volume %s failed with error: %v", volName, err) } + // set quota limits, if specified in NFS storage class + isQuotaEnabled := s.opts.IsQuotaEnabled + if isQuotaEnabled { + // get filesystem (NFS volume), newly created + fs, err := system.GetFileSystemByIDName(fsResp.ID, "") + if err != nil { + Log.Debugf("Find Volume response error: %v", err) + return nil, status.Errorf(codes.Unknown, "Find Volume response error: %v", err) + } + path, ok := params[KeyPath] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "`%s` is a required parameter", KeyPath) + } + + softLimit, ok := params[KeySoftLimit] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "`%s` is a required parameter", KeySoftLimit) + } + + gracePeriod, ok := params[KeyGracePeriod] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "`%s` is a required parameter", KeyGracePeriod) + } + + // create quota for the filesystem + quotaID, err := s.createQuota(fsResp.ID, path, softLimit, gracePeriod, int(size), isQuotaEnabled, systemID) + if err != nil { + // roll back, delete the newly created volume + if err = system.DeleteFileSystem(fs.Name); err != nil { + return nil, status.Errorf(codes.Internal, + "rollback (deleting volume '%s') failed with error : '%v'", fs.Name, err.Error()) + } + return nil, fmt.Errorf("error creating quota ('%s', '%d' bytes), abort, also successfully rolled back by deleting the newly created volume", fs.Name, size) + } + Log.Infof("Tree quota set for: %d bytes on directory: '%s', quota ID: %s", size, path, quotaID) + } + newFs, err := system.GetFileSystemByIDName(fsResp.ID, "") if err != nil { - Log.Debugf("Find Volume response: %v Error: %v", newFs, err) + Log.Debugf("Find Volume response error: %v", err) + return nil, status.Errorf(codes.Unknown, "Find Volume response error: %v", err) } if newFs != nil { vi := s.getCSIVolumeFromFilesystem(newFs, systemID) @@ -524,6 +575,99 @@ func (s *service) CreateVolume( return nil, status.Errorf(codes.NotFound, "Volume/Filesystem not found after create. %v", err) } +func (s *service) createQuota(fsID, path, softLimit, gracePeriod string, size int, isQuotaEnabled bool, systemID string) (string, error) { + system, err := s.adminClients[systemID].FindSystem(systemID, "", "") + if err != nil { + return "", err + } + + // enabling quota on FS + fs, err := system.GetFileSystemByIDName(fsID, "") + if err != nil { + Log.Debugf("Find Volume response error: %v", err) + return "", status.Errorf(codes.Unknown, "Find Volume response error: %v", err) + } + + var softLimitInt, gracePeriodInt int64 + gracePeriodInt, err = strconv.ParseInt(gracePeriod, 10, 64) + if err != nil { + Log.Debugf("Invalid gracePeriod value. Setting it to default.") + gracePeriodInt = 0 + } + + // converting soft limit from percentage to value + if softLimit != "" { + softi, err := strconv.ParseInt(softLimit, 10, 64) + if err != nil { + Log.Debugf("Invalid softLimit value. Setting it to default.") + softLimitInt = 0 + } else { + softLimitInt = (softi * int64(size)) / 100 + } + } + + // modify FS to set quota + fsModify := &siotypes.FSModify{ + IsQuotaEnabled: isQuotaEnabled, + } + + err = system.ModifyFileSystem(fsModify, fs.ID) + if err != nil { + Log.Debugf("Modify filesystem failed with error: %v", err) + return "", status.Errorf(codes.Unknown, "Modify filesystem failed with error: %v", err) + } + + fs, err = system.GetFileSystemByIDName(fsID, "") + if err != nil { + Log.Debugf("Find Volume response error: %v", err) + return "", status.Errorf(codes.Unknown, "Find Volume response error: %v", err) + } + + // need to set the quota based on the requested pv size + // if a size isn't requested, skip creating the quota + if size <= 0 { + Log.Debugf("Quotas is enabled, but storage size is not requested, skip creating quotas for volume '%s'", fsID) + return "", nil + } + + // Check if softLimit < 100 + if int(softLimitInt) >= size { + Log.Warnf("SoftLimit thresholds must be smaller than the hard threshold. Setting it to default for Volume '%s'", fsID) + softLimitInt, gracePeriodInt = 0, 0 + } + + //Check if grace period is set along with soft limit + if (softLimitInt != 0) && (gracePeriodInt == 0) { + Log.Warnf("Grace period must be configured along with soft limit. Setting it to default for Volume '%s'", fsID) + softLimitInt, gracePeriodInt = 0, 0 + } + + Log.Debugf("Begin to set quota for FS '%s', size '%d', quota enabled: '%t'", fsID, size, isQuotaEnabled) + // log all parameters used in CreateTreeQuota call + fields := map[string]interface{}{ + "FileSystemID": fsID, + "Path": path, + "HardLimit": size, + "SoftLimit": softLimitInt, + "GracePeriod": gracePeriodInt, + } + Log.WithFields(fields).Info("Executing CreateTreeQuota with following fields") + + createQuotaParams := &siotypes.TreeQuotaCreate{ + FileSystemID: fsID, + Path: path, + HardLimit: size, + SoftLimit: int(softLimitInt), + GracePeriod: int(gracePeriodInt), + } + quota, err := system.CreateTreeQuota(createQuotaParams) + if err != nil { + Log.Debugf("Creating quota failed with error: %v", err) + return "", status.Errorf(codes.Unknown, "Creating quota failed with error: %v", err) + } + return quota.ID, nil +} + // Copies the interesting parameters to the output map. func copyInterestingParameters(parameters, out map[string]string) { for _, str := range interestingParameters { @@ -2617,6 +2761,33 @@ func (s *service) ControllerExpandVolume(ctx context.Context, req *csi.Controlle return nil, status.Error(codes.Internal, err.Error()) } + // update tree quota hard limit and soft limit if pvc size has changed + + isQuotaEnabled := s.opts.IsQuotaEnabled + if isQuotaEnabled && fs.IsQuotaEnabled { + treeQuota, err := system.GetTreeQuotaByFSID(fsID) + if err != nil { + Log.Errorf("Fetching tree quota for filesystem failed, error: %s", err.Error()) + return nil, status.Error(codes.Internal, err.Error()) + } + + // Modify Tree Quota + updatedSoftLimit := treeQuota.SoftLimit * (requestedSize / treeQuota.HardLimit) + treeQuotaID := treeQuota.ID + Log.Infof("Modifying tree quota ID %s for filesystem ID: %s", treeQuotaID, fsID) + quotaModify := &siotypes.TreeQuotaModify{ + HardLimit: requestedSize, + SoftLimit: updatedSoftLimit, + } + + err = system.ModifyTreeQuota(quotaModify, treeQuotaID) + if err != nil { + Log.Errorf("Modifying tree quota for filesystem failed, error: %s", err.Error()) + return nil, status.Error(codes.Internal, err.Error()) + } + Log.Infof("Tree quota modified successfully.") + } + csiResp := &csi.ControllerExpandVolumeResponse{ CapacityBytes: int64(requestedSize), NodeExpansionRequired: false, diff --git a/service/envvars.go b/service/envvars.go index 5cb89a5c..eac929f2 100644 --- a/service/envvars.go +++ b/service/envvars.go @@ -63,6 +63,10 @@ const ( // EnvExternalAccess is used to specify additional entries for host to access NFS volumes. EnvExternalAccess = "X_CSI_POWERFLEX_EXTERNAL_ACCESS" + // EnvMaxVolumesPerNode specifies maximum number of volumes that controller can publish to the node. EnvMaxVolumesPerNode = "X_CSI_MAX_VOLUMES_PER_NODE" + + // EnvQuotaEnabled enables setting of quota for NFS volumes. + EnvQuotaEnabled = "X_CSI_QUOTA_ENABLED" ) diff --git a/service/features/controller_publish_unpublish.feature b/service/features/controller_publish_unpublish.feature index ddcc653c..e194fa35 100644 --- a/service/features/controller_publish_unpublish.feature +++ b/service/features/controller_publish_unpublish.feature @@ -552,3 +552,62 @@ Feature: VxFlex OS CSI interface And I call UnpublishVolume And no error was received Then the number of SDC mappings is 0 + + Scenario: Create NFS volume, enable quota with all key parameters + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "86400" + And I call CreateVolumeSize nfs "vol-inttest-nfs" "10" + Then a valid CreateVolumeResponse is returned + + Scenario: Create NFS volume, enable quota without path key + Given a VxFlexOS service + And I enable quota for filesystem + And I specify NoPath + And I call CreateVolumeSize nfs "vol-inttest-nfs" "10" + Then the error contains "rpc error: code = InvalidArgument desc = `path` is a required parameter" + + Scenario: Create NFS volume, enable quota without soft limit key + Given a VxFlexOS service + And I enable quota for filesystem + And I specify NoSoftLimit + And I call CreateVolumeSize nfs "vol-inttest-nfs" "10" + Then the error contains "rpc error: code = InvalidArgument desc = `softLimit` is a required parameter" + + Scenario: Create NFS volume, enable quota without grace period key + Given a VxFlexOS service + And I enable quota for filesystem + And I specify NoGracePeriod + And I call CreateVolumeSize nfs "vol-inttest-nfs" "10" + Then the error contains "rpc error: code = InvalidArgument desc = `gracePeriod` is a required parameter" + + Scenario: Create NFS volume, create quota error + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "86400" + And I induce error "CreateQuotaError" + And I call CreateVolumeSize nfs "vol-inttest-nfs" "10" + Then the error contains "error creating quota ('vol-inttest-nfs', '10737418240' bytes), abort, also successfully rolled back by deleting the newly created volume" + + Scenario: Create NFS volume, invalid soft limit, set to default + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "abc" graceperiod "86400" + And I call CreateVolumeSize nfs "vol-inttest-nfs" "10" + Then a valid CreateVolumeResponse is returned + + Scenario: Create NFS volume, invalid grace period, set to default + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "xyz" + And I call CreateVolumeSize nfs "vol-inttest-nfs" "10" + Then a valid CreateVolumeResponse is returned + + Scenario: Create NFS volume, enable quota, with FS quota disabled + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "86400" + And a capability with voltype "mount" access "single-node-single-writer" fstype "nfs" + And I induce error "FSQuotaError" + When I call CreateVolumeSize nfs "vol-inttest-nfs" "8" + Then the error contains "error creating quota " diff --git a/service/features/filesystem.json.template b/service/features/filesystem.json.template index 605f8ed6..7d7ad786 100644 --- a/service/features/filesystem.json.template +++ b/service/features/filesystem.json.template @@ -19,7 +19,7 @@ "is_smb_notify_on_write_enabled": false, "smb_notify_on_change_dir_depth": 512, "is_async_MTime_enabled": false, - "is_quota_enabled": true, + "is_quota_enabled": __IS_QUOTA_ENABLED__, "grace_period": 86400, "default_hard_limit": 6442450944, "default_soft_limit": 3221225472, diff --git a/service/features/service.feature b/service/features/service.feature index 58cab2d4..480d1c12 100644 --- a/service/features/service.feature +++ b/service/features/service.feature @@ -824,7 +824,7 @@ Feature: VxFlex OS CSI interface When I call Probe And I induce error "DeleteSnapshotError" And I call DeleteSnapshot NFS - Then the error contains "error while deleting the filesytem snapshot" + Then the error contains "error while deleting the filesystem snapshot" Scenario: Delete snapshot consistency group Given a VxFlexOS service @@ -1305,4 +1305,67 @@ Feature: VxFlex OS CSI interface And a controller published volume And I induce error "NoVolumeIDError" Then I call ControllerExpandVolume set to "16" - And the error contains "volume ID is required" \ No newline at end of file + And the error contains "volume ID is required" + + Scenario: Controller expand volume for NFS with quota enabled + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "86400" + And a capability with voltype "mount" access "single-node-single-writer" fstype "nfs" + When I call CreateVolumeSize nfs "vol-inttest-nfs" "8" + And a controller published volume + When I call ControllerExpandVolume set to "12" + Then no error was received + + Scenario: Controller expand volume for NFS with quota disabled + Given a VxFlexOS service + And I disable quota for filesystem + And a capability with voltype "mount" access "single-node-single-writer" fstype "nfs" + When I call CreateVolumeSize nfs "vol-inttest-nfs" "8" + And a controller published volume + When I call ControllerExpandVolume set to "12" + Then no error was received + + Scenario: Controller expand volume for NFS with quota enabled, modify filesystem error + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "86400" + And a capability with voltype "mount" access "single-node-single-writer" fstype "nfs" + When I call CreateVolumeSize nfs "vol-inttest-nfs" "8" + And I induce error "ModifyFSError" + And a controller published volume + When I call ControllerExpandVolume set to "12" + Then the error contains "Modify filesystem failed with error:" + + Scenario: Controller expand volume for NFS with quota enabled, modify quota error + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "86400" + And a capability with voltype "mount" access "single-node-single-writer" fstype "nfs" + When I call CreateVolumeSize nfs "vol-inttest-nfs" "8" + And I induce error "ModifyQuotaError" + And a controller published volume + When I call ControllerExpandVolume set to "12" + Then the error contains "Modifying tree quota for filesystem failed, error:" + + Scenario: Controller expand volume for NFS with quota enabled, GetFileSystemsByIdError + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "86400" + And a capability with voltype "mount" access "single-node-single-writer" fstype "nfs" + When I call CreateVolumeSize nfs "vol-inttest-nfs" "8" + And I induce error "GetFileSystemsByIdError" + And a controller published volume + When I call ControllerExpandVolume set to "12" + Then the error contains "rpc error: code = NotFound desc = volume" + + Scenario: Controller expand volume for NFS with quota enabled, GetQuotaByFSIDError + Given a VxFlexOS service + And I enable quota for filesystem + And I set quota with path "/fs" softLimit "20" graceperiod "86400" + And a capability with voltype "mount" access "single-node-single-writer" fstype "nfs" + When I call CreateVolumeSize nfs "vol-inttest-nfs" "8" + And I induce error "GetQuotaByFSIDError" + And a controller published volume + When I call ControllerExpandVolume set to "12" + Then the error contains "Fetching tree quota for filesystem failed, error:" diff --git a/service/features/treequota.json.template b/service/features/treequota.json.template new file mode 100644 index 00000000..ab118302 --- /dev/null +++ b/service/features/treequota.json.template @@ -0,0 +1,12 @@ +{ + "id": "__ID__", + "file_system_id": "766f6c2d696e74746573742d6e6673", + "path": "__PATH__", + "description": "tree quota modified", + "is_user_quotas_enforced": false, + "state": "OK", + "hard_limit": __HARD_LIMIT_SIZE__, + "soft_limit": __SOFT_LIMIT_SIZE__, + "remaining_grace_period": __GRACE_PERIOD__, + "size_used": 0 +} \ No newline at end of file diff --git a/service/service.go b/service/service.go index 0ac0714d..0834bc81 100644 --- a/service/service.go +++ b/service/service.go @@ -150,9 +150,10 @@ type Opts struct { IsApproveSDCEnabled bool replicationContextPrefix string replicationPrefix string - NfsAcls string - ExternalAccess string + NfsAcls string // enables setting permissions on NFS mount directory + ExternalAccess string // allow additional entries for host to access NFS volumes MaxVolumesPerNode int64 + IsQuotaEnabled bool // allow driver to enable quota limits for NFS volumes } type service struct { @@ -347,6 +348,7 @@ func (s *service) BeforeServe( "nfsAcls": s.opts.NfsAcls, "externalAccess": s.opts.ExternalAccess, "MaxVolumesPerNode": s.opts.MaxVolumesPerNode, + "IsQuotaEnabled": s.opts.IsQuotaEnabled, } Log.WithFields(fields).Infof("configured %s", Name) @@ -411,6 +413,11 @@ func (s *service) BeforeServe( opts.IsApproveSDCEnabled = true } } + if quotaEnabled, ok := csictx.LookupEnv(ctx, EnvQuotaEnabled); ok { + if quotaEnabled == "true" { + opts.IsQuotaEnabled = true + } + } if s.privDir == "" { s.privDir = defaultPrivDir diff --git a/service/step_defs_test.go b/service/step_defs_test.go index 87217ebb..8be83f2c 100644 --- a/service/step_defs_test.go +++ b/service/step_defs_test.go @@ -642,6 +642,9 @@ func getTypicalNFSCreateVolumeRequest() *csi.CreateVolumeRequest { req := new(csi.CreateVolumeRequest) params := make(map[string]string) params["storagepool"] = "viki_pool_HDD_20181031" + params["path"] = "/fs" + params["softLimit"] = "0" + params["gracePeriod"] = "0" req.Parameters = params req.Name = "mount1" capacityRange := new(csi.CapacityRange) @@ -4325,6 +4328,56 @@ func (f *feature) iCallExecuteAction(arg1 string) error { return nil } +func (f *feature) iCallEnableFSQuota() error { + f.service.opts.IsQuotaEnabled = true + isQuotaEnabled = true + return nil +} + +func (f *feature) iCallDisableFSQuota() error { + f.service.opts.IsQuotaEnabled = false + isQuotaEnabled = false + return nil +} + +func (f *feature) iCallSetQuotaParams(path, softlimit, graceperiod string) error { + if f.createVolumeRequest == nil { + req := getTypicalNFSCreateVolumeRequest() + f.createVolumeRequest = req + } + f.createVolumeRequest.Parameters["path"] = path + f.createVolumeRequest.Parameters["softLimit"] = softlimit + f.createVolumeRequest.Parameters["gracePeriod"] = graceperiod + return nil +} + +func (f *feature) iSpecifyNoPath() error { + if f.createVolumeRequest == nil { + req := getTypicalNFSCreateVolumeRequest() + f.createVolumeRequest = req + } + delete(f.createVolumeRequest.Parameters, "path") + return nil +} + +func (f *feature) iSpecifyNoSoftLimit() error { + if f.createVolumeRequest == nil { + req := getTypicalNFSCreateVolumeRequest() + f.createVolumeRequest = req + } + delete(f.createVolumeRequest.Parameters, "softLimit") + return nil +} + +func (f *feature) iSpecifyNoGracePeriod() error { + if f.createVolumeRequest == nil { + req := getTypicalNFSCreateVolumeRequest() + f.createVolumeRequest = req + } + delete(f.createVolumeRequest.Parameters, "gracePeriod") + return nil +} + func FeatureContext(s *godog.ScenarioContext) { f := &feature{} s.Step(`^a VxFlexOS service$`, f.aVxFlexOSService) @@ -4523,6 +4576,12 @@ func FeatureContext(s *godog.ScenarioContext) { s.Step(`^I call DeleteVolume "([^"]*)"$`, f.iCallDeleteVolume) s.Step(`^I call DeleteStorageProtectionGroup$`, f.iCallDeleteStorageProtectionGroup) s.Step(`^I call ExecuteAction "([^"]*)"$`, f.iCallExecuteAction) + s.Step(`^I enable quota for filesystem$`, f.iCallEnableFSQuota) + s.Step(`^I disable quota for filesystem$`, f.iCallDisableFSQuota) + s.Step(`^I set quota with path "([^"]*)" softLimit "([^"]*)" graceperiod "([^"]*)"$`, f.iCallSetQuotaParams) + s.Step(`^I specify NoPath$`, f.iSpecifyNoPath) + s.Step(`^I specify NoSoftLimit`, f.iSpecifyNoSoftLimit) + s.Step(`^I specify NoGracePeriod`, f.iSpecifyNoGracePeriod) s.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { if f.server != nil { diff --git a/service/step_handlers_test.go b/service/step_handlers_test.go index e321074e..61f4f1de 100644 --- a/service/step_handlers_test.go +++ b/service/step_handlers_test.go @@ -37,6 +37,7 @@ var ( sdcMappingsID string setSdcNameSuccess bool sdcIDToName map[string]string + isQuotaEnabled bool stepHandlersErrors struct { FindVolumeIDError bool @@ -149,6 +150,11 @@ func getHandler() http.Handler { nfsExportIDReadWriteRootHosts = make(map[string][]string) nfsExportIDReadWriteHosts = make(map[string][]string) nfsExportIDReadOnlyHosts = make(map[string][]string) + treeQuotaID = make(map[string]string) + treeQuotaIDToPath = make(map[string]string) + treeQuotaIDToSoftLimit = make(map[string]string) + treeQuotaIDToGracePeriod = make(map[string]string) + treeQuotaIDToHardLimit = make(map[string]string) debug = false stepHandlersErrors.FindVolumeIDError = false stepHandlersErrors.GetVolByIDError = false @@ -245,6 +251,8 @@ func getRouter() http.Handler { scaleioRouter.HandleFunc("/api/types/PeerMdm/instances", handlePeerMdmInstances) scaleioRouter.HandleFunc("/api/types/ReplicationConsistencyGroup/instances", handleReplicationConsistencyGroupInstances) scaleioRouter.HandleFunc("/api/types/ReplicationPair/instances", handleReplicationPairInstances) + scaleioRouter.HandleFunc("/rest/v1/file-tree-quotas", handleFileTreeQuotas) + scaleioRouter.HandleFunc("/rest/v1/file-tree-quotas/{id}", handleGetFileTreeQuotas) return scaleioRouter } @@ -770,6 +778,7 @@ func handleFileSystems(w http.ResponseWriter, r *http.Request) { replacementMap["__NAME__"] = fs["name"] replacementMap["__SIZE_IN_Total__"] = fs["size_total"] replacementMap["__PARENT_ID__"] = fs["parent_id"] + replacementMap["__IS_QUOTA_ENABLED__"] = strconv.FormatBool(isQuotaEnabled) data := returnJSONFile("features", "filesystem.json.template", nil, replacementMap) fs := new(types.FileSystem) err := json.Unmarshal(data, fs) @@ -792,6 +801,7 @@ func handleFileSystems(w http.ResponseWriter, r *http.Request) { replacementMap["__NAME__"] = name replacementMap["__SIZE_IN_Total__"] = fileSystemIDToSizeTotal[id] replacementMap["__PARENT_ID__"] = fileSystemIDParentID[id] + replacementMap["__IS_QUOTA_ENABLED__"] = strconv.FormatBool(isQuotaEnabled) data := returnJSONFile("features", "filesystem.json.template", nil, replacementMap) fs := new(types.FileSystem) err := json.Unmarshal(data, fs) @@ -840,6 +850,7 @@ func handleGetFileSystems(w http.ResponseWriter, r *http.Request) { replacementMap["__ID__"] = fs["id"] replacementMap["__NAME__"] = fs["name"] replacementMap["__SIZE_IN_Total__"] = fs["size_total"] + replacementMap["__IS_QUOTA_ENABLED__"] = strconv.FormatBool(isQuotaEnabled) replacementMap["__PARENT_ID__"] = fs["parent_id"] if fs["parent_id"] != "" { if inducedError.Error() == "GetSnashotByIdError" { @@ -852,6 +863,7 @@ func handleGetFileSystems(w http.ResponseWriter, r *http.Request) { replacementMap["__NAME__"] = fileSystemIDName[id] replacementMap["__SIZE_IN_Total__"] = fileSystemIDToSizeTotal[id] replacementMap["__PARENT_ID__"] = fileSystemIDParentID[id] + replacementMap["__IS_QUOTA_ENABLED__"] = strconv.FormatBool(isQuotaEnabled) if fileSystemIDParentID[id] != "" { if inducedError.Error() == "GetSnashotByIdError" { writeError(w, "could not find snapshot id", http.StatusNotFound, codes.NotFound) @@ -874,7 +886,7 @@ func handleGetFileSystems(w http.ResponseWriter, r *http.Request) { } case http.MethodDelete: if inducedError.Error() == "DeleteSnapshotError" { - writeError(w, "error while deleting the filesytem snapshot", http.StatusGatewayTimeout, codes.Internal) + writeError(w, "error while deleting the filesystem snapshot", http.StatusGatewayTimeout, codes.Internal) return } vars := mux.Vars(r) @@ -896,7 +908,38 @@ func handleGetFileSystems(w http.ResponseWriter, r *http.Request) { fileSystemIDName[id] = "" fileSystemNameToID[fs["name"]] = "" fileSystemIDToSizeTotal[id] = "" + case http.MethodPatch: + req := types.FSModify{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&req) + if err != nil { + log.Printf("error decoding json: %s\n", err.Error()) + } + if inducedError.Error() == "ModifyFSError" { + writeError(w, "Modify filesystem failed with error:", http.StatusRequestTimeout, codes.Internal) + return + } + if inducedError.Error() == "FSQuotaError" { + req.IsQuotaEnabled = false + writeError(w, "error creating quota ", http.StatusRequestTimeout, codes.Internal) + return + } + vars := mux.Vars(r) + id := vars["id"] + fmt.Println("id:", id) + fmt.Printf("patchReq:%#v\n", req) + fmt.Printf("req.IsQuotaEnabled:%#v\n", req.IsQuotaEnabled) + if array, ok := systemArrays[r.Host]; ok { + array.fileSystems[id]["size"] = strconv.Itoa(req.Size) + array.fileSystems[id]["description"] = req.Description + array.fileSystems[id]["isquotaenabled"] = strconv.FormatBool(req.IsQuotaEnabled) + array.fileSystems[id]["hardlimit"] = strconv.Itoa(req.DefaultHardLimit) + array.fileSystems[id]["softlimit"] = strconv.Itoa(req.DefaultSoftLimit) + array.fileSystems[id]["graceperiod"] = strconv.Itoa(req.GracePeriod) + } + w.WriteHeader(http.StatusNoContent) + log.Printf("end modify file systems") } // returnJSONFile("features", "get_file_system_response.json", w, nil) @@ -1021,6 +1064,21 @@ var fileSystemIDToSizeTotal map[string]string // Replication group state to replace for. var replicationGroupState string +// Map of Tree quota ID +var treeQuotaID map[string]string + +// Map of Tree quota ID to Path +var treeQuotaIDToPath map[string]string + +// Map of Tree quota ID to soft limit +var treeQuotaIDToSoftLimit map[string]string + +// Map of Tree quota ID to grace period +var treeQuotaIDToGracePeriod map[string]string + +// Map of Tree quota ID to hard limit +var treeQuotaIDToHardLimit map[string]string + // Possible rework, every systemID should have a instances similar to an array. type systemArray struct { ID string @@ -1030,6 +1088,7 @@ type systemArray struct { nfsExports map[string]map[string]string replicationConsistencyGroups map[string]map[string]string replicationPairs map[string]map[string]string + treeQuotas map[string]map[string]string } func (s *systemArray) Init() { @@ -1038,6 +1097,7 @@ func (s *systemArray) Init() { s.nfsExports = make(map[string]map[string]string) s.replicationConsistencyGroups = make(map[string]map[string]string) s.replicationPairs = make(map[string]map[string]string) + s.treeQuotas = make(map[string]map[string]string) } func (s *systemArray) Link(remoteSystem *systemArray) { @@ -1981,6 +2041,135 @@ func handleReplicationPairInstances(w http.ResponseWriter, r *http.Request) { } } +func handleFileTreeQuotas(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + if inducedError.Error() == "CreateQuotaError" { + writeError(w, "error creating tree quota", http.StatusRequestTimeout, codes.Internal) + return + } + req := types.TreeQuotaCreate{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&req) + if err != nil { + log.Printf("error decoding json: %s\n", err.Error()) + } + + // good response + resp := new(types.TreeQuotaCreateResponse) + resp.ID = hex.EncodeToString([]byte("dummy-name")) + treeQuotaID[resp.ID] = resp.ID + treeQuotaIDToPath[resp.ID] = req.Path + treeQuotaIDToSoftLimit[resp.ID] = strconv.Itoa(req.SoftLimit) + treeQuotaIDToGracePeriod[resp.ID] = strconv.Itoa(req.GracePeriod) + treeQuotaIDToHardLimit[resp.ID] = strconv.Itoa(req.HardLimit) + + if array, ok := systemArrays[r.Host]; ok { + fmt.Printf("Host Endpoint %s\n", r.Host) + array.treeQuotas[resp.ID] = make(map[string]string) + array.treeQuotas[resp.ID]["id"] = resp.ID + array.treeQuotas[resp.ID]["path"] = req.Path + array.treeQuotas[resp.ID]["description"] = req.Description + array.treeQuotas[resp.ID]["softlimit"] = strconv.Itoa(req.SoftLimit) + array.treeQuotas[resp.ID]["graceperiod"] = strconv.Itoa(req.GracePeriod) + array.treeQuotas[resp.ID]["hardlimit"] = strconv.Itoa(req.HardLimit) + } + if debug { + log.Printf("request \"dummy-name\" id: %s\n", resp.ID) + } + encoder := json.NewEncoder(w) + err = encoder.Encode(resp) + if err != nil { + log.Printf("error encoding json: %s\n", err.Error()) + } + log.Printf("end make tree quotas") + case http.MethodGet: + if inducedError.Error() == "GetQuotaByFSIDError" { + writeError(w, "Fetching tree quota for filesystem failed, error:", http.StatusRequestTimeout, codes.Internal) + return + } + instances := make([]*types.TreeQuota, 0) + treeQuotas := make(map[string]map[string]string) + + if array, ok := systemArrays[r.Host]; ok { + treeQuotas = array.treeQuotas + + for _, tq := range treeQuotas { + replacementMap := make(map[string]string) + replacementMap["__ID__"] = tq["id"] + replacementMap["__PATH__"] = tq["path"] + replacementMap["__HARD_LIMIT_SIZE__"] = tq["hardlimit"] + replacementMap["__SOFT_LIMIT_SIZE__"] = tq["softlimit"] + replacementMap["__GRACE_PERIOD__"] = tq["graceperiod"] + data := returnJSONFile("features", "treequota.json.template", nil, replacementMap) + tq := new(types.TreeQuota) + err := json.Unmarshal(data, tq) + if err != nil { + log.Printf("error unmarshalling json: %s\n", string(data)) + } + instances = append(instances, tq) + } + } + + // Add none-created volumes (old) + for id := range treeQuotaID { + if _, ok := treeQuotaID[id]; ok { + continue + } + + replacementMap := make(map[string]string) + replacementMap["__ID__"] = id + replacementMap["__PATH__"] = treeQuotaIDToPath["path"] + replacementMap["__HARD_LIMIT_SIZE__"] = treeQuotaIDToHardLimit["hardlimit"] + replacementMap["__SOFT_LIMIT_SIZE__"] = treeQuotaIDToSoftLimit["softlimit"] + replacementMap["__GRACE_PERIOD__"] = treeQuotaIDToGracePeriod["graceperiod"] + data := returnJSONFile("features", "filesystem.json.template", nil, replacementMap) + tq := new(types.TreeQuota) + err := json.Unmarshal(data, tq) + if err != nil { + log.Printf("error unmarshalling json: %s\n", string(data)) + } + instances = append(instances, tq) + } + + encoder := json.NewEncoder(w) + err := encoder.Encode(instances) + if err != nil { + log.Printf("error encoding json: %s\n", err) + } + log.Printf("end get tree quotas") + } +} + +func handleGetFileTreeQuotas(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPatch: + if inducedError.Error() == "ModifyQuotaError" { + writeError(w, "Modifying tree quota for filesystem failed, error:", http.StatusRequestTimeout, codes.Internal) + return + } + vars := mux.Vars(r) + id := vars["id"] + fmt.Println("id:", id) + + req := types.TreeQuotaModify{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&req) + if err != nil { + log.Printf("error decoding json: %s\n", err.Error()) + } + fmt.Printf("patchReq:%#v\n", req) + if array, ok := systemArrays[r.Host]; ok { + array.treeQuotas[id]["description"] = req.Description + array.treeQuotas[id]["softlimit"] = strconv.Itoa(req.SoftLimit) + array.treeQuotas[id]["graceperiod"] = strconv.Itoa(req.GracePeriod) + array.treeQuotas[id]["hardlimit"] = strconv.Itoa(req.HardLimit) + } + w.WriteHeader(http.StatusNoContent) + log.Printf("end modify tree quotas") + } +} + // Write an error code to the response writer func writeError(w http.ResponseWriter, message string, httpStatus int, errorCode codes.Code) { w.WriteHeader(httpStatus)