diff --git a/examples/experimental/persistent-volume-provisioning/README.md b/examples/experimental/persistent-volume-provisioning/README.md index 1a9ad0dc908..de83f46b181 100644 --- a/examples/experimental/persistent-volume-provisioning/README.md +++ b/examples/experimental/persistent-volume-provisioning/README.md @@ -80,6 +80,7 @@ parameters: secretName: "heketi-secret" gidMin: "40000" gidMax: "50000" + volumetype: "replicate:3" ``` * `resturl` : Gluster REST service/Heketi service url which provision gluster volumes on demand. The general format should be `IPaddress:Port` and this is a mandatory parameter for GlusterFS dynamic provisioner. If Heketi service is exposed as a routable service in openshift/kubernetes setup, this can have a format similar to @@ -96,6 +97,17 @@ Example of a secret can be found in [glusterfs-provisioning-secret.yaml](gluster * `gidMin` + `gidMax` : The minimum and maximum value of GID range for the storage class. A unique value (GID) in this range ( gidMin-gidMax ) will be used for dynamically provisioned volumes. These are optional values. If not specified, the volume will be provisioned with a value between 2000-2147483647 which are defaults for gidMin and gidMax respectively. +* `volumetype` : The volume type and it's parameters can be configured with this optional value. If the volume type is not mentioned, it's up to the provisioner to decide the volume type. +For example: + 'Replica volume': + `volumetype: replicate:3` where '3' is replica count. + 'Disperse/EC volume': + `volumetype: disperse:4:2` where '4' is data and '2' is the redundancy count. + 'Distribute volume': + `volumetype: none` + +For available volume types and it's administration options refer: ([Administration Guide](https://access.redhat.com/documentation/en-US/Red_Hat_Storage/3.1/html/Administration_Guide/part-Overview.html)) + Reference : ([How to configure Heketi](https://github.com/heketi/heketi/wiki/Setting-up-the-topology)) When the persistent volumes are dynamically provisioned, the Gluster plugin automatically create an endpoint and a headless service in the name `gluster-dynamic-`. This dynamic endpoint and service will be deleted automatically when the persistent volume claim is deleted. diff --git a/examples/experimental/persistent-volume-provisioning/glusterfs-dp.yaml b/examples/experimental/persistent-volume-provisioning/glusterfs-dp.yaml index b9d720ee065..d14f034733c 100644 --- a/examples/experimental/persistent-volume-provisioning/glusterfs-dp.yaml +++ b/examples/experimental/persistent-volume-provisioning/glusterfs-dp.yaml @@ -11,3 +11,4 @@ parameters: secretName: "heketi-secret" gidMin: "40000" gidMax: "50000" + volumetype: "replicate:3" diff --git a/pkg/volume/glusterfs/BUILD b/pkg/volume/glusterfs/BUILD index 92e2cedc243..7850847bf98 100644 --- a/pkg/volume/glusterfs/BUILD +++ b/pkg/volume/glusterfs/BUILD @@ -57,6 +57,7 @@ go_test( "//pkg/util/testing:go_default_library", "//pkg/volume:go_default_library", "//pkg/volume/testing:go_default_library", + "//vendor:github.com/heketi/heketi/pkg/glusterfs/api", ], ) diff --git a/pkg/volume/glusterfs/glusterfs.go b/pkg/volume/glusterfs/glusterfs.go index 61b9038bc0d..5349910fde6 100644 --- a/pkg/volume/glusterfs/glusterfs.go +++ b/pkg/volume/glusterfs/glusterfs.go @@ -378,6 +378,7 @@ type provisioningConfig struct { clusterId string gidMin int gidMax int + volumeType gapi.VolumeDurabilityInfo } type glusterfsVolumeProvisioner struct { @@ -402,6 +403,19 @@ func convertGid(gidString string) (int, error) { return gid, nil } +func convertVolumeParam(volumeString string) (int, error) { + + count, err := strconv.Atoi(volumeString) + if err != nil { + return 0, fmt.Errorf("failed to parse %q", volumeString) + } + + if count < 0 { + return 0, fmt.Errorf("negative values are not allowed") + } + return count, nil +} + func (plugin *glusterfsPlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, error) { return plugin.newDeleterInternal(spec) } @@ -849,6 +863,7 @@ func parseClassParameters(params map[string]string, kubeClient clientset.Interfa cfg.gidMax = defaultGidMax authEnabled := true + parseVolumeType := "" for k, v := range params { switch dstrings.ToLower(k) { case "resturl": @@ -891,6 +906,9 @@ func parseClassParameters(params map[string]string, kubeClient clientset.Interfa return nil, fmt.Errorf("glusterfs: gidMax must be <= %v", absoluteGidMax) } cfg.gidMax = parseGidMax + case "volumetype": + parseVolumeType = v + default: return nil, fmt.Errorf("glusterfs: invalid option %q for volume plugin %s", k, glusterfsPluginName) } @@ -900,6 +918,42 @@ func parseClassParameters(params map[string]string, kubeClient clientset.Interfa return nil, fmt.Errorf("StorageClass for provisioner %s must contain 'resturl' parameter", glusterfsPluginName) } + if len(parseVolumeType) == 0 { + cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityReplicate, Replicate: gapi.ReplicaDurability{Replica: replicaCount}} + } else { + parseVolumeTypeInfo := dstrings.Split(parseVolumeType, ":") + + switch parseVolumeTypeInfo[0] { + case "replicate": + if len(parseVolumeTypeInfo) >= 2 { + newReplicaCount, err := convertVolumeParam(parseVolumeTypeInfo[1]) + if err != nil { + return nil, fmt.Errorf("error [%v] when parsing value %q of option '%s' for volume plugin %s.", err, parseVolumeTypeInfo[1], "volumetype", glusterfsPluginName) + } + cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityReplicate, Replicate: gapi.ReplicaDurability{Replica: newReplicaCount}} + } else { + cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityReplicate, Replicate: gapi.ReplicaDurability{Replica: replicaCount}} + } + case "disperse": + if len(parseVolumeTypeInfo) >= 3 { + newDisperseData, err := convertVolumeParam(parseVolumeTypeInfo[1]) + if err != nil { + return nil, fmt.Errorf("error [%v] when parsing value %q of option '%s' for volume plugin %s.", parseVolumeTypeInfo[1], err, "volumetype", glusterfsPluginName) + } + newDisperseRedundancy, err := convertVolumeParam(parseVolumeTypeInfo[2]) + if err != nil { + return nil, fmt.Errorf("error [%v] when parsing value %q of option '%s' for volume plugin %s.", err, parseVolumeTypeInfo[2], "volumetype", glusterfsPluginName) + } + cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityEC, Disperse: gapi.DisperseDurability{Data: newDisperseData, Redundancy: newDisperseRedundancy}} + } else { + return nil, fmt.Errorf("StorageClass for provisioner %q must have data:redundancy count set for disperse volumes in storage class option '%s'", glusterfsPluginName, "volumetype") + } + case "none": + cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityDistributeOnly} + default: + return nil, fmt.Errorf("error parsing value for option 'volumetype' for volume plugin %s", glusterfsPluginName) + } + } if !authEnabled { cfg.user = "" cfg.secretName = "" diff --git a/pkg/volume/glusterfs/glusterfs_test.go b/pkg/volume/glusterfs/glusterfs_test.go index ae867d1a0a4..a1316087d11 100644 --- a/pkg/volume/glusterfs/glusterfs_test.go +++ b/pkg/volume/glusterfs/glusterfs_test.go @@ -22,6 +22,7 @@ import ( "reflect" "testing" + gapi "github.com/heketi/heketi/pkg/glusterfs/api" "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/fake" "k8s.io/kubernetes/pkg/client/testing/core" @@ -247,7 +248,6 @@ func TestParseClassParameters(t *testing.T) { "data": []byte("mypassword"), }, } - tests := []struct { name string parameters map[string]string @@ -271,6 +271,7 @@ func TestParseClassParameters(t *testing.T) { secretValue: "password", gidMin: 2000, gidMax: 2147483647, + volumeType: gapi.VolumeDurabilityInfo{Type: "replicate", Replicate: gapi.ReplicaDurability{Replica: 3}, Disperse: gapi.DisperseDurability{Data: 0, Redundancy: 0}}, }, }, { @@ -291,6 +292,7 @@ func TestParseClassParameters(t *testing.T) { secretValue: "mypassword", gidMin: 2000, gidMax: 2147483647, + volumeType: gapi.VolumeDurabilityInfo{Type: "replicate", Replicate: gapi.ReplicaDurability{Replica: 3}, Disperse: gapi.DisperseDurability{Data: 0, Redundancy: 0}}, }, }, { @@ -302,9 +304,10 @@ func TestParseClassParameters(t *testing.T) { &secret, false, // expect error &provisioningConfig{ - url: "https://localhost:8080", - gidMin: 2000, - gidMax: 2147483647, + url: "https://localhost:8080", + gidMin: 2000, + gidMax: 2147483647, + volumeType: gapi.VolumeDurabilityInfo{Type: "replicate", Replicate: gapi.ReplicaDurability{Replica: 3}, Disperse: gapi.DisperseDurability{Data: 0, Redundancy: 0}}, }, }, { @@ -438,9 +441,10 @@ func TestParseClassParameters(t *testing.T) { &secret, false, // expect error &provisioningConfig{ - url: "https://localhost:8080", - gidMin: 4000, - gidMax: 2147483647, + url: "https://localhost:8080", + gidMin: 4000, + gidMax: 2147483647, + volumeType: gapi.VolumeDurabilityInfo{Type: "replicate", Replicate: gapi.ReplicaDurability{Replica: 3}, Disperse: gapi.DisperseDurability{Data: 0, Redundancy: 0}}, }, }, { @@ -453,9 +457,10 @@ func TestParseClassParameters(t *testing.T) { &secret, false, // expect error &provisioningConfig{ - url: "https://localhost:8080", - gidMin: 2000, - gidMax: 5000, + url: "https://localhost:8080", + gidMin: 2000, + gidMax: 5000, + volumeType: gapi.VolumeDurabilityInfo{Type: "replicate", Replicate: gapi.ReplicaDurability{Replica: 3}, Disperse: gapi.DisperseDurability{Data: 0, Redundancy: 0}}, }, }, { @@ -469,11 +474,94 @@ func TestParseClassParameters(t *testing.T) { &secret, false, // expect error &provisioningConfig{ - url: "https://localhost:8080", - gidMin: 4000, - gidMax: 5000, + url: "https://localhost:8080", + gidMin: 4000, + gidMax: 5000, + volumeType: gapi.VolumeDurabilityInfo{Type: "replicate", Replicate: gapi.ReplicaDurability{Replica: 3}, Disperse: gapi.DisperseDurability{Data: 0, Redundancy: 0}}, }, }, + + { + "valid volumetype: replicate", + map[string]string{ + "resturl": "https://localhost:8080", + "restauthenabled": "false", + "gidMin": "4000", + "gidMax": "5000", + "volumetype": "replicate:4", + }, + &secret, + false, // expect error + &provisioningConfig{ + url: "https://localhost:8080", + gidMin: 4000, + gidMax: 5000, + volumeType: gapi.VolumeDurabilityInfo{Type: "replicate", Replicate: gapi.ReplicaDurability{Replica: 4}, Disperse: gapi.DisperseDurability{Data: 0, Redundancy: 0}}, + }, + }, + + { + "valid volumetype: disperse", + map[string]string{ + "resturl": "https://localhost:8080", + "restauthenabled": "false", + "gidMin": "4000", + "gidMax": "5000", + "volumetype": "disperse:4:2", + }, + &secret, + false, // expect error + &provisioningConfig{ + url: "https://localhost:8080", + gidMin: 4000, + gidMax: 5000, + volumeType: gapi.VolumeDurabilityInfo{Type: "disperse", Replicate: gapi.ReplicaDurability{Replica: 0}, Disperse: gapi.DisperseDurability{Data: 4, Redundancy: 2}}, + }, + }, + { + "invalid volumetype (disperse) parameter", + map[string]string{ + "resturl": "https://localhost:8080", + "restauthenabled": "false", + "volumetype": "disperse:4:asd", + }, + &secret, + true, // expect error + nil, + }, + { + "invalid volumetype (replicate) parameter", + map[string]string{ + "resturl": "https://localhost:8080", + "restauthenabled": "false", + "volumetype": "replicate:asd", + }, + &secret, + true, // expect error + nil, + }, + { + "invalid volumetype: unknown volumetype", + map[string]string{ + "resturl": "https://localhost:8080", + "restauthenabled": "false", + "volumetype": "dispersereplicate:4:2", + }, + &secret, + true, // expect error + nil, + }, + { + "invalid volumetype : negative value", + map[string]string{ + "resturl": "https://localhost:8080", + "restauthenabled": "false", + "volumetype": "replicate:-1000", + }, + &secret, + true, // expect error + nil, + }, } for _, test := range tests {