From 6bf7ebae9fcf77109fa18431e4a003a94d1a1f20 Mon Sep 17 00:00:00 2001 From: Clinton Knight Date: Mon, 4 Feb 2019 15:38:39 -0500 Subject: [PATCH] Added infrastructure for virtual pools --- core/orchestrator_core_test.go | 26 +- persistent_store/passthrough_test.go | 25 +- storage/backend.go | 57 +- storage/fake/volume.go | 7 +- storage/storage_pool.go | 19 +- storage_attribute/bool.go | 24 + storage_attribute/common_attributes.go | 10 + storage_attribute/label.go | 363 ++++++++++++ storage_attribute/offer.go | 8 +- storage_attribute/request.go | 7 + storage_attribute/storage_attribute_test.go | 113 +++- storage_attribute/string.go | 24 + storage_attribute/types.go | 10 + storage_class/storage_class.go | 25 + storage_drivers/eseries/eseries_iscsi.go | 43 +- storage_drivers/fake/fake.go | 552 ++++++++++++++---- .../fake}/storage_class_test.go | 200 +++++-- .../fake}/test_utils/utils.go | 36 ++ storage_drivers/ontap/ontap_nas.go | 46 +- storage_drivers/ontap/ontap_nas_flexgroup.go | 36 +- storage_drivers/ontap/ontap_nas_qtree.go | 45 +- storage_drivers/ontap/ontap_san.go | 48 +- storage_drivers/solidfire/solidfire_san.go | 57 +- storage_drivers/types.go | 63 +- 24 files changed, 1486 insertions(+), 358 deletions(-) create mode 100644 storage_attribute/label.go rename {storage_class => storage_drivers/fake}/storage_class_test.go (78%) rename {storage_class => storage_drivers/fake}/test_utils/utils.go (55%) diff --git a/core/orchestrator_core_test.go b/core/orchestrator_core_test.go index 5687370e1..56bc10260 100644 --- a/core/orchestrator_core_test.go +++ b/core/orchestrator_core_test.go @@ -18,8 +18,8 @@ import ( "github.com/netapp/trident/storage/fake" sa "github.com/netapp/trident/storage_attribute" "github.com/netapp/trident/storage_class" - tu "github.com/netapp/trident/storage_class/test_utils" fakedriver "github.com/netapp/trident/storage_drivers/fake" + tu "github.com/netapp/trident/storage_drivers/fake/test_utils" ) var ( @@ -108,8 +108,8 @@ func cleanupStoreVersion(t *testing.T, etcd string) { func diffConfig(expected, got interface{}, fieldToSkip string) []string { diffs := make([]string, 0, 0) - expectedStruct := reflect.ValueOf(expected).Elem() - gotStruct := reflect.ValueOf(got).Elem() + expectedStruct := reflect.Indirect(reflect.ValueOf(expected)) + gotStruct := reflect.Indirect(reflect.ValueOf(got)) for i := 0; i < expectedStruct.NumField(); i++ { @@ -149,11 +149,17 @@ func diffExternalBackends(t *testing.T, expected, got *storage.BackendExternal) expectedConfig := expected.Config gotConfig := got.Config - expectedValElem := reflect.ValueOf(expected.Config).Elem() - gotValElem := reflect.ValueOf(got.Config).Elem() + expectedConfigTypeName := reflect.TypeOf(expectedConfig).Name() + gotConfigTypeName := reflect.TypeOf(gotConfig).Name() + if expectedConfigTypeName != gotConfigTypeName { + t.Errorf("Config type mismatch: %v != %v", expectedConfigTypeName, gotConfigTypeName) + } + + expectedConfigValue := reflect.ValueOf(expectedConfig) + gotConfigValue := reflect.ValueOf(gotConfig) - expectedCSDCIntf := expectedValElem.FieldByName("CommonStorageDriverConfigExternal").Interface() - gotCSDCIntf := gotValElem.FieldByName("CommonStorageDriverConfigExternal").Interface() + expectedCSDCIntf := expectedConfigValue.FieldByName("CommonStorageDriverConfig").Interface() + gotCSDCIntf := gotConfigValue.FieldByName("CommonStorageDriverConfig").Interface() var configDiffs []string @@ -1344,8 +1350,7 @@ func TestBackendUpdateAndDelete(t *testing.T) { persistentBackend, err := orchestrator.storeClient.GetBackend(backendName) if err != nil { t.Error("Unable to retrieve backend from store client: ", err) - } else if !reflect.DeepEqual(newBackend.ConstructPersistent(), - persistentBackend) { + } else if !reflect.DeepEqual(newBackend.ConstructPersistent(), persistentBackend) { t.Errorf("Backend not correctly updated in persistent store.") } previousBackends = append(previousBackends, newBackend) @@ -1416,8 +1421,7 @@ func TestBackendUpdateAndDelete(t *testing.T) { t.Error("Unable to find backend after bootstrapping.") } else if !reflect.DeepEqual(bootstrappedBackend, backend.ConstructExternal()) { - diffExternalBackends(t, backend.ConstructExternal(), - bootstrappedBackend) + diffExternalBackends(t, backend.ConstructExternal(), bootstrappedBackend) } orchestrator.mutex.Unlock() diff --git a/persistent_store/passthrough_test.go b/persistent_store/passthrough_test.go index af50707c8..e44289671 100644 --- a/persistent_store/passthrough_test.go +++ b/persistent_store/passthrough_test.go @@ -523,13 +523,30 @@ func TestPassthroughClient_DeleteVolumeIgnoreNotFoundNonexistent(t *testing.T) { func TestPassthroughClient_GetVolumes(t *testing.T) { p := newPassthroughClient() fakeBackend := getFakeBackend() - createOpts := map[string]string{"pool": "pool-0"} - fakeBackend.Driver.Create("fake_volume_1", 1000000000, createOpts) - fakeBackend.Driver.Create("fake_volume_2", 2000000000, createOpts) + + volConfig := &storage.VolumeConfig{ + Name: "fake_volume_1", + InternalName: "trident_fake_volume_1", + Size: "1000000000", + } + err := fakeBackend.Driver.Create(volConfig, fakeBackend.Storage["pool-0"], make(map[string]sa.Request)) + if err != nil { + t.Error(err) + } + + volConfig = &storage.VolumeConfig{ + Name: "fake_volume_2", + InternalName: "trident_fake_volume_2", + Size: "2000000000", + } + err = fakeBackend.Driver.Create(volConfig, fakeBackend.Storage["pool-0"], make(map[string]sa.Request)) + if err != nil { + t.Error(err) + } + p.AddBackend(fakeBackend) result, err := p.GetVolumes() - if err != nil { t.Error("Could not get volumes from passthrough client!") } diff --git a/storage/backend.go b/storage/backend.go index 6e3334225..fbb18a1ad 100644 --- a/storage/backend.go +++ b/storage/backend.go @@ -14,7 +14,7 @@ import ( log "github.com/sirupsen/logrus" tridentconfig "github.com/netapp/trident/config" - "github.com/netapp/trident/storage_attribute" + sa "github.com/netapp/trident/storage_attribute" drivers "github.com/netapp/trident/storage_drivers" "github.com/netapp/trident/utils" ) @@ -26,8 +26,8 @@ type Driver interface { Initialized() bool // Terminate tells the driver to clean up, as it won't be called again. Terminate() - Create(name string, sizeBytes uint64, opts map[string]string) error - CreateClone(name, source, snapshot string, opts map[string]string) error + Create(volConfig *VolumeConfig, storagePool *Pool, volAttributes map[string]sa.Request) error + CreateClone(volConfig *VolumeConfig) error Destroy(name string) error Publish(name string, publishInfo *utils.VolumePublishInfo) error SnapshotList(name string) ([]Snapshot, error) @@ -42,11 +42,6 @@ type Driver interface { // value of CommonStorageDriver.SnapshotPrefix to the name. GetInternalVolumeName(name string) string GetStorageBackendSpecs(backend *Backend) error - GetVolumeOpts( - volConfig *VolumeConfig, - pool *Pool, - requests map[string]storageattribute.Request, - ) (map[string]string, error) GetProtocol() tridentconfig.Protocol StoreConfig(b *PersistentStorageBackendConfig) // GetExternalConfig returns a version of the driver configuration that @@ -99,26 +94,16 @@ func (b *Backend) GetProtocol() tridentconfig.Protocol { } func (b *Backend) AddVolume( - volConfig *VolumeConfig, - storagePool *Pool, - volumeAttributes map[string]storageattribute.Request, + volConfig *VolumeConfig, storagePool *Pool, volAttributes map[string]sa.Request, ) (*Volume, error) { - // Determine volume size in bytes - requestedSize, err := utils.ConvertSizeToBytes(volConfig.Size) - if err != nil { - return nil, fmt.Errorf("could not convert volume size %s: %v", volConfig.Size, err) - } - volSize, err := strconv.ParseUint(requestedSize, 10, 64) - if err != nil { - return nil, fmt.Errorf("%v is an invalid volume size: %v", volConfig.Size, err) - } + var err error log.WithFields(log.Fields{ "backend": b.Name, "volume": volConfig.InternalName, "storage_pool": storagePool.Name, - "size": volSize, + "size": volConfig.Size, "storage_class": volConfig.StorageClass, }).Debug("Attempting volume create.") @@ -127,17 +112,8 @@ func (b *Backend) AddVolume( // 2. Ensure no volume with the same name exists on that backend if b.Driver.CreatePrepare(volConfig) { - // add volume to the backend - args, err := b.Driver.GetVolumeOpts(volConfig, storagePool, - volumeAttributes) - if err != nil { - // An error on GetVolumeOpts is almost certainly going to indicate - // a formatting mistake, so go ahead and return an error, rather - // than just log a warning. - return nil, err - } - - if err := b.Driver.Create(volConfig.InternalName, volSize, args); err != nil { + // Add volume to the backend + if err = b.Driver.Create(volConfig, storagePool, volAttributes); err != nil { // Implement idempotency at the Trident layer // Ignore the error if the volume exists already if b.Driver.Get(volConfig.InternalName) != nil { @@ -145,7 +121,7 @@ func (b *Backend) AddVolume( } } - if err = b.Driver.CreateFollowup(volConfig); err != nil { + if err := b.Driver.CreateFollowup(volConfig); err != nil { errDestroy := b.Driver.Destroy(volConfig.InternalName) if errDestroy != nil { log.WithFields(log.Fields{ @@ -165,7 +141,7 @@ func (b *Backend) AddVolume( log.WithFields(log.Fields{ "backend": b.Name, "storage_pool": storagePool.Name, - "size": volSize, + "size": volConfig.Size, "storage_class": volConfig.StorageClass, }).Debug("Storage pool does not match volume request.") } @@ -189,18 +165,7 @@ func (b *Backend) CloneVolume(volConfig *VolumeConfig) (*Volume, error) { return nil, errors.New("failed to prepare clone create") } - nilAttributes := make(map[string]storageattribute.Request) - args, err := b.Driver.GetVolumeOpts(volConfig, nil, nilAttributes) - if err != nil { - // An error on GetVolumeOpts is almost certainly going to indicate - // a formatting mistake, so go ahead and return an error, rather - // than just log a warning. - return nil, err - } - - err = b.Driver.CreateClone(volConfig.InternalName, - volConfig.CloneSourceVolumeInternal, volConfig.CloneSourceSnapshot, - args) + err := b.Driver.CreateClone(volConfig) if err != nil { return nil, err } diff --git a/storage/fake/volume.go b/storage/fake/volume.go index 5984217b0..77d97a13e 100644 --- a/storage/fake/volume.go +++ b/storage/fake/volume.go @@ -1,7 +1,8 @@ package fake type Volume struct { - Name string - PoolName string - SizeBytes uint64 + Name string + RequestedPool string + PhysicalPool string + SizeBytes uint64 } diff --git a/storage/storage_pool.go b/storage/storage_pool.go index e1b9f8b4e..84f7baaff 100644 --- a/storage/storage_pool.go +++ b/storage/storage_pool.go @@ -10,19 +10,20 @@ import ( type Pool struct { Name string - // A Trident storage pool can potentially satisfy more than one storage - // class. - StorageClasses []string - Backend *Backend - Attributes map[string]sa.Offer + // A Trident storage pool can potentially satisfy more than one storage class. + StorageClasses []string + Backend *Backend + Attributes map[string]sa.Offer // These attributes are used to match storage classes + InternalAttributes map[string]string // These attributes are defined & used internally by storage drivers } func NewStoragePool(backend *Backend, name string) *Pool { return &Pool{ - Name: name, - StorageClasses: make([]string, 0), - Backend: backend, - Attributes: make(map[string]sa.Offer), + Name: name, + StorageClasses: make([]string, 0), + Backend: backend, + Attributes: make(map[string]sa.Offer), + InternalAttributes: make(map[string]string), } } diff --git a/storage_attribute/bool.go b/storage_attribute/bool.go index fcf4f3c2b..1da5d3af8 100644 --- a/storage_attribute/bool.go +++ b/storage_attribute/bool.go @@ -12,6 +12,30 @@ func NewBoolOffer(offer bool) Offer { } } +func NewBoolOfferFromOffers(offers ...Offer) Offer { + + anyTrueOffer := false + + for _, offer := range offers { + if bOffer, ok := offer.(*boolOffer); ok { + if bOffer.Offer { + anyTrueOffer = true + } + } + } + + // A boolOffer must hold either a true or false value. If any of the + // supplied offers are true, the combined result must be true. Otherwise, + // the supplied offers were all false, in which case the combined result + // must be false. + + if anyTrueOffer { + return &boolOffer{Offer: true} + } else { + return &boolOffer{Offer: false} + } +} + // Matches is a boolean offer of true matches any request; a boolean offer of false // only matches a false request. This assumes that the requested parameter // will be passed into the driver. diff --git a/storage_attribute/common_attributes.go b/storage_attribute/common_attributes.go index 34af73640..23786449a 100644 --- a/storage_attribute/common_attributes.go +++ b/storage_attribute/common_attributes.go @@ -15,6 +15,12 @@ const ( ProvisioningType = "provisioningType" BackendType = "backendType" Media = "media" + Region = "region" + Zone = "zone" + + // Constants for label attributes + Labels = "labels" + Selector = "selector" // Testing constants RecoveryTest = "recoveryTest" @@ -41,6 +47,10 @@ var attrTypes = map[string]Type{ ProvisioningType: stringType, BackendType: stringType, Media: stringType, + Region: stringType, + Zone: stringType, + Labels: labelType, + Selector: labelType, RecoveryTest: boolType, UniqueOptions: stringType, TestingAttribute: boolType, diff --git a/storage_attribute/label.go b/storage_attribute/label.go new file mode 100644 index 000000000..e0bf02b75 --- /dev/null +++ b/storage_attribute/label.go @@ -0,0 +1,363 @@ +// Copyright 2018 NetApp, Inc. All Rights Reserved. + +package storageattribute + +import ( + "fmt" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" +) + +var ( + labelEqualRegex = regexp.MustCompile(`^(?P[\w]+)\s*={1,2}\s*(?P[\w]+)$`) + labelNotEqualRegex = regexp.MustCompile(`^(?P[\w]+)\s*!=\s*(?P[\w]+)$`) + labelInSetRegex = regexp.MustCompile(`^(?P[\w]+)\s+in\s+[(](?P[\s\w,]+)[)]$`) + labelNotInSetRegex = regexp.MustCompile(`^(?P[\w]+)\s+notin\s+[(](?P[\s\w,]+)[)]$`) + labelExistsRegex = regexp.MustCompile(`^(?P[\w]+)$`) + labelNotExistsRegex = regexp.MustCompile(`^!(?P[\w]+)$`) +) + +func NewLabelOffer(labelMaps ...map[string]string) Offer { + + // Combine multiple maps into a single map + offers := make(map[string]string) + + for _, labelMap := range labelMaps { + for k, v := range labelMap { + offers[k] = v + } + } + + log.WithField("offers", offers).Debug("NewLabelOffer") + + return &labelOffer{ + Offers: offers, + } +} + +func (o *labelOffer) Matches(r Request) bool { + + log.WithFields(log.Fields{ + "request": r, + "offers": o.Offers, + }).Debug("Matches") + + // Check that this is a label request + request, ok := r.(*labelRequest) + if !ok { + return false + } + + // Check that each selector finds a match among the offered labels + for _, selector := range request.selectors { + if !selector.Matches(*o) { + return false + } + } + + return true +} + +func (o *labelOffer) String() string { + return fmt.Sprintf("{Offers: %v}", o.Offers) +} + +func NewLabelRequest(request string) (Request, error) { + + log.WithField("request", request).Debug("NewLabelRequest") + + if len(request) == 0 { + return nil, fmt.Errorf("label selector may not be empty") + } + + // Split selector line into individual selectors and parse each according to its type + var selectors []labelSelector + for _, r := range strings.Split(request, ";") { + + r = strings.TrimSpace(r) + + if labelEqualRegex.MatchString(r) { + selectors = append(selectors, newLabelEqualRequest(r)) + } else if labelNotEqualRegex.MatchString(r) { + selectors = append(selectors, newLabelNotEqualRequest(r)) + } else if labelInSetRegex.MatchString(r) { + selectors = append(selectors, newLabelInSetRequest(r)) + } else if labelNotInSetRegex.MatchString(r) { + selectors = append(selectors, newLabelNotInSetRequest(r)) + } else if labelExistsRegex.MatchString(r) { + selectors = append(selectors, newLabelExistsRequest(r)) + } else if labelNotExistsRegex.MatchString(r) { + selectors = append(selectors, newLabelNotExistsRequest(r)) + } else { + return nil, fmt.Errorf("invalid label selector: %s", r) + } + } + + return &labelRequest{ + Request: request, + selectors: selectors, + }, nil +} + +func NewLabelRequestMustCompile(request string) Request { + + r, err := NewLabelRequest(request) + if err != nil { + panic(err) + } + return r +} + +func (r *labelRequest) Value() interface{} { + return r.Request +} + +func (r *labelRequest) GetType() Type { + return labelType +} + +func (r *labelRequest) String() string { + return r.Request +} + +// Common interface for the various types of label requests (==, !=, in, notin, exists) +type labelSelector interface { + Matches(offer labelOffer) bool +} + +///////////////////////////////////////////////////////////////////////////// +// labelSelector for equality (equals) +///////////////////////////////////////////////////////////////////////////// + +type labelEqualRequest struct { + labelName string + labelValue string +} + +func newLabelEqualRequest(request string) labelSelector { + + match := labelEqualRegex.FindStringSubmatch(request) + paramsMap := make(map[string]string) + for i, name := range labelEqualRegex.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + + return &labelEqualRequest{ + labelName: paramsMap["labelName"], + labelValue: paramsMap["labelValue"], + } +} + +func (r *labelEqualRequest) Matches(offer labelOffer) bool { + for labelName, labelValue := range offer.Offers { + if r.labelName == labelName && r.labelValue == labelValue { + return true + } + } + return false +} + +///////////////////////////////////////////////////////////////////////////// +// labelSelector for equality (not equals) +///////////////////////////////////////////////////////////////////////////// + +type labelNotEqualRequest struct { + labelName string + labelValue string +} + +func newLabelNotEqualRequest(request string) labelSelector { + + match := labelNotEqualRegex.FindStringSubmatch(request) + paramsMap := make(map[string]string) + for i, name := range labelNotEqualRegex.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + + return &labelNotEqualRequest{ + labelName: paramsMap["labelName"], + labelValue: paramsMap["labelValue"], + } +} + +func (r *labelNotEqualRequest) Matches(offer labelOffer) bool { + for labelName, labelValue := range offer.Offers { + if r.labelName == labelName && r.labelValue != labelValue { + return true + } + } + return false +} + +///////////////////////////////////////////////////////////////////////////// +// labelSelector for sets (in) +///////////////////////////////////////////////////////////////////////////// + +type labelInSetRequest struct { + labelName string + labelSet []string +} + +func newLabelInSetRequest(request string) labelSelector { + + match := labelInSetRegex.FindStringSubmatch(request) + paramsMap := make(map[string]string) + for i, name := range labelInSetRegex.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + + labelSet := make([]string, 0) + for _, value := range strings.Split(paramsMap["labelSet"], ",") { + value := strings.TrimSpace(value) + if value != "" { + labelSet = append(labelSet, value) + } + } + + return &labelInSetRequest{ + labelName: paramsMap["labelName"], + labelSet: labelSet, + } +} + +func (r *labelInSetRequest) Matches(offer labelOffer) bool { + for labelName, labelValue := range offer.Offers { + if r.labelName == labelName { + // Found match in key + for _, setValue := range r.labelSet { + if setValue == labelValue { + // Found match in values set + return true + } + } + } + } + return false +} + +///////////////////////////////////////////////////////////////////////////// +// labelSelector for sets (notin) +///////////////////////////////////////////////////////////////////////////// + +type labelNotInSetRequest struct { + labelName string + labelSet []string +} + +func newLabelNotInSetRequest(request string) labelSelector { + + match := labelNotInSetRegex.FindStringSubmatch(request) + paramsMap := make(map[string]string) + for i, name := range labelNotInSetRegex.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + + labelSet := make([]string, 0) + for _, value := range strings.Split(paramsMap["labelSet"], ",") { + value := strings.TrimSpace(value) + if value != "" { + labelSet = append(labelSet, value) + } + } + + return &labelNotInSetRequest{ + labelName: paramsMap["labelName"], + labelSet: labelSet, + } +} + +func (r *labelNotInSetRequest) Matches(offer labelOffer) bool { + for labelName, labelValue := range offer.Offers { + if r.labelName == labelName { + // Found match in key + for _, setValue := range r.labelSet { + if setValue == labelValue { + // Found match in set --> no match + return false + } + } + // Found key but no match in set --> match + return true + } + } + // Found no match in key --> match + return true +} + +///////////////////////////////////////////////////////////////////////////// +// labelSelector for sets (exists) +///////////////////////////////////////////////////////////////////////////// + +type labelExistsRequest struct { + labelName string +} + +func newLabelExistsRequest(request string) labelSelector { + + match := labelExistsRegex.FindStringSubmatch(request) + paramsMap := make(map[string]string) + for i, name := range labelExistsRegex.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + + return &labelExistsRequest{ + labelName: paramsMap["labelName"], + } +} + +func (r *labelExistsRequest) Matches(offer labelOffer) bool { + for labelName := range offer.Offers { + if r.labelName == labelName { + // Found match in key --> match + return true + } + } + // Found no match in key --> no match + return false +} + +///////////////////////////////////////////////////////////////////////////// +// labelSelector for sets (not exists) +///////////////////////////////////////////////////////////////////////////// + +type labelNotExistsRequest struct { + labelName string +} + +func newLabelNotExistsRequest(request string) labelSelector { + + match := labelNotExistsRegex.FindStringSubmatch(request) + paramsMap := make(map[string]string) + for i, name := range labelNotExistsRegex.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + + return &labelNotExistsRequest{ + labelName: paramsMap["labelName"], + } +} + +func (r *labelNotExistsRequest) Matches(offer labelOffer) bool { + for labelName := range offer.Offers { + if r.labelName == labelName { + // Found match in key --> no match + return false + } + } + // Found no match in key --> match + return true +} diff --git a/storage_attribute/offer.go b/storage_attribute/offer.go index 2c1469b1e..e227d9bde 100644 --- a/storage_attribute/offer.go +++ b/storage_attribute/offer.go @@ -7,9 +7,7 @@ import ( "fmt" ) -func UnmarshalOfferMap(mapJSON json.RawMessage) ( - map[string]Offer, error, -) { +func UnmarshalOfferMap(mapJSON json.RawMessage) (map[string]Offer, error) { var tmp map[string]json.RawMessage ret := make(map[string]Offer) @@ -21,6 +19,7 @@ func UnmarshalOfferMap(mapJSON json.RawMessage) ( var ( final Offer ) + baseType, ok := attrTypes[name] if !ok { return nil, fmt.Errorf("unknown storage attribute: %s", name) @@ -32,6 +31,8 @@ func UnmarshalOfferMap(mapJSON json.RawMessage) ( final = new(intOffer) case baseType == stringType: final = new(stringOffer) + case baseType == labelType: + final = new(labelOffer) default: return nil, fmt.Errorf("offer %s has unrecognized type %s", name, baseType) @@ -43,5 +44,6 @@ func UnmarshalOfferMap(mapJSON json.RawMessage) ( } ret[name] = final } + return ret, nil } diff --git a/storage_attribute/request.go b/storage_attribute/request.go index 13472cbdf..7f7df7dd2 100644 --- a/storage_attribute/request.go +++ b/storage_attribute/request.go @@ -44,6 +44,8 @@ func MarshalRequestMap(requestMap map[string]Request) ([]byte, error) { func CreateAttributeRequestFromAttributeValue(name, val string) (Request, error) { var req Request + var err error + valType, ok := attrTypes[name] if !ok { return nil, fmt.Errorf("unrecognized storage attribute: %s", name) @@ -63,6 +65,11 @@ func CreateAttributeRequestFromAttributeValue(name, val string) (Request, error) req = NewIntRequest(int(v)) case stringType: req = NewStringRequest(val) + case labelType: + req, err = NewLabelRequest(val) + if err != nil { + return nil, fmt.Errorf("storage attribute value (%s) doesn't match the specified type (%s)", val, valType) + } default: return nil, fmt.Errorf("unrecognized type for a storage attribute request: %s", valType) } diff --git a/storage_attribute/storage_attribute_test.go b/storage_attribute/storage_attribute_test.go index f0cfd41c7..c408ff4d2 100644 --- a/storage_attribute/storage_attribute_test.go +++ b/storage_attribute/storage_attribute_test.go @@ -4,7 +4,6 @@ package storageattribute import ( "encoding/json" - "log" "reflect" "testing" ) @@ -15,7 +14,6 @@ func TestMatches(t *testing.T) { o Offer expected bool }{ - // This is all basically trivial, but it doesn't hurt to test. {NewIntRequest(5), NewIntOffer(0, 10), true}, {NewIntRequest(5), NewIntOffer(6, 10), false}, {NewIntRequest(11), NewIntOffer(6, 10), false}, @@ -23,13 +21,90 @@ func TestMatches(t *testing.T) { {NewBoolRequest(false), NewBoolOffer(true), true}, {NewBoolRequest(true), NewBoolOffer(true), true}, {NewBoolRequest(true), NewBoolOffer(false), false}, - {NewStringRequest("bar"), NewStringOffer("foo", "bar"), - true}, - {NewStringRequest("baz"), NewStringOffer("foo", "bar"), - false}, + {NewStringRequest("bar"), NewStringOffer("foo", "bar"), true}, + {NewStringRequest("baz"), NewStringOffer("foo", "bar"), false}, {NewIntRequest(5), NewStringOffer("foo", "bar"), false}, {NewIntRequest(5), NewBoolOffer(true), false}, {NewBoolRequest(false), NewIntOffer(0, 10), false}, + {NewBoolRequest(false), NewLabelOffer(map[string]string{"performance": "gold"}), false}, + + {NewLabelRequestMustCompile("performance = gold"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("performance=gold;protection=minimal"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("performance=gold;protection=full"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + false, + }, + {NewLabelRequestMustCompile("performance=gold;protection=minimal"), + NewLabelOffer(map[string]string{"performance": "silver", "protection": "minimal"}), + false, + }, + {NewLabelRequestMustCompile("performance=gold;protection=minimal"), + NewLabelOffer(map[string]string{"performance": "silver", "protection": "minimal"}), + false, + }, + {NewLabelRequestMustCompile("protection != full"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("performance = gold;protection != minimal"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + false, + }, + {NewLabelRequestMustCompile("performance in (gold,silver)"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("performance in (silver, bronze)"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + false, + }, + {NewLabelRequestMustCompile("performance in (gold, silver); protection in (minimal, full)"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("performance notin (gold,silver)"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + false, + }, + {NewLabelRequestMustCompile("performance notin (silver, bronze)"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("location notin (east, west)"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("performance"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("performance;protection"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("!performance; !protection"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + false, + }, + {NewLabelRequestMustCompile("protection;!cloud"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + true, + }, + {NewLabelRequestMustCompile("protection;foo"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}), + false, + }, + {NewLabelRequestMustCompile("performance=gold;protection!=full;cloud in (aws, azure);!foo"), + NewLabelOffer(map[string]string{"performance": "gold", "protection": "minimal"}, + map[string]string{"cloud": "aws", "bar": "baz"}), + true, + }, } { if test.o.Matches(test.r) != test.expected { t.Errorf("Test case %d failed", i) @@ -52,15 +127,22 @@ func TestUnmarshalOffer(t *testing.T) { ProvisioningType: &stringOffer{ Offers: []string{"foo", "bar"}, }, + Labels: NewLabelOffer( + map[string]string{"performance": "gold", "protection": "minimal"}, + map[string]string{"cloud": "aws"}, + ), } + data, err := json.Marshal(offerMap) if err != nil { t.Fatal("Unable to marshal: ", err) } + targetOfferMap, err = UnmarshalOfferMap(data) if err != nil { t.Fatal("Unable to unmarshal: ", err) } + if !reflect.DeepEqual(offerMap, targetOfferMap) { t.Errorf("Maps are unequal.\n Expected: %s\nGot: %s\n", offerMap, targetOfferMap) @@ -68,9 +150,9 @@ func TestUnmarshalOffer(t *testing.T) { } func TestUnmarshalRequest(t *testing.T) { - var ( - targetRequestMap map[string]Request - ) + + labelRequest, _ := NewLabelRequest("performance=gold") + requestMap := map[string]Request{ IOPS: &intRequest{ Request: 5, @@ -81,19 +163,20 @@ func TestUnmarshalRequest(t *testing.T) { BackendType: &stringRequest{ Request: "foo", }, + Labels: labelRequest, } - //data, err := json.Marshal(requestMap) + data, err := MarshalRequestMap(requestMap) if err != nil { - t.Fatal("Unable to marshal: ", err) + t.Fatal("Unable to marshal: ", err) } - log.Print("Marshaled data: ", string(data)) - targetRequestMap, err = UnmarshalRequestMap(data) + + targetRequestMap, err := UnmarshalRequestMap(data) if err != nil { t.Fatal("Unable to unmarshal: ", err) } + if !reflect.DeepEqual(requestMap, targetRequestMap) { - t.Errorf("Maps are unequal.\n Expected: %s\nGot: %s\n", requestMap, - targetRequestMap) + t.Errorf("Maps are unequal.\n Expected: %s\nGot: %s\n", requestMap, targetRequestMap) } } diff --git a/storage_attribute/string.go b/storage_attribute/string.go index 830f000c1..d9f9fe830 100644 --- a/storage_attribute/string.go +++ b/storage_attribute/string.go @@ -13,6 +13,30 @@ func NewStringOffer(offers ...string) Offer { } } +func NewStringOfferFromOffers(offers ...Offer) Offer { + + // Use a map as a set to deduplicate the string offers + offerMap := make(map[string]struct{}) + + // For each offer that is a stringOffer, place its contents in the map + for _, offer := range offers { + if sOffer, ok := offer.(*stringOffer); ok { + for _, s := range sOffer.Offers { + offerMap[s] = struct{}{} + } + } + } + + offerKeys := make([]string, 0) + for key := range offerMap { + offerKeys = append(offerKeys, key) + } + + return &stringOffer{ + Offers: offerKeys, + } +} + func (o *stringOffer) Matches(r Request) bool { sr, ok := r.(*stringRequest) if !ok { diff --git a/storage_attribute/types.go b/storage_attribute/types.go index b490467fb..5223a9929 100644 --- a/storage_attribute/types.go +++ b/storage_attribute/types.go @@ -20,6 +20,7 @@ const ( intType Type = "int" boolType Type = "bool" stringType Type = "string" + labelType Type = "label" ) type intOffer struct { @@ -46,3 +47,12 @@ type stringOffer struct { type stringRequest struct { Request string `json:"request"` } + +type labelOffer struct { + Offers map[string]string `json:"offer"` +} + +type labelRequest struct { + Request string `json:"request"` + selectors []labelSelector `json:"-"` +} diff --git a/storage_class/storage_class.go b/storage_class/storage_class.go index 69d865c12..5767f36ea 100644 --- a/storage_class/storage_class.go +++ b/storage_class/storage_class.go @@ -39,6 +39,21 @@ func NewFromPersistent(persistent *Persistent) *StorageClass { return New(persistent.Config) } +func NewFromAttributes(attributes map[string]storageattribute.Request) *StorageClass { + + cfg := &Config{ + Version: "1", + Attributes: attributes, + Pools: make(map[string][]string), + AdditionalPools: make(map[string][]string), + ExcludePools: make(map[string][]string), + } + return &StorageClass{ + config: cfg, + pools: make([]*storage.Pool, 0), + } +} + func (s *StorageClass) regexMatcherImpl(storagePool *storage.Pool, storagePoolBackendName string, storagePoolList []string) bool { if storagePool == nil { return false @@ -155,6 +170,12 @@ func (s *StorageClass) Matches(storagePool *storage.Pool) bool { // storage class, then all must match. attributesMatch := true for name, request := range s.config.Attributes { + + // Remap the "selector" storage class attribute to the "labels" pool attribute + if name == "selector" { + name = "labels" + } + if offer, ok := storagePool.Attributes[name]; !ok || !offer.Matches(request) { log.WithFields(log.Fields{ "offer": offer, @@ -257,6 +278,10 @@ func (s *StorageClass) GetStoragePoolsForProtocol(p config.Protocol) []*storage. return ret } +func (s *StorageClass) Pools() []*storage.Pool { + return s.pools +} + func (s *StorageClass) ConstructExternal() *External { ret := &External{ Config: s.config, diff --git a/storage_drivers/eseries/eseries_iscsi.go b/storage_drivers/eseries/eseries_iscsi.go index 1626f2768..f1fb784a0 100644 --- a/storage_drivers/eseries/eseries_iscsi.go +++ b/storage_drivers/eseries/eseries_iscsi.go @@ -36,14 +36,6 @@ type SANStorageDriver struct { API *api.Client } -type SANStorageDriverConfigExternal struct { - *drivers.CommonStorageDriverConfigExternal - Username string - ControllerA string - ControllerB string - HostDataIP string -} - func (d *SANStorageDriver) Name() string { return drivers.EseriesIscsiStorageDriverName } @@ -272,19 +264,32 @@ func (d *SANStorageDriver) validate() error { // Create is called by Docker to create a container volume. Besides the volume name, a few optional parameters such as size // and disk media type may be provided in the opts map. If more than one pool on the storage controller can satisfy the request, the // one with the most free space is selected. -func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string]string) error { +func (d *SANStorageDriver) Create( + volConfig *storage.VolumeConfig, storagePool *storage.Pool, volAttributes map[string]sa.Request, +) error { + + name := volConfig.InternalName if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ "Method": "Create", "Type": "SANStorageDriver", "name": name, - "opts": opts, + "attrs": volAttributes, } log.WithFields(fields).Debug(">>>> Create") defer log.WithFields(fields).Debug("<<<< Create") } + // Determine volume size in bytes + requestedSize, err := utils.ConvertSizeToBytes(volConfig.Size) + if err != nil { + return fmt.Errorf("could not convert volume size %s: %v", volConfig.Size, err) + } + sizeBytes, err := strconv.ParseUint(requestedSize, 10, 64) + if err != nil { + return fmt.Errorf("%v is an invalid volume size: %v", volConfig.Size, err) + } if sizeBytes == 0 { defaultSize, _ := utils.ConvertSizeToBytes(d.Config.Size) sizeBytes, _ = strconv.ParseUint(defaultSize, 10, 64) @@ -297,6 +302,12 @@ func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string return checkVolumeSizeLimitsError } + // Get options + opts, err := d.GetVolumeOpts(volConfig, storagePool, volAttributes) + if err != nil { + return err + } + // Get media type, or default to "hdd" if not specified mediaType := utils.GetV(opts, "mediaType", "") @@ -617,7 +628,11 @@ func (d *SANStorageDriver) SnapshotList(name string) ([]storage.Snapshot, error) // CreateClone creates a new volume from the named volume, either by direct clone or from the named snapshot. The E-series volume plugin // does not support cloning or snapshots, so this method always returns an error. -func (d *SANStorageDriver) CreateClone(name, source, snapshot string, opts map[string]string) error { +func (d *SANStorageDriver) CreateClone(volConfig *storage.VolumeConfig) error { + + name := volConfig.InternalName + source := volConfig.CloneSourceVolumeInternal + snapshot := volConfig.CloneSourceSnapshot if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ @@ -964,14 +979,14 @@ func (d *SANStorageDriver) GetVolumeExternalWrappers( // Get all volumes volumes, err := d.API.GetVolumes() if err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } // Get all pools pools, err := d.API.GetVolumePools("", 0, "") if err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } @@ -1003,7 +1018,7 @@ func (d *SANStorageDriver) GetVolumeExternalWrappers( continue } - channel <- &storage.VolumeExternalWrapper{d.getVolumeExternal(&volume, &pool), nil} + channel <- &storage.VolumeExternalWrapper{Volume: d.getVolumeExternal(&volume, &pool), Error: nil} } } diff --git a/storage_drivers/fake/fake.go b/storage_drivers/fake/fake.go index bf0fc679e..eea432d10 100644 --- a/storage_drivers/fake/fake.go +++ b/storage_drivers/fake/fake.go @@ -8,7 +8,9 @@ import ( "encoding/json" "errors" "fmt" + "math/rand" "strconv" + "strings" "github.com/RoaringBitmap/roaring" log "github.com/sirupsen/logrus" @@ -17,13 +19,20 @@ import ( "github.com/netapp/trident/storage" "github.com/netapp/trident/storage/fake" sa "github.com/netapp/trident/storage_attribute" + sc "github.com/netapp/trident/storage_class" drivers "github.com/netapp/trident/storage_drivers" "github.com/netapp/trident/utils" ) const ( - FakePoolAttribute = "pool" MinimumVolumeSizeBytes = 1048576 // 1 MiB + + defaultLimitVolumeSize = "" + + // Constants for internal pool attributes + Size = "size" + Region = "region" + Zone = "zone" ) type StorageDriver struct { @@ -38,21 +47,46 @@ type StorageDriver struct { // different driver instances with the same config won't actually share // state. DestroyedVolumes map[string]bool + + physicalPools map[string]*storage.Pool + virtualPools map[string]*storage.Pool +} + +func NewFakeStorageBackend(configJSON string) (sb *storage.Backend, err error) { + + // Parse the common config struct from JSON + commonConfig, err := drivers.ValidateCommonSettings(configJSON) + if err != nil { + err = fmt.Errorf("input failed validation: %v", err) + return nil, err + } + + storageDriver := &StorageDriver{} + + if initializeErr := storageDriver.Initialize( + tridentconfig.CurrentDriverContext, configJSON, commonConfig); initializeErr != nil { + err = fmt.Errorf("problem initializing storage driver '%s': %v", + commonConfig.StorageDriverName, initializeErr) + return nil, err + } + + return storage.NewStorageBackend(storageDriver) } func NewFakeStorageDriver(config drivers.FakeStorageDriverConfig) *StorageDriver { - return &StorageDriver{ + driver := &StorageDriver{ initialized: true, Config: config, Volumes: make(map[string]fake.Volume), DestroyedVolumes: make(map[string]bool), } + driver.populateConfigurationDefaults(&config) + driver.initializeStoragePools() + return driver } -func newFakeStorageDriverConfigJSON( - name string, - protocol tridentconfig.Protocol, - pools map[string]*fake.StoragePool, +func NewFakeStorageDriverConfigJSON( + name string, protocol tridentconfig.Protocol, pools map[string]*fake.StoragePool, ) (string, error) { prefix := "" jsonBytes, err := json.Marshal( @@ -60,7 +94,7 @@ func newFakeStorageDriverConfigJSON( CommonStorageDriverConfig: &drivers.CommonStorageDriverConfig{ Version: 1, StorageDriverName: drivers.FakeStorageDriverName, - StoragePrefixRaw: json.RawMessage("{}"), + StoragePrefixRaw: json.RawMessage("\"\""), StoragePrefix: &prefix, }, Protocol: protocol, @@ -74,18 +108,42 @@ func newFakeStorageDriverConfigJSON( return string(jsonBytes), nil } -func NewFakeStorageDriverConfigJSON( - name string, - protocol tridentconfig.Protocol, - pools map[string]*fake.StoragePool, +func NewFakeStorageDriverConfigJSONWithVirtualPools( + name string, protocol tridentconfig.Protocol, pools map[string]*fake.StoragePool, + vpool drivers.FakeStorageDriverPool, vpools []drivers.FakeStorageDriverPool, ) (string, error) { - return newFakeStorageDriverConfigJSON(name, protocol, pools) + prefix := "" + jsonBytes, err := json.Marshal( + &drivers.FakeStorageDriverConfig{ + CommonStorageDriverConfig: &drivers.CommonStorageDriverConfig{ + Version: 1, + StorageDriverName: drivers.FakeStorageDriverName, + StoragePrefixRaw: json.RawMessage("\"\""), + StoragePrefix: &prefix, + }, + Protocol: protocol, + Pools: pools, + InstanceName: name, + FakeStorageDriverPool: vpool, + Storage: vpools, + }, + ) + if err != nil { + return "", err + } + return string(jsonBytes), nil } func (d *StorageDriver) Name() string { return drivers.FakeStorageDriverName } +// poolName returns the name of the pool reported by this driver instance +func (d *StorageDriver) poolName(region string) string { + name := fmt.Sprintf("%s_%s", d.Name(), strings.Replace(region, "-", "", -1)) + return strings.Replace(name, "__", "_", -1) +} + func (d *StorageDriver) Initialize( context tridentconfig.DriverContext, configJSON string, commonConfig *drivers.CommonStorageDriverConfig, ) error { @@ -100,6 +158,11 @@ func (d *StorageDriver) Initialize( return fmt.Errorf("could not populate configuration defaults: %v", err) } + err = d.initializeStoragePools() + if err != nil { + return fmt.Errorf("could not configure storage pools: %v", err) + } + d.Volumes = make(map[string]fake.Volume) d.DestroyedVolumes = make(map[string]bool) d.Config.SerialNumbers = []string{d.Config.InstanceName + "_SN"} @@ -107,6 +170,11 @@ func (d *StorageDriver) Initialize( s, _ := json.Marshal(d.Config) log.Debugf("FakeStorageDriverConfig: %s", string(s)) + err = d.validate() + if err != nil { + return fmt.Errorf("error validating %s driver. %v", d.Name(), err) + } + d.initialized = true return nil } @@ -128,14 +196,19 @@ func (d *StorageDriver) populateConfigurationDefaults(config *drivers.FakeStorag defer log.WithFields(fields).Debug("<<<< populateConfigurationDefaults") } + if config.StoragePrefix == nil { + prefix := drivers.GetDefaultStoragePrefix(config.DriverContext) + config.StoragePrefix = &prefix + config.StoragePrefixRaw = json.RawMessage("\"" + *config.StoragePrefix + "\"") + } + // Ensure the default volume size is valid, using a "default default" of 1G if not set if config.Size == "" { config.Size = drivers.DefaultVolumeSize - } else { - _, err := utils.ConvertSizeToBytes(config.Size) - if err != nil { - return fmt.Errorf("invalid config value for default volume size: %v", err) - } + } + + if config.LimitVolumeSize == "" { + config.LimitVolumeSize = defaultLimitVolumeSize } log.WithFields(log.Fields{ @@ -145,22 +218,161 @@ func (d *StorageDriver) populateConfigurationDefaults(config *drivers.FakeStorag return nil } -func (d *StorageDriver) Create(name string, sizeBytes uint64, opts map[string]string) error { +func (d *StorageDriver) initializeStoragePools() error { - poolName, ok := opts[FakePoolAttribute] - if !ok { - return fmt.Errorf("no pool specified; expected %s in opts map", FakePoolAttribute) + d.physicalPools = make(map[string]*storage.Pool) + d.virtualPools = make(map[string]*storage.Pool) + + snapshotOffers := make([]sa.Offer, 0) + cloneOffers := make([]sa.Offer, 0) + encryptionOffers := make([]sa.Offer, 0) + provisioningTypeOffers := make([]sa.Offer, 0) + mediaOffers := make([]sa.Offer, 0) + + // Define physical pools + for name, fakeStoragePool := range d.Config.Pools { + + pool := storage.NewStoragePool(nil, name) + + pool.Attributes = fakeStoragePool.Attrs + pool.Attributes[sa.BackendType] = sa.NewStringOffer(d.Name()) + pool.Attributes[sa.Region] = sa.NewStringOffer(d.Config.Region) + if d.Config.Zone != "" { + pool.Attributes[sa.Zone] = sa.NewStringOffer(d.Config.Zone) + } + + if snapshotOffer, ok := pool.Attributes[sa.Snapshots]; ok { + snapshotOffers = append(snapshotOffers, snapshotOffer) + } + if cloneOffer, ok := pool.Attributes[sa.Clones]; ok { + cloneOffers = append(cloneOffers, cloneOffer) + } + if encryptionOffer, ok := pool.Attributes[sa.Encryption]; ok { + encryptionOffers = append(encryptionOffers, encryptionOffer) + } + if provisioningTypeOffer, ok := pool.Attributes[sa.ProvisioningType]; ok { + provisioningTypeOffers = append(provisioningTypeOffers, provisioningTypeOffer) + } + if mediaOffer, ok := pool.Attributes[sa.Media]; ok { + mediaOffers = append(mediaOffers, mediaOffer) + } + + pool.InternalAttributes[Size] = d.Config.Size + pool.InternalAttributes[Region] = d.Config.Region + pool.InternalAttributes[Zone] = d.Config.Zone + + d.physicalPools[pool.Name] = pool } - pool, ok := d.Config.Pools[poolName] - if !ok { - return fmt.Errorf("could not find pool %s", poolName) + // Define virtual pools + for index, vpool := range d.Config.Storage { + + region := d.Config.Region + if vpool.Region != "" { + region = vpool.Region + } + + zone := d.Config.Zone + if vpool.Zone != "" { + zone = vpool.Zone + } + + size := d.Config.Size + if vpool.Size != "" { + size = vpool.Size + } + + pool := storage.NewStoragePool(nil, d.poolName(fmt.Sprintf(region+"_pool_%d", index))) + + pool.Attributes[sa.BackendType] = sa.NewStringOffer(d.Name()) + pool.Attributes[sa.Labels] = sa.NewLabelOffer(d.Config.Labels, vpool.Labels) + pool.Attributes[sa.Region] = sa.NewStringOffer(region) + if zone != "" { + pool.Attributes[sa.Zone] = sa.NewStringOffer(zone) + } + + if len(snapshotOffers) > 0 { + pool.Attributes[sa.Snapshots] = sa.NewBoolOfferFromOffers(snapshotOffers...) + } + if len(cloneOffers) > 0 { + pool.Attributes[sa.Clones] = sa.NewBoolOfferFromOffers(cloneOffers...) + } + if len(encryptionOffers) > 0 { + pool.Attributes[sa.Encryption] = sa.NewBoolOfferFromOffers(encryptionOffers...) + } + if len(provisioningTypeOffers) > 0 { + pool.Attributes[sa.ProvisioningType] = sa.NewStringOfferFromOffers(provisioningTypeOffers...) + } + if len(mediaOffers) > 0 { + pool.Attributes[sa.Media] = sa.NewStringOfferFromOffers(mediaOffers...) + } + + pool.InternalAttributes[Size] = size + pool.InternalAttributes[Region] = region + pool.InternalAttributes[Zone] = zone + + d.virtualPools[pool.Name] = pool + } + + return nil +} + +// validate ensures the driver configuration and execution environment are valid and working +func (d *StorageDriver) validate() error { + + if d.Config.DebugTraceFlags["method"] { + fields := log.Fields{"Method": "validate", "Type": "StorageDriver"} + log.WithFields(fields).Debug(">>>> validate") + defer log.WithFields(fields).Debug("<<<< validate") } - if _, ok = d.Volumes[name]; ok { + // Validate driver-level attributes + + // Validate pool-level attributes + allPools := make([]*storage.Pool, 0, len(d.physicalPools)+len(d.virtualPools)) + + for _, pool := range d.physicalPools { + allPools = append(allPools, pool) + } + for _, pool := range d.virtualPools { + allPools = append(allPools, pool) + } + + for _, pool := range allPools { + + // Validate default size + if _, err := utils.ConvertSizeToBytes(pool.InternalAttributes[Size]); err != nil { + return fmt.Errorf("invalid value for default volume size in pool %s: %v", pool.Name, err) + } + } + + return nil +} + +func (d *StorageDriver) Create( + volConfig *storage.VolumeConfig, storagePool *storage.Pool, volAttributes map[string]sa.Request, +) error { + + name := volConfig.InternalName + if _, ok := d.Volumes[name]; ok { return fmt.Errorf("volume %s already exists", name) } + // Get candidate physical pools + physicalPools, err := d.getPoolsForCreate(volConfig, storagePool, volAttributes) + if err != nil { + return err + } + + // Determine volume size in bytes + requestedSize, err := utils.ConvertSizeToBytes(volConfig.Size) + if err != nil { + return fmt.Errorf("could not convert volume size %s: %v", volConfig.Size, err) + } + sizeBytes, err := strconv.ParseUint(requestedSize, 10, 64) + if err != nil { + return fmt.Errorf("%v is an invalid volume size: %v", volConfig.Size, err) + } if sizeBytes == 0 { defaultSize, _ := utils.ConvertSizeToBytes(d.Config.Size) sizeBytes, _ = strconv.ParseUint(defaultSize, 10, 64) @@ -170,30 +382,104 @@ func (d *StorageDriver) Create(name string, sizeBytes uint64, opts map[string]st sizeBytes, MinimumVolumeSizeBytes) } - if sizeBytes > pool.Bytes { - return fmt.Errorf("requested volume is too large; requested %d bytes; have %d available in pool %s", - sizeBytes, pool.Bytes, poolName) + if _, _, err = drivers.CheckVolumeSizeLimits(sizeBytes, d.Config.CommonStorageDriverConfig); err != nil { + return err } - d.Volumes[name] = fake.Volume{ - Name: name, - PoolName: poolName, - SizeBytes: sizeBytes, + createErrors := make([]error, 0) + + for _, physicalPool := range physicalPools { + + fakePoolName := physicalPool.Name + fakePool, ok := d.Config.Pools[physicalPool.Name] + if !ok { + errMessage := fmt.Sprintf("fake pool %s not found.", fakePoolName) + log.Error(errMessage) + createErrors = append(createErrors, errors.New(errMessage)) + continue + } + + if sizeBytes > fakePool.Bytes { + errMessage := fmt.Sprintf("requested volume is too large, requested %d bytes, "+ + "have %d available in pool %s", sizeBytes, fakePool.Bytes, fakePoolName) + log.Error(errMessage) + createErrors = append(createErrors, errors.New(errMessage)) + continue + } + + d.Volumes[name] = fake.Volume{ + Name: name, + RequestedPool: storagePool.Name, + PhysicalPool: fakePoolName, + SizeBytes: sizeBytes, + } + d.DestroyedVolumes[name] = false + fakePool.Bytes -= sizeBytes + + log.WithFields(log.Fields{ + "backend": d.Config.InstanceName, + "name": name, + "requestedPool": storagePool.Name, + "physicalPool": fakePoolName, + "sizeBytes": sizeBytes, + }).Debug("Created fake volume.") + + return nil } - d.DestroyedVolumes[name] = false - pool.Bytes -= sizeBytes - log.WithFields(log.Fields{ - "backend": d.Config.InstanceName, - "Name": name, - "PoolName": poolName, - "SizeBytes": sizeBytes, - }).Debug("Created fake volume.") + // All physical pools that were eligible ultimately failed, so don't try this backend again + return drivers.NewBackendIneligibleError(name, createErrors) +} - return nil +func (d *StorageDriver) getPoolsForCreate( + volConfig *storage.VolumeConfig, storagePool *storage.Pool, volAttributes map[string]sa.Request, +) ([]*storage.Pool, error) { + + // If a physical pool was requested, just use it + if _, ok := d.physicalPools[storagePool.Name]; ok { + return []*storage.Pool{storagePool}, nil + } + + // If a virtual pool was requested, find a physical pool to satisfy it + if _, ok := d.virtualPools[storagePool.Name]; !ok { + return nil, fmt.Errorf("could not find pool %s", storagePool.Name) + } + + // Make a storage class from the volume attributes to simplify pool matching + attributesCopy := make(map[string]sa.Request) + for k, v := range volAttributes { + attributesCopy[k] = v + } + delete(attributesCopy, sa.Selector) + storageClass := sc.NewFromAttributes(attributesCopy) + + // Find matching pools + candidatePools := make([]*storage.Pool, 0) + + for _, pool := range d.physicalPools { + if storageClass.Matches(pool) { + candidatePools = append(candidatePools, pool) + } + } + + if len(candidatePools) == 0 { + err := errors.New("backend has no physical pools that can satisfy request") + return nil, drivers.NewBackendIneligibleError(volConfig.InternalName, []error{err}) + } + + // Shuffle physical pools + rand.Shuffle(len(candidatePools), func(i, j int) { + candidatePools[i], candidatePools[j] = candidatePools[j], candidatePools[i] + }) + + return candidatePools, nil } -func (d *StorageDriver) CreateClone(name, source, snapshot string, opts map[string]string) error { +func (d *StorageDriver) CreateClone(volConfig *storage.VolumeConfig) error { + + name := volConfig.InternalName + source := volConfig.CloneSourceVolumeInternal + snapshot := volConfig.CloneSourceSnapshot // Ensure source volume exists sourceVolume, ok := d.Volumes[source] @@ -207,34 +493,36 @@ func (d *StorageDriver) CreateClone(name, source, snapshot string, opts map[stri } // Use the same pool as the source - poolName := sourceVolume.PoolName - pool, ok := d.Config.Pools[poolName] + physicalPool := sourceVolume.PhysicalPool + fakePool, ok := d.Config.Pools[physicalPool] if !ok { - return fmt.Errorf("could not find pool %s", poolName) + return fmt.Errorf("could not find pool %s", physicalPool) } // Use the same size as the source sizeBytes := sourceVolume.SizeBytes - if sizeBytes > pool.Bytes { + if sizeBytes > fakePool.Bytes { return fmt.Errorf("requested clone is too large: requested %d bytes; have %d available in pool %s", - sizeBytes, pool.Bytes, poolName) + sizeBytes, fakePool.Bytes, physicalPool) } d.Volumes[name] = fake.Volume{ - Name: name, - PoolName: poolName, - SizeBytes: sizeBytes, + Name: name, + RequestedPool: sourceVolume.RequestedPool, + PhysicalPool: physicalPool, + SizeBytes: sizeBytes, } d.DestroyedVolumes[name] = false - pool.Bytes -= sizeBytes + fakePool.Bytes -= sizeBytes log.WithFields(log.Fields{ - "backend": d.Config.InstanceName, - "Name": name, - "source": sourceVolume.Name, - "snapshot": snapshot, - "PoolName": poolName, - "SizeBytes": sizeBytes, + "backend": d.Config.InstanceName, + "Name": name, + "source": sourceVolume.Name, + "snapshot": snapshot, + "requestedPool": sourceVolume.RequestedPool, + "physicalPool": physicalPool, + "SizeBytes": sizeBytes, }).Debug("Cloned fake volume.") return nil @@ -249,19 +537,21 @@ func (d *StorageDriver) Destroy(name string) error { return nil } - pool, ok := d.Config.Pools[volume.PoolName] + physicalPool := volume.PhysicalPool + fakePool, ok := d.Config.Pools[physicalPool] if !ok { - return fmt.Errorf("could not find pool %s", volume.PoolName) + return fmt.Errorf("could not find pool %s", physicalPool) } - pool.Bytes += volume.SizeBytes + fakePool.Bytes += volume.SizeBytes delete(d.Volumes, name) log.WithFields(log.Fields{ - "backend": d.Config.InstanceName, - "Name": name, - "PoolName": volume.PoolName, - "SizeBytes": volume.SizeBytes, + "backend": d.Config.InstanceName, + "Name": name, + "requestedPool": volume.RequestedPool, + "physicalPool": physicalPool, + "sizeBytes": volume.SizeBytes, }).Debug("Deleted fake volume.") return nil @@ -285,7 +575,26 @@ func (d *StorageDriver) Get(name string) error { return nil } +// Resize expands the volume size. +func (d *StorageDriver) Resize(name string, sizeBytes uint64) error { + vol := d.Volumes[name] + + if vol.SizeBytes == sizeBytes { + return nil + } + + if sizeBytes < vol.SizeBytes { + return fmt.Errorf("requested size %d is less than existing volume size %d", sizeBytes, vol.SizeBytes) + } else { + vol.SizeBytes = sizeBytes + d.Volumes[name] = vol + } + + return nil +} + func (d *StorageDriver) GetStorageBackendSpecs(backend *storage.Backend) error { + if d.Config.BackendName == "" { // Use the old naming scheme if no backend is specified backend.Name = d.Config.InstanceName @@ -293,40 +602,43 @@ func (d *StorageDriver) GetStorageBackendSpecs(backend *storage.Backend) error { backend.Name = d.Config.BackendName } - for name, pool := range d.Config.Pools { - vc := &storage.Pool{ - Name: name, - StorageClasses: make([]string, 0), - Backend: backend, - Attributes: pool.Attrs, + virtual := len(d.virtualPools) > 0 + + for _, pool := range d.physicalPools { + pool.Backend = backend + if !virtual { + backend.AddStoragePool(pool) } - vc.Attributes[sa.BackendType] = sa.NewStringOffer(d.Name()) - backend.AddStoragePool(vc) } - return nil -} -func (d *StorageDriver) GetVolumeOpts( - volConfig *storage.VolumeConfig, - pool *storage.Pool, - requests map[string]sa.Request, -) (map[string]string, error) { - opts := make(map[string]string) - if pool != nil { - opts[FakePoolAttribute] = pool.Name + for _, pool := range d.virtualPools { + pool.Backend = backend + if virtual { + backend.AddStoragePool(pool) + } } - return opts, nil + + return nil } func (d *StorageDriver) GetInternalVolumeName(name string) string { - return drivers.GetCommonInternalVolumeName(d.Config.CommonStorageDriverConfig, name) + if tridentconfig.UsingPassthroughStore { + // With a passthrough store, the name mapping must remain reversible + return *d.Config.StoragePrefix + name + } else { + // With an external store, any transformation of the name is fine + internal := drivers.GetCommonInternalVolumeName(d.Config.CommonStorageDriverConfig, name) + internal = strings.Replace(internal, "--", "-", -1) // Remove any double hyphens + internal = strings.Replace(internal, "__", "_", -1) // Remove any double underscores + internal = strings.Replace(internal, "_-", "-", -1) // Remove any strange delimiter + return internal + } } func (d *StorageDriver) CreatePrepare(volConfig *storage.VolumeConfig) bool { volConfig.InternalName = d.GetInternalVolumeName(volConfig.Name) if volConfig.CloneSourceVolume != "" { - volConfig.CloneSourceVolumeInternal = - d.GetInternalVolumeName(volConfig.CloneSourceVolume) + volConfig.CloneSourceVolumeInternal = d.GetInternalVolumeName(volConfig.CloneSourceVolume) } return true } @@ -358,30 +670,32 @@ func (d *StorageDriver) StoreConfig(b *storage.PersistentStorageBackendConfig) { var cloneCommonConfig drivers.CommonStorageDriverConfig Clone(d.Config.CommonStorageDriverConfig, &cloneCommonConfig) cloneCommonConfig.SerialNumbers = nil + if cloneCommonConfig.StoragePrefix == nil { + cloneCommonConfig.StoragePrefixRaw = json.RawMessage("{}") + } else { + cloneCommonConfig.StoragePrefixRaw = json.RawMessage("\"" + *cloneCommonConfig.StoragePrefix + "\"") + } + cloneCommonConfig.StoragePrefix = nil + + var cloneFakePool drivers.FakeStorageDriverPool + Clone(&d.Config.FakeStorageDriverPool, &cloneFakePool) + + var cloneFakePools []drivers.FakeStorageDriverPool + Clone(&d.Config.Storage, &cloneFakePools) b.FakeStorageDriverConfig = &drivers.FakeStorageDriverConfig{ CommonStorageDriverConfig: &cloneCommonConfig, Protocol: d.Config.Protocol, Pools: d.Config.Pools, InstanceName: d.Config.InstanceName, + Storage: cloneFakePools, + FakeStorageDriverPool: cloneFakePool, } } func (d *StorageDriver) GetExternalConfig() interface{} { drivers.SanitizeCommonStorageDriverConfig(d.Config.CommonStorageDriverConfig) - - return &struct { - *drivers.CommonStorageDriverConfigExternal - Protocol tridentconfig.Protocol `json:"protocol"` - Pools map[string]*fake.StoragePool `json:"pools"` - InstanceName string - }{ - drivers.GetCommonStorageDriverConfigExternal( - d.Config.CommonStorageDriverConfig), - d.Config.Protocol, - d.Config.Pools, - d.Config.InstanceName, - } + return d.Config } func (d *StorageDriver) GetVolumeExternal(name string) (*storage.VolumeExternal, error) { @@ -402,31 +716,41 @@ func (d *StorageDriver) GetVolumeExternalWrappers( // Convert all volumes to VolumeExternal and write them to the channel for _, volume := range d.Volumes { - channel <- &storage.VolumeExternalWrapper{d.getVolumeExternal(volume), nil} + channel <- &storage.VolumeExternalWrapper{Volume: d.getVolumeExternal(volume), Error: nil} } } func (d *StorageDriver) getVolumeExternal(volume fake.Volume) *storage.VolumeExternal { + internalName := volume.Name + name := internalName[len(*d.Config.StoragePrefix):] + volumeConfig := &storage.VolumeConfig{ Version: tridentconfig.OrchestratorAPIVersion, - Name: volume.Name, - InternalName: volume.Name, + Name: name, + InternalName: internalName, Size: strconv.FormatUint(volume.SizeBytes, 10), } volumeExternal := &storage.VolumeExternal{ Config: volumeConfig, Backend: d.Name(), - Pool: volume.PoolName, + Pool: drivers.UnsetPool, } return volumeExternal } // GetUpdateType returns a bitmap populated with updates to the driver -func (d *StorageDriver) GetUpdateType(dOrig storage.Driver) *roaring.Bitmap { - //TODO +func (d *StorageDriver) GetUpdateType(driverOrig storage.Driver) *roaring.Bitmap { + + bitmap := roaring.New() + _, ok := driverOrig.(*StorageDriver) + if !ok { + bitmap.Add(storage.InvalidUpdate) + return bitmap + } + return roaring.New() } @@ -437,21 +761,3 @@ func Clone(a, b interface{}) { enc.Encode(a) dec.Decode(b) } - -// Resize expands the volume size. -func (d *StorageDriver) Resize(name string, sizeBytes uint64) error { - vol := d.Volumes[name] - - if vol.SizeBytes == sizeBytes { - return nil - } - - if sizeBytes < vol.SizeBytes { - return fmt.Errorf("requested size %d is less than existing volume size %d", sizeBytes, vol.SizeBytes) - } else { - vol.SizeBytes = sizeBytes - d.Volumes[name] = vol - } - - return nil -} diff --git a/storage_class/storage_class_test.go b/storage_drivers/fake/storage_class_test.go similarity index 78% rename from storage_class/storage_class_test.go rename to storage_drivers/fake/storage_class_test.go index 2c0829cb2..28bebaa03 100644 --- a/storage_class/storage_class_test.go +++ b/storage_drivers/fake/storage_class_test.go @@ -1,6 +1,6 @@ // Copyright 2018 NetApp, Inc. All Rights Reserved. -package storageclass +package fake import ( "strings" @@ -8,32 +8,30 @@ import ( "github.com/netapp/trident/config" "github.com/netapp/trident/storage" - "github.com/netapp/trident/storage/factory" "github.com/netapp/trident/storage/fake" sa "github.com/netapp/trident/storage_attribute" - tu "github.com/netapp/trident/storage_class/test_utils" - fake_driver "github.com/netapp/trident/storage_drivers/fake" + sc "github.com/netapp/trident/storage_class" + tu "github.com/netapp/trident/storage_drivers/fake/test_utils" ) func TestAttributeMatches(t *testing.T) { mockPools := tu.GetFakePools() - config, err := fake_driver.NewFakeStorageDriverConfigJSON("mock", config.File, - mockPools) + fakeConfig, err := NewFakeStorageDriverConfigJSON("mock", config.File, mockPools) if err != nil { t.Fatalf("Unable to construct config JSON.") } - backend, err := factory.NewStorageBackendForConfig(config) + backend, err := NewFakeStorageBackend(fakeConfig) if err != nil { t.Fatalf("Unable to construct backend using mock driver.") } for _, test := range []struct { name string - sc *StorageClass + sc *sc.StorageClass expectedPools []string }{ { name: "Slow", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "slow", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(40), @@ -47,7 +45,7 @@ func TestAttributeMatches(t *testing.T) { // Tests that a request for false will return backends offering // both true and false name: "Bool", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "no-snapshots", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(40), @@ -59,7 +57,7 @@ func TestAttributeMatches(t *testing.T) { }, { name: "Fast", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "fast", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(2000), @@ -74,7 +72,7 @@ func TestAttributeMatches(t *testing.T) { // This tests the correctness of matching string requests, using // ProvisioningType name: "String", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "string", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(2000), @@ -88,7 +86,7 @@ func TestAttributeMatches(t *testing.T) { // This uses sa.UniqueOptions to test that StorageClass only // matches backends that have all required attributes. name: "Unique", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "unique", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(2000), @@ -102,7 +100,7 @@ func TestAttributeMatches(t *testing.T) { { // Tests overlapping int ranges name: "Overlap", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "overlap", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(1000), @@ -117,7 +115,7 @@ func TestAttributeMatches(t *testing.T) { { // Tests non-existent bool attribute name: "Bool failure", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "bool-failure", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(1000), @@ -130,7 +128,7 @@ func TestAttributeMatches(t *testing.T) { }, { name: "Invalid string value", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "invalid-string", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(1000), @@ -142,7 +140,7 @@ func TestAttributeMatches(t *testing.T) { }, { name: "Invalid int range", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "invalid-int", Attributes: map[string]sa.Request{ sa.IOPS: sa.NewIntRequest(300), @@ -160,7 +158,126 @@ func TestAttributeMatches(t *testing.T) { } unmatched := make([]string, 0) matched := make([]string, 0) - for _, vc := range test.sc.pools { + for _, vc := range test.sc.Pools() { + if _, ok := expectedMap[vc.Name]; !ok { + unmatched = append(unmatched, vc.Name) + } else { + delete(expectedMap, vc.Name) + matched = append(matched, vc.Name) + } + } + if len(unmatched) > 0 { + t.Errorf("%s:\n\tFailed to match pools: %s\n\tMatched: %s", + test.name, + strings.Join(unmatched, ", "), + strings.Join(matched, ", "), + ) + } + if len(expectedMap) > 0 { + expectedMatches := make([]string, 0) + for k := range expectedMap { + expectedMatches = append(expectedMatches, k) + } + t.Errorf("%s:\n\tExpected additional matches: %s\n\tMatched: %s", + test.name, + strings.Join(expectedMatches, ", "), + strings.Join(matched, ", "), + ) + } + } +} + +func TestAttributeMatchesWithVirtualPools(t *testing.T) { + mockPools := tu.GetFakePools() + vpool, vpools := tu.GetFakeVirtualPools() + fakeConfig, err := NewFakeStorageDriverConfigJSONWithVirtualPools("mock", config.File, mockPools, vpool, vpools) + if err != nil { + t.Fatalf("Unable to construct config JSON.") + } + backend, err := NewFakeStorageBackend(fakeConfig) + if err != nil { + t.Fatalf("Unable to construct backend using mock driver.") + } + for _, test := range []struct { + name string + sc *sc.StorageClass + expectedPools []string + }{ + { + name: "Gold", + sc: sc.New(&sc.Config{ + Name: "gold", + Attributes: map[string]sa.Request{ + sa.Selector: sa.NewLabelRequestMustCompile("performance=gold"), + }, + }), + expectedPools: []string{"fake_useast_pool_0", "fake_useast_pool_3"}, + }, + { + name: "Gold-Zone1", + sc: sc.New(&sc.Config{ + Name: "Gold-Zone1", + Attributes: map[string]sa.Request{ + sa.Zone: sa.NewStringRequest("1"), + sa.Selector: sa.NewLabelRequestMustCompile("performance=gold"), + }, + }), + expectedPools: []string{"fake_useast_pool_0"}, + }, + { + name: "Gold-Zone2-AWS", + sc: sc.New(&sc.Config{ + Name: "Gold-Zone2-AWS", + Attributes: map[string]sa.Request{ + sa.Zone: sa.NewStringRequest("2"), + sa.Selector: sa.NewLabelRequestMustCompile("performance=gold; cloud in (aws,azure)"), + }, + }), + expectedPools: []string{"fake_useast_pool_3"}, + }, + { + name: "Silver-Zone2-NotAWS", + sc: sc.New(&sc.Config{ + Name: "Silver-Zone2-NotAWS", + Attributes: map[string]sa.Request{ + sa.Zone: sa.NewStringRequest("2"), + sa.Selector: sa.NewLabelRequestMustCompile("performance=silver; cloud notin (aws)"), + }, + }), + expectedPools: []string{}, + }, + { + name: "Silver-USEast-Zone2-AnyCloud", + sc: sc.New(&sc.Config{ + Name: "Silver-USEast-Zone2-AnyCloud", + Attributes: map[string]sa.Request{ + sa.Zone: sa.NewStringRequest("2"), + sa.Region: sa.NewStringRequest("us-east"), + sa.Selector: sa.NewLabelRequestMustCompile("performance=silver; cloud"), + }, + }), + expectedPools: []string{"fake_useast_pool_4"}, + }, + { + name: "Silver-Zone2-NoCloud", + sc: sc.New(&sc.Config{ + Name: "Silver-Zone2-NoCloud", + Attributes: map[string]sa.Request{ + sa.Zone: sa.NewStringRequest("2"), + sa.Selector: sa.NewLabelRequestMustCompile("performance=bronze; !cloud"), + }, + }), + expectedPools: []string{}, + }, + } { + test.sc.CheckAndAddBackend(backend) + expectedMap := make(map[string]bool, len(test.expectedPools)) + for _, pool := range test.expectedPools { + expectedMap[pool] = true + } + unmatched := make([]string, 0) + matched := make([]string, 0) + for _, vc := range test.sc.Pools() { if _, ok := expectedMap[vc.Name]; !ok { unmatched = append(unmatched, vc.Name) } else { @@ -207,12 +324,11 @@ func TestSpecificBackends(t *testing.T) { for _, poolName := range c.poolNames { pools[poolName] = mockPools[poolName] } - config, err := fake_driver.NewFakeStorageDriverConfigJSON(c.name, config.File, - pools) + fakeConfig, err := NewFakeStorageDriverConfigJSON(c.name, config.File, pools) if err != nil { t.Fatalf("Unable to generate config JSON for %s: %v", c.name, err) } - backend, err := factory.NewStorageBackendForConfig(config) + backend, err := NewFakeStorageBackend(fakeConfig) if err != nil { t.Fatalf("Unable to construct backend using mock driver.") } @@ -220,12 +336,12 @@ func TestSpecificBackends(t *testing.T) { } for _, test := range []struct { name string - sc *StorageClass + sc *sc.StorageClass expected []*tu.PoolMatch }{ { name: "Specific backends", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "specific", AdditionalPools: map[string][]string{ "fast-a": {tu.FastThinOnly}, @@ -240,7 +356,7 @@ func TestSpecificBackends(t *testing.T) { }, { name: "Mixed attributes, backends", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "mixed", AdditionalPools: map[string][]string{ "slow": {tu.SlowNoSnapshots}, @@ -325,11 +441,11 @@ func TestRegex(t *testing.T) { for _, poolName := range c.poolNames { pools[poolName] = mockPools[poolName] } - config, err := fake_driver.NewFakeStorageDriverConfigJSON(c.backendName, config.File, pools) + fakeConfig, err := NewFakeStorageDriverConfigJSON(c.backendName, config.File, pools) if err != nil { t.Fatalf("Unable to generate config JSON for %s: %v", c.backendName, err) } - backend, err := factory.NewStorageBackendForConfig(config) + backend, err := NewFakeStorageBackend(fakeConfig) if err != nil { t.Fatalf("Unable to construct backend using mock driver.") } @@ -337,12 +453,12 @@ func TestRegex(t *testing.T) { } for _, test := range []struct { description string - sc *StorageClass + sc *sc.StorageClass expected []*tu.PoolMatch }{ { description: "All pools for the 'slow' backend via regex '.*''", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex1", Pools: map[string][]string{ "slow": {".*"}, @@ -356,7 +472,7 @@ func TestRegex(t *testing.T) { }, { description: "TBD", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex1b", Pools: map[string][]string{ "slow": {"slow.*", tu.MediumOverlap}, @@ -370,7 +486,7 @@ func TestRegex(t *testing.T) { }, { description: "Implicitly exclude the medium-overlap pool via regex by only matching those starting with 'slow-'", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex2", Pools: map[string][]string{ "slow": {"slow-.*"}, @@ -383,7 +499,7 @@ func TestRegex(t *testing.T) { }, { description: "Backends whose names start with 'fast-' and all of their respective pools", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex3", Pools: map[string][]string{ "fast-.*": {".*"}, // All fast backends: all their pools @@ -398,7 +514,7 @@ func TestRegex(t *testing.T) { }, { description: "Backends {fast-a, slow} and all of their respective pools", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex4", Pools: map[string][]string{ "fast-a|slow": {".*"}, // fast-a or slow: all their pools @@ -414,7 +530,7 @@ func TestRegex(t *testing.T) { }, { description: "Backends {fast-a, slow} and all of their respective pools", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex5", Pools: map[string][]string{ "(fast-a|slow|NOT_ACTUALLY_THERE)": {".*"}, // fast-a or slow or 1 that doesn't exist: all their pools @@ -430,7 +546,7 @@ func TestRegex(t *testing.T) { }, { description: "Every backend and every pool", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex6", Pools: map[string][]string{ ".*": {".*"}, // All backends: all pools @@ -448,7 +564,7 @@ func TestRegex(t *testing.T) { }, { description: "Exclusion test", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex7", Pools: map[string][]string{ ".*": {".*"}, // Start off allowing all backends: all pools @@ -468,7 +584,7 @@ func TestRegex(t *testing.T) { }, { description: "Exclude the FastThinOnly pools from the fast backends", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex8", Pools: map[string][]string{ "fast-.*": {".*"}, // All fast backends: all their pools @@ -548,11 +664,11 @@ func TestRegex2(t *testing.T) { for _, poolName := range c.poolNames { pools[poolName] = mockPools[poolName] } - config, err := fake_driver.NewFakeStorageDriverConfigJSON(c.backendName, config.File, pools) + fakeConfig, err := NewFakeStorageDriverConfigJSON(c.backendName, config.File, pools) if err != nil { t.Fatalf("Unable to generate config JSON for %s: %v", c.backendName, err) } - backend, err := factory.NewStorageBackendForConfig(config) + backend, err := NewFakeStorageBackend(fakeConfig) if err != nil { t.Fatalf("Unable to construct backend using mock driver.") } @@ -560,12 +676,12 @@ func TestRegex2(t *testing.T) { } for _, test := range []struct { description string - sc *StorageClass + sc *sc.StorageClass expected []*tu.PoolMatch }{ { description: "slow only, not slow+slower", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex1", Pools: map[string][]string{ "slow": {".*"}, @@ -579,7 +695,7 @@ func TestRegex2(t *testing.T) { }, { description: "slow only, not slow+slower", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex2", Pools: map[string][]string{ "^slow$": {".*"}, @@ -593,7 +709,7 @@ func TestRegex2(t *testing.T) { }, { description: "slow and slower combined", - sc: New(&Config{ + sc: sc.New(&sc.Config{ Name: "regex2", Pools: map[string][]string{ "slow.*": {".*"}, diff --git a/storage_class/test_utils/utils.go b/storage_drivers/fake/test_utils/utils.go similarity index 55% rename from storage_class/test_utils/utils.go rename to storage_drivers/fake/test_utils/utils.go index 4213b74af..3b87242bd 100644 --- a/storage_class/test_utils/utils.go +++ b/storage_drivers/fake/test_utils/utils.go @@ -8,6 +8,7 @@ import ( "github.com/netapp/trident/storage" "github.com/netapp/trident/storage/fake" sa "github.com/netapp/trident/storage_attribute" + drivers "github.com/netapp/trident/storage_drivers" ) const ( @@ -32,6 +33,35 @@ func (p *PoolMatch) String() string { return fmt.Sprintf("%s:%s", p.Backend, p.Pool) } +func getFakeVirtualPool(size, region, zone string, labels map[string]string) drivers.FakeStorageDriverPool { + + commonConfigDefaults := drivers.CommonStorageDriverConfigDefaults{Size: size} + fakeConfigDefaults := drivers.FakeStorageDriverConfigDefaults{ + CommonStorageDriverConfigDefaults: commonConfigDefaults, + } + + return drivers.FakeStorageDriverPool{ + Labels: labels, + Region: region, + Zone: zone, + FakeStorageDriverConfigDefaults: fakeConfigDefaults, + } +} + +func GetFakeVirtualPools() (drivers.FakeStorageDriverPool, []drivers.FakeStorageDriverPool) { + + pool := getFakeVirtualPool("10G", "us-east", "", map[string]string{"cloud": "aws"}) + + return pool, []drivers.FakeStorageDriverPool{ + getFakeVirtualPool("1G", "", "1", map[string]string{"performance": "gold", "cost": "3"}), + getFakeVirtualPool("", "", "1", map[string]string{"performance": "silver", "cost": "2"}), + getFakeVirtualPool("", "", "1", map[string]string{"performance": "bronze", "cost": "1"}), + getFakeVirtualPool("1G", "", "2", map[string]string{"performance": "gold", "cost": "3"}), + getFakeVirtualPool("", "", "2", map[string]string{"performance": "silver", "cost": "2"}), + getFakeVirtualPool("", "", "2", map[string]string{"performance": "bronze", "cost": "1"}), + } +} + func GetFakePools() map[string]*fake.StoragePool { return map[string]*fake.StoragePool{ SlowNoSnapshots: { @@ -40,6 +70,7 @@ func GetFakePools() map[string]*fake.StoragePool { sa.IOPS: sa.NewIntOffer(0, 100), sa.Snapshots: sa.NewBoolOffer(false), sa.ProvisioningType: sa.NewStringOffer("thick", "thin"), + sa.Labels: sa.NewLabelOffer(map[string]string{"cloud": "aws", "performance": "bronze"}), }, }, SlowSnapshots: { @@ -48,6 +79,7 @@ func GetFakePools() map[string]*fake.StoragePool { sa.IOPS: sa.NewIntOffer(0, 100), sa.Snapshots: sa.NewBoolOffer(true), sa.ProvisioningType: sa.NewStringOffer("thick", "thin"), + sa.Labels: sa.NewLabelOffer(map[string]string{"cloud": "aws", "performance": "bronze"}), }, }, FastSmall: { @@ -56,6 +88,7 @@ func GetFakePools() map[string]*fake.StoragePool { sa.IOPS: sa.NewIntOffer(1000, 10000), sa.Snapshots: sa.NewBoolOffer(true), sa.ProvisioningType: sa.NewStringOffer("thick", "thin"), + sa.Labels: sa.NewLabelOffer(map[string]string{"cloud": "aws", "performance": "gold"}), }, }, FastThinOnly: { @@ -64,6 +97,7 @@ func GetFakePools() map[string]*fake.StoragePool { sa.IOPS: sa.NewIntOffer(1000, 10000), sa.Snapshots: sa.NewBoolOffer(true), sa.ProvisioningType: sa.NewStringOffer("thin"), + sa.Labels: sa.NewLabelOffer(map[string]string{"cloud": "azure", "performance": "gold"}), }, }, FastUniqueAttr: { @@ -72,6 +106,7 @@ func GetFakePools() map[string]*fake.StoragePool { sa.IOPS: sa.NewIntOffer(1000, 10000), sa.Snapshots: sa.NewBoolOffer(true), sa.ProvisioningType: sa.NewStringOffer("thin", "thick"), + sa.Labels: sa.NewLabelOffer(map[string]string{"cloud": "azure", "performance": "gold"}), "uniqueOptions": sa.NewStringOffer("foo", "bar", "baz"), }, }, @@ -81,6 +116,7 @@ func GetFakePools() map[string]*fake.StoragePool { sa.IOPS: sa.NewIntOffer(500, 1000), sa.Snapshots: sa.NewBoolOffer(true), sa.ProvisioningType: sa.NewStringOffer("thin"), + sa.Labels: sa.NewLabelOffer(map[string]string{"cloud": "azure", "performance": "silver"}), }, }, } diff --git a/storage_drivers/ontap/ontap_nas.go b/storage_drivers/ontap/ontap_nas.go index 09f498e27..1deb6954f 100644 --- a/storage_drivers/ontap/ontap_nas.go +++ b/storage_drivers/ontap/ontap_nas.go @@ -114,15 +114,18 @@ func (d *NASStorageDriver) validate() error { } // Create a volume with the specified options -func (d *NASStorageDriver) Create(name string, sizeBytes uint64, opts map[string]string) error { +func (d *NASStorageDriver) Create( + volConfig *storage.VolumeConfig, storagePool *storage.Pool, volAttributes map[string]sa.Request, +) error { + + name := volConfig.InternalName if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ - "Method": "Create", - "Type": "NASStorageDriver", - "name": name, - "sizeBytes": sizeBytes, - "opts": opts, + "Method": "Create", + "Type": "NASStorageDriver", + "name": name, + "attrs": volAttributes, } log.WithFields(fields).Debug(">>>> Create") defer log.WithFields(fields).Debug("<<<< Create") @@ -137,11 +140,26 @@ func (d *NASStorageDriver) Create(name string, sizeBytes uint64, opts map[string return fmt.Errorf("volume %s already exists", name) } + // Determine volume size in bytes + requestedSize, err := utils.ConvertSizeToBytes(volConfig.Size) + if err != nil { + return fmt.Errorf("could not convert volume size %s: %v", volConfig.Size, err) + } + sizeBytes, err := strconv.ParseUint(requestedSize, 10, 64) + if err != nil { + return fmt.Errorf("%v is an invalid volume size: %v", volConfig.Size, err) + } sizeBytes, err = GetVolumeSize(sizeBytes, d.Config) if err != nil { return err } + // Get options + opts, err := d.GetVolumeOpts(volConfig, storagePool, volAttributes) + if err != nil { + return err + } + // get options with default fallback values // see also: ontap_common.go#PopulateConfigurationDefaults size := strconv.FormatUint(sizeBytes, 10) @@ -229,7 +247,11 @@ func (d *NASStorageDriver) Create(name string, sizeBytes uint64, opts map[string } // Create a volume clone -func (d *NASStorageDriver) CreateClone(name, source, snapshot string, opts map[string]string) error { +func (d *NASStorageDriver) CreateClone(volConfig *storage.VolumeConfig) error { + + name := volConfig.InternalName + source := volConfig.CloneSourceVolumeInternal + snapshot := volConfig.CloneSourceSnapshot if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ @@ -238,12 +260,16 @@ func (d *NASStorageDriver) CreateClone(name, source, snapshot string, opts map[s "name": name, "source": source, "snapshot": snapshot, - "opts": opts, } log.WithFields(fields).Debug(">>>> CreateClone") defer log.WithFields(fields).Debug("<<<< CreateClone") } + opts, err := d.GetVolumeOpts(volConfig, nil, make(map[string]sa.Request)) + if err != nil { + return err + } + split, err := strconv.ParseBool(utils.GetV(opts, "splitOnClone", d.Config.SplitOnClone)) if err != nil { return fmt.Errorf("invalid boolean value for splitOnClone: %v", err) @@ -432,14 +458,14 @@ func (d *NASStorageDriver) GetVolumeExternalWrappers( // Get all volumes matching the storage prefix volumesResponse, err := d.API.VolumeGetAll(*d.Config.StoragePrefix) if err = api.GetError(volumesResponse, err); err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } // Convert all volumes to VolumeExternal and write them to the channel if volumesResponse.Result.AttributesListPtr != nil { for _, volume := range volumesResponse.Result.AttributesListPtr.VolumeAttributesPtr { - channel <- &storage.VolumeExternalWrapper{d.getVolumeExternal(&volume), nil} + channel <- &storage.VolumeExternalWrapper{Volume: d.getVolumeExternal(&volume), Error: nil} } } } diff --git a/storage_drivers/ontap/ontap_nas_flexgroup.go b/storage_drivers/ontap/ontap_nas_flexgroup.go index 0e46a725d..51685e56e 100644 --- a/storage_drivers/ontap/ontap_nas_flexgroup.go +++ b/storage_drivers/ontap/ontap_nas_flexgroup.go @@ -118,15 +118,18 @@ func (d *NASFlexGroupStorageDriver) validate() error { } // Create a volume with the specified options -func (d *NASFlexGroupStorageDriver) Create(name string, sizeBytes uint64, opts map[string]string) error { +func (d *NASFlexGroupStorageDriver) Create( + volConfig *storage.VolumeConfig, storagePool *storage.Pool, volAttributes map[string]sa.Request, +) error { + + name := volConfig.InternalName if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ - "Method": "Create", - "Type": "NASFlexGroupStorageDriver", - "name": name, - "sizeBytes": sizeBytes, - "opts": opts, + "Method": "Create", + "Type": "NASFlexGroupStorageDriver", + "name": name, + "attrs": volAttributes, } log.WithFields(fields).Debug(">>>> Create") defer log.WithFields(fields).Debug("<<<< Create") @@ -141,6 +144,15 @@ func (d *NASFlexGroupStorageDriver) Create(name string, sizeBytes uint64, opts m return fmt.Errorf("FlexGroup %s already exists", name) } + // Determine volume size in bytes + requestedSize, err := utils.ConvertSizeToBytes(volConfig.Size) + if err != nil { + return fmt.Errorf("could not convert volume size %s: %v", volConfig.Size, err) + } + sizeBytes, err := strconv.ParseUint(requestedSize, 10, 64) + if err != nil { + return fmt.Errorf("%v is an invalid volume size: %v", volConfig.Size, err) + } sizeBytes, err = GetVolumeSize(sizeBytes, d.Config) if err != nil { return err @@ -170,6 +182,12 @@ func (d *NASFlexGroupStorageDriver) Create(name string, sizeBytes uint64, opts m "aggregates": vserverAggrs, }).Debug("Read aggregates assigned to SVM.") + // Get options + opts, err := d.GetVolumeOpts(volConfig, storagePool, volAttributes) + if err != nil { + return err + } + // get options with default fallback values // see also: ontap_common.go#PopulateConfigurationDefaults spaceReserve := utils.GetV(opts, "spaceReserve", d.Config.SpaceReserve) @@ -242,7 +260,7 @@ func (d *NASFlexGroupStorageDriver) Create(name string, sizeBytes uint64, opts m } // Create a volume clone -func (d *NASFlexGroupStorageDriver) CreateClone(name, source, snapshot string, opts map[string]string) error { +func (d *NASFlexGroupStorageDriver) CreateClone(volConfig *storage.VolumeConfig) error { return errors.New("clones are not supported for FlexGroups") } @@ -467,14 +485,14 @@ func (d *NASFlexGroupStorageDriver) GetVolumeExternalWrappers( // Get all volumes matching the storage prefix volumesResponse, err := d.API.FlexGroupGetAll(*d.Config.StoragePrefix) if err = api.GetError(volumesResponse, err); err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } // Convert all volumes to VolumeExternal and write them to the channel if volumesResponse.Result.AttributesListPtr != nil { for _, volume := range volumesResponse.Result.AttributesListPtr.VolumeAttributesPtr { - channel <- &storage.VolumeExternalWrapper{d.getVolumeExternal(&volume), nil} + channel <- &storage.VolumeExternalWrapper{Volume: d.getVolumeExternal(&volume), Error: nil} } } } diff --git a/storage_drivers/ontap/ontap_nas_qtree.go b/storage_drivers/ontap/ontap_nas_qtree.go index 96e8de337..b3b793f5b 100644 --- a/storage_drivers/ontap/ontap_nas_qtree.go +++ b/storage_drivers/ontap/ontap_nas_qtree.go @@ -196,15 +196,18 @@ func (d *NASQtreeStorageDriver) validate() error { } // Create a qtree-backed volume with the specified options -func (d *NASQtreeStorageDriver) Create(name string, sizeBytes uint64, opts map[string]string) error { +func (d *NASQtreeStorageDriver) Create( + volConfig *storage.VolumeConfig, storagePool *storage.Pool, volAttributes map[string]sa.Request, +) error { + + name := volConfig.InternalName if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ - "Method": "Create", - "Type": "NASQtreeStorageDriver", - "name": name, - "sizeBytes": sizeBytes, - "opts": opts, + "Method": "Create", + "Type": "NASQtreeStorageDriver", + "name": name, + "attrs": volAttributes, } log.WithFields(fields).Debug(">>>> Create") defer log.WithFields(fields).Debug("<<<< Create") @@ -228,6 +231,15 @@ func (d *NASQtreeStorageDriver) Create(name string, sizeBytes uint64, opts map[s return fmt.Errorf("volume %s already exists", name) } + // Determine volume size in bytes + requestedSize, err := utils.ConvertSizeToBytes(volConfig.Size) + if err != nil { + return fmt.Errorf("could not convert volume size %s: %v", volConfig.Size, err) + } + sizeBytes, err := strconv.ParseUint(requestedSize, 10, 64) + if err != nil { + return fmt.Errorf("%v is an invalid volume size: %v", volConfig.Size, err) + } sizeBytes, err = GetVolumeSize(sizeBytes, d.Config) if err != nil { return err @@ -238,6 +250,12 @@ func (d *NASQtreeStorageDriver) Create(name string, sizeBytes uint64, opts map[s return fmt.Errorf("volume %s name exceeds the limit of %d characters", name, maxQtreeNameLength) } + // Get options + opts, err := d.GetVolumeOpts(volConfig, storagePool, volAttributes) + if err != nil { + return err + } + // Get Flexvol options with default fallback values // see also: ontap_common.go#PopulateConfigurationDefaults aggregate := utils.GetV(opts, "aggregate", d.Config.Aggregate) @@ -296,7 +314,11 @@ func (d *NASQtreeStorageDriver) Create(name string, sizeBytes uint64, opts map[s } // Create a volume clone -func (d *NASQtreeStorageDriver) CreateClone(name, source, snapshot string, opts map[string]string) error { +func (d *NASQtreeStorageDriver) CreateClone(volConfig *storage.VolumeConfig) error { + + name := volConfig.InternalName + source := volConfig.CloneSourceVolumeInternal + snapshot := volConfig.CloneSourceSnapshot if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ @@ -305,7 +327,6 @@ func (d *NASQtreeStorageDriver) CreateClone(name, source, snapshot string, opts "name": name, "source": source, "snapshot": snapshot, - "opts": opts, } log.WithFields(fields).Debug(">>>> CreateClone") defer log.WithFields(fields).Debug("<<<< CreateClone") @@ -1097,7 +1118,7 @@ func (d *NASQtreeStorageDriver) GetVolumeExternalWrappers( // Get all volumes matching the storage prefix volumesResponse, err := d.API.VolumeGetAll(d.FlexvolNamePrefix()) if err = api.GetError(volumesResponse, err); err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } @@ -1112,7 +1133,7 @@ func (d *NASQtreeStorageDriver) GetVolumeExternalWrappers( // Get all qtrees in all Flexvols matching the storage prefix qtreesResponse, err := d.API.QtreeGetAll(d.FlexvolNamePrefix()) if err = api.GetError(qtreesResponse, err); err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } @@ -1127,7 +1148,7 @@ func (d *NASQtreeStorageDriver) GetVolumeExternalWrappers( // Get all quotas in all Flexvols matching the storage prefix quotasResponse, err := d.API.QuotaEntryList(d.FlexvolNamePrefix() + "*") if err = api.GetError(quotasResponse, err); err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } @@ -1175,7 +1196,7 @@ func (d *NASQtreeStorageDriver) GetVolumeExternalWrappers( continue } - channel <- &storage.VolumeExternalWrapper{d.getVolumeExternal(&qtree, &volume, "a), nil} + channel <- &storage.VolumeExternalWrapper{Volume: d.getVolumeExternal(&qtree, &volume, "a), Error: nil} } } } diff --git a/storage_drivers/ontap/ontap_san.go b/storage_drivers/ontap/ontap_san.go index 1ceb9d6fa..b5788c563 100644 --- a/storage_drivers/ontap/ontap_san.go +++ b/storage_drivers/ontap/ontap_san.go @@ -184,15 +184,18 @@ func (d *SANStorageDriver) validate() error { } // Create a volume+LUN with the specified options -func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string]string) error { +func (d *SANStorageDriver) Create( + volConfig *storage.VolumeConfig, storagePool *storage.Pool, volAttributes map[string]sa.Request, +) error { + + name := volConfig.InternalName if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ - "Method": "Create", - "Type": "SANStorageDriver", - "name": name, - "sizeBytes": sizeBytes, - "opts": opts, + "Method": "Create", + "Type": "SANStorageDriver", + "name": name, + "attrs": volAttributes, } log.WithFields(fields).Debug(">>>> Create") defer log.WithFields(fields).Debug("<<<< Create") @@ -207,11 +210,26 @@ func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string return fmt.Errorf("volume %s already exists", name) } + // Determine volume size in bytes + requestedSize, err := utils.ConvertSizeToBytes(volConfig.Size) + if err != nil { + return fmt.Errorf("could not convert volume size %s: %v", volConfig.Size, err) + } + sizeBytes, err := strconv.ParseUint(requestedSize, 10, 64) + if err != nil { + return fmt.Errorf("%v is an invalid volume size: %v", volConfig.Size, err) + } sizeBytes, err = GetVolumeSize(sizeBytes, d.Config) if err != nil { return err } + // Get options + opts, err := d.GetVolumeOpts(volConfig, storagePool, volAttributes) + if err != nil { + return err + } + // Get options with default fallback values // see also: ontap_common.go#PopulateConfigurationDefaults size := strconv.FormatUint(sizeBytes, 10) @@ -308,7 +326,11 @@ func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string } // Create a volume clone -func (d *SANStorageDriver) CreateClone(name, source, snapshot string, opts map[string]string) error { +func (d *SANStorageDriver) CreateClone(volConfig *storage.VolumeConfig) error { + + name := volConfig.InternalName + source := volConfig.CloneSourceVolumeInternal + snapshot := volConfig.CloneSourceSnapshot if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ @@ -317,12 +339,16 @@ func (d *SANStorageDriver) CreateClone(name, source, snapshot string, opts map[s "name": name, "source": source, "snapshot": snapshot, - "opts": opts, } log.WithFields(fields).Debug(">>>> CreateClone") defer log.WithFields(fields).Debug("<<<< CreateClone") } + opts, err := d.GetVolumeOpts(volConfig, nil, make(map[string]sa.Request)) + if err != nil { + return err + } + split, err := strconv.ParseBool(utils.GetV(opts, "splitOnClone", d.Config.SplitOnClone)) if err != nil { return fmt.Errorf("invalid boolean value for splitOnClone: %v", err) @@ -714,7 +740,7 @@ func (d *SANStorageDriver) GetVolumeExternalWrappers( // Get all volumes matching the storage prefix volumesResponse, err := d.API.VolumeGetAll(*d.Config.StoragePrefix) if err = api.GetError(volumesResponse, err); err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } @@ -722,7 +748,7 @@ func (d *SANStorageDriver) GetVolumeExternalWrappers( lunPathPattern := fmt.Sprintf("/vol/%v/lun0", *d.Config.StoragePrefix+"*") lunsResponse, err := d.API.LunGetAll(lunPathPattern) if err = api.GetError(lunsResponse, err); err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } @@ -745,7 +771,7 @@ func (d *SANStorageDriver) GetVolumeExternalWrappers( continue } - channel <- &storage.VolumeExternalWrapper{d.getVolumeExternal(&lun, &volume), nil} + channel <- &storage.VolumeExternalWrapper{Volume: d.getVolumeExternal(&lun, &volume), Error: nil} } } } diff --git a/storage_drivers/solidfire/solidfire_san.go b/storage_drivers/solidfire/solidfire_san.go index 097da1a85..588f81370 100644 --- a/storage_drivers/solidfire/solidfire_san.go +++ b/storage_drivers/solidfire/solidfire_san.go @@ -41,17 +41,6 @@ type SANStorageDriver struct { InitiatorIFace string } -type StorageDriverConfigExternal struct { - *drivers.CommonStorageDriverConfigExternal - TenantName string - EndPoint string - SVIP string - InitiatorIFace string //iface to use of iSCSI initiator - Types *[]api.VolType - AccessGroups []int64 - UseCHAP bool -} - type Telemetry struct { tridentconfig.Telemetry Plugin string `json:"plugin"` @@ -498,15 +487,18 @@ func MakeSolidFireName(name string) string { } // Create a SolidFire volume -func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string]string) error { +func (d *SANStorageDriver) Create( + volConfig *storage.VolumeConfig, storagePool *storage.Pool, volAttributes map[string]sa.Request, +) error { + + name := volConfig.InternalName if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ - "Method": "Create", - "Type": "SANStorageDriver", - "name": name, - "sizeBytes": sizeBytes, - "opts": opts, + "Method": "Create", + "Type": "SANStorageDriver", + "name": name, + "attrs": volAttributes, } log.WithFields(fields).Debug(">>>> Create") defer log.WithFields(fields).Debug("<<<< Create") @@ -526,6 +518,15 @@ func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string return errors.New("volume with requested name already exists") } + // Determine volume size in bytes + requestedSize, err := utils.ConvertSizeToBytes(volConfig.Size) + if err != nil { + return fmt.Errorf("could not convert volume size %s: %v", volConfig.Size, err) + } + sizeBytes, err := strconv.ParseUint(requestedSize, 10, 64) + if err != nil { + return fmt.Errorf("%v is an invalid volume size: %v", volConfig.Size, err) + } if sizeBytes == 0 { defaultSize, _ := utils.ConvertSizeToBytes(d.Config.Size) sizeBytes, _ = strconv.ParseUint(defaultSize, 10, 64) @@ -538,6 +539,12 @@ func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string return checkVolumeSizeLimitsError } + // Get options + opts, err := d.GetVolumeOpts(volConfig, storagePool, volAttributes) + if err != nil { + return err + } + qosOpt := utils.GetV(opts, "qos", "") if qosOpt != "" { qos, err = parseQOS(qosOpt) @@ -602,7 +609,11 @@ func (d *SANStorageDriver) Create(name string, sizeBytes uint64, opts map[string } // Create a volume clone -func (d *SANStorageDriver) CreateClone(name, sourceName, snapshotName string, opts map[string]string) error { +func (d *SANStorageDriver) CreateClone(volConfig *storage.VolumeConfig) error { + + name := volConfig.InternalName + sourceName := volConfig.CloneSourceVolumeInternal + snapshotName := volConfig.CloneSourceSnapshot if d.Config.DebugTraceFlags["method"] { fields := log.Fields{ @@ -611,7 +622,6 @@ func (d *SANStorageDriver) CreateClone(name, sourceName, snapshotName string, op "name": name, "source": sourceName, "snapshot": snapshotName, - "opts": opts, } log.WithFields(fields).Debug(">>>> CreateClone") defer log.WithFields(fields).Debug("<<<< CreateClone") @@ -621,6 +631,11 @@ func (d *SANStorageDriver) CreateClone(name, sourceName, snapshotName string, op var qos api.QoS doModify := false + opts, err := d.GetVolumeOpts(volConfig, nil, make(map[string]sa.Request)) + if err != nil { + return err + } + qosOpt := utils.GetV(opts, "qos", "") if qosOpt != "" { doModify = true @@ -1196,13 +1211,13 @@ func (d *SANStorageDriver) GetVolumeExternalWrappers( // Get all volumes volumes, err := d.getVolumes() if err != nil { - channel <- &storage.VolumeExternalWrapper{nil, err} + channel <- &storage.VolumeExternalWrapper{Volume: nil, Error: err} return } // Convert all volumes to VolumeExternal and write them to the channel for externalName, volume := range volumes { - channel <- &storage.VolumeExternalWrapper{d.getVolumeExternal(externalName, &volume), nil} + channel <- &storage.VolumeExternalWrapper{Volume: d.getVolumeExternal(externalName, &volume), Error: nil} } } diff --git a/storage_drivers/types.go b/storage_drivers/types.go index 5f7367814..da0ac8bca 100644 --- a/storage_drivers/types.go +++ b/storage_drivers/types.go @@ -28,7 +28,7 @@ type CommonStorageDriverConfig struct { DisableDelete bool `json:"disableDelete"` StoragePrefixRaw json.RawMessage `json:"storagePrefix,string"` StoragePrefix *string `json:"-"` - SerialNumbers []string `json:"-"` + SerialNumbers []string `json:"serialNumbers,omitEmpty"` DriverContext trident.DriverContext `json:"-"` LimitVolumeSize string `json:"limitVolumeSize"` } @@ -124,9 +124,17 @@ type SolidfireStorageDriverConfigDefaults struct { type FakeStorageDriverConfig struct { *CommonStorageDriverConfig Protocol trident.Protocol `json:"protocol"` - // pools represents the possible buckets into which a given volume should go - Pools map[string]*fake.StoragePool `json:"pools"` - InstanceName string `json:"instanceName"` + // Pools are the modeled physical pools. At least one is required. + Pools map[string]*fake.StoragePool `json:"pools"` + InstanceName string `json:"instanceName"` + FakeStorageDriverPool + Storage []FakeStorageDriverPool `json:"storage"` +} + +type FakeStorageDriverPool struct { + Labels map[string]string `json:"labels"` + Region string `json:"region"` + Zone string `json:"zone"` FakeStorageDriverConfigDefaults `json:"defaults"` } @@ -225,33 +233,12 @@ func GetDefaultIgroupName(context trident.DriverContext) string { } } -type CommonStorageDriverConfigExternal struct { - Version int `json:"version"` - StorageDriverName string `json:"storageDriverName"` - StoragePrefix *string `json:"storagePrefix"` - SerialNumbers []string `json:"serialNumbers"` -} - func SanitizeCommonStorageDriverConfig(c *CommonStorageDriverConfig) { if c != nil && c.StoragePrefixRaw == nil { c.StoragePrefixRaw = json.RawMessage("{}") } } -func GetCommonStorageDriverConfigExternal( - c *CommonStorageDriverConfig, -) *CommonStorageDriverConfigExternal { - - SanitizeCommonStorageDriverConfig(c) - - return &CommonStorageDriverConfigExternal{ - Version: c.Version, - StorageDriverName: c.StorageDriverName, - StoragePrefix: c.StoragePrefix, - SerialNumbers: c.SerialNumbers, - } -} - func GetCommonInternalVolumeName(c *CommonStorageDriverConfig, name string) string { prefixToUse := trident.OrchestratorName @@ -315,3 +302,29 @@ func Clone(source, destination interface{}) { enc.Encode(source) dec.Decode(destination) } + +type BackendIneligibleError struct { + message string +} + +func (e *BackendIneligibleError) Error() string { return e.message } + +func NewBackendIneligibleError(volumeName string, errors []error) error { + messages := make([]string, 0) + for _, err := range errors { + messages = append(messages, err.Error()) + } + + return &BackendIneligibleError{ + message: fmt.Sprintf("backend cannot satisfy create request for volume %s: (%s)", + volumeName, strings.Join(messages, "; ")), + } +} + +func IsBackendIneligibleError(err error) bool { + if err == nil { + return false + } + _, ok := err.(*BackendIneligibleError) + return ok +}