From 6cbf03ed70145d8a0fc940b5264b8f6e9b557ae0 Mon Sep 17 00:00:00 2001 From: Sirish Bathina Date: Tue, 7 Dec 2021 08:45:31 -1000 Subject: [PATCH] Tagging support (#1146) * tagging support * use parsed id * use attachTag * using categorys and tags * update snapshot list * using list instead * add login method * revamped tagging * adding support to delete tags * fix lint issues * Unit tests * after carls review * Use a parse method --- go.mod | 2 +- go.sum | 4 +- pkg/blockstorage/vmware/vmware.go | 222 +++++++++++++++++++++- pkg/blockstorage/vmware/vmware_test.go | 247 +++++++++++++++++++++++++ 4 files changed, 465 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 2d0e0eb75c..4387350ff5 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/softlayer/softlayer-go v0.0.0-20190615201252-ba6e7f295217 // indirect github.com/spf13/cobra v1.1.3 - github.com/vmware/govmomi v0.21.1-0.20191008161538-40aebf13ba45 + github.com/vmware/govmomi v0.22.2-0.20200329013745-f2eef8fc745f github.com/zeebo/blake3 v0.1.2 // indirect go.mongodb.org/mongo-driver v1.5.1 // indirect go.uber.org/zap v1.17.0 diff --git a/go.sum b/go.sum index 4bdf2339ad..3ad1cc9d8d 100644 --- a/go.sum +++ b/go.sum @@ -1038,8 +1038,8 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/vmware/govmomi v0.21.1-0.20191008161538-40aebf13ba45 h1:zpQBW+l4uPQTfTOxedN5GEcSONhabbCf3X+5+P/H4Jk= -github.com/vmware/govmomi v0.21.1-0.20191008161538-40aebf13ba45/go.mod h1:zbnFoBQ9GIjs2RVETy8CNEpb+L+Lwkjs3XZUL0B3/m0= +github.com/vmware/govmomi v0.22.2-0.20200329013745-f2eef8fc745f h1:6LIYlihC1/LDUhZ7zYVp1WOEY5owzsvogiaHBqvBzPU= +github.com/vmware/govmomi v0.22.2-0.20200329013745-f2eef8fc745f/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAulACy9Lc= github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= diff --git a/pkg/blockstorage/vmware/vmware.go b/pkg/blockstorage/vmware/vmware.go index 42ce204e59..dfdd14b9c6 100644 --- a/pkg/blockstorage/vmware/vmware.go +++ b/pkg/blockstorage/vmware/vmware.go @@ -6,11 +6,14 @@ import ( "net/url" "os" "strconv" + "strings" "time" "github.com/pkg/errors" uuid "github.com/satori/go.uuid" "github.com/vmware/govmomi/cns" + "github.com/vmware/govmomi/vapi/rest" + vapitags "github.com/vmware/govmomi/vapi/tags" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/soap" @@ -52,8 +55,11 @@ var ( // FcdProvider provides blockstorage.Provider type FcdProvider struct { - Gom *vslm.GlobalObjectManager - Cns *cns.Client + Gom *vslm.GlobalObjectManager + Cns *cns.Client + TagsSvc *vapitags.Manager + tagManager tagManager + categoryID string } // NewProvider creates new VMWare FCD provider with the config. @@ -96,10 +102,21 @@ func NewProvider(config map[string]string) (blockstorage.Provider, error) { if err != nil { return nil, errors.Wrap(err, "Failed to create VSLM client") } + c := rest.NewClient(cli) + err = c.Login(ctx, url.UserPassword(username, password)) + if err != nil { + return nil, errors.Wrap(err, "Failed to login to VAPI rest client") + } + tm := vapitags.NewManager(c) + if err != nil { + return nil, errors.Wrap(err, "Failed to create tag manager") + } gom := vslm.NewGlobalObjectManager(vslmCli) return &FcdProvider{ - Cns: cnsCli, - Gom: gom, + Cns: cnsCli, + Gom: gom, + TagsSvc: tm, + tagManager: tm, }, nil } @@ -108,6 +125,11 @@ func (p *FcdProvider) Type() blockstorage.Type { return blockstorage.TypeFCD } +// Type is part of blockstorage.Provider +func (p *FcdProvider) SetCategoryID(categoryID string) { + p.categoryID = categoryID +} + // VolumeCreate is part of blockstorage.Provider func (p *FcdProvider) VolumeCreate(ctx context.Context, volume blockstorage.Volume) (*blockstorage.Volume, error) { return nil, errors.New("Not implemented") @@ -237,8 +259,12 @@ func (p *FcdProvider) SnapshotCreate(ctx context.Context, volume blockstorage.Vo if snap.SizeInBytes == 0 { snap.SizeInBytes = volume.SizeInBytes } - snap.Volume = &volume + + if err = p.SetTags(ctx, snap, tags); err != nil { + return nil, errors.Wrap(err, "Failed to set tags") + } + return snap, nil } @@ -294,6 +320,10 @@ func (p *FcdProvider) SnapshotDelete(ctx context.Context, snapshot *blockstorage return false, errors.Wrap(lerr, "Failed to wait on task") } log.Debug().Print("SnapshotDelete task complete", field.M{"VolumeID": volID, "SnapshotID": snapshotID}) + err = p.deleteSnapshotTags(ctx, snapshot) + if err != nil { + return false, errors.Wrap(err, "Failed to delete snapshot tags") + } return true, nil }) } @@ -337,7 +367,7 @@ func (p *FcdProvider) SetTags(ctx context.Context, resource interface{}, tags ma case *blockstorage.Volume: return p.setTagsVolume(ctx, r, tags) case *blockstorage.Snapshot: - return nil + return p.setSnapshotTags(ctx, r, tags) default: return errors.New("Unsupported type for resource") } @@ -358,6 +388,121 @@ func (p *FcdProvider) setTagsVolume(ctx context.Context, volume *blockstorage.Vo return nil } +// GetOrCreateCategory takes a category name and attempts to get or create the category +// it returns the category ID. +func (p *FcdProvider) GetOrCreateCategory(ctx context.Context, categoryName string) (string, error) { + id, err := p.GetCategoryID(ctx, categoryName) + if err != nil { + if strings.Contains(err.Error(), "404 Not Found") { + id, err := p.tagManager.CreateCategory(ctx, &vapitags.Category{ + Name: categoryName, + Cardinality: "SINGLE", + }) + if err != nil { + return "", errors.Wrap(err, "Failed to create category") + } + return id, nil + } + return "", err + } + return id, nil +} + +// GetCategoryID takes a category name and returns the category ID if it finds it. +func (p *FcdProvider) GetCategoryID(ctx context.Context, categoryName string) (string, error) { + cat, err := p.tagManager.GetCategory(ctx, categoryName) + if err != nil { + return "", errors.Wrap(err, "Failed to find category") + } + return cat.ID, nil +} + +// snapshotTag is the struct that will be used to create vmware tags +// the tags are of the form volid:snapid:tag:value +// these tags are assigned to a predefined category that is initialized by the FcdProvider +type snapshotTag struct { + volid string + snapid string + key string + value string +} + +func (t *snapshotTag) String() string { + volid := strings.ReplaceAll(t.volid, ":", "-") + snapid := strings.ReplaceAll(t.snapid, ":", "-") + key := strings.ReplaceAll(t.key, ":", "-") + value := strings.ReplaceAll(t.value, ":", "-") + return fmt.Sprintf("%s:%s:%s:%s", volid, snapid, key, value) +} + +func (t *snapshotTag) Parse(tag string) error { + parts := strings.Split(tag, ":") + if len(parts) != 4 { + return errors.Errorf("Malformed tag (%s)", tag) + } + t.volid, t.snapid, t.key, t.value = parts[0], parts[1], parts[2], parts[3] + return nil +} + +// setSnapshotTags sets tags for a snapshot +func (p *FcdProvider) setSnapshotTags(ctx context.Context, snapshot *blockstorage.Snapshot, tags map[string]string) error { + if p.categoryID == "" { + log.Debug().Print("vSphere snapshot tagging is disabled") + return nil + } + if snapshot == nil { + return errors.New("Empty snapshot") + } + volID, snapID, err := SplitSnapshotFullID(snapshot.ID) + if err != nil { + return errors.Wrap(err, "Cannot infer volumeID and snapshotID from full snapshot ID") + } + + for k, v := range tags { + tag := &snapshotTag{volID, snapID, k, v} + _, err = p.tagManager.CreateTag(ctx, &vapitags.Tag{ + CategoryID: p.categoryID, + Name: tag.String(), + }) + if err != nil && !strings.Contains(err.Error(), "ALREADY_EXISTS") { + return errors.Wrapf(err, "Failed to create tag (%s) for categoryID (%s) ", tag, p.categoryID) + } + } + return nil +} + +func (p *FcdProvider) deleteSnapshotTags(ctx context.Context, snapshot *blockstorage.Snapshot) error { + if p.categoryID == "" { + log.Debug().Print("vSphere snapshot tagging is disabled (categoryID not set). Cannot list snapshots") + return nil + } + if snapshot == nil { + return errors.New("Empty snapshot") + } + volID, snapID, err := SplitSnapshotFullID(snapshot.ID) + if err != nil { + return errors.Wrap(err, "Cannot infer volumeID and snapshotID from full snapshot ID") + } + categoryTags, err := p.tagManager.GetTagsForCategory(ctx, p.categoryID) + if err != nil { + return errors.Wrap(err, "Failed to list tags") + } + for _, tag := range categoryTags { + parsedTag := &snapshotTag{} + err := parsedTag.Parse(tag.Name) + if err != nil { + return errors.Wrapf(err, "Failed to parse tag (%s)", tag.Name) + } + if parsedTag.snapid == snapID && parsedTag.volid == volID { + err := p.tagManager.DeleteTag(ctx, &tag) + if err != nil { + return errors.Wrapf(err, "Failed to delete tag (%s)", tag.Name) + } + } + } + return nil +} + // VolumesList is part of blockstorage.Provider func (p *FcdProvider) VolumesList(ctx context.Context, tags map[string]string, zone string) ([]*blockstorage.Volume, error) { return nil, errors.New("Not implemented") @@ -365,7 +510,62 @@ func (p *FcdProvider) VolumesList(ctx context.Context, tags map[string]string, z // SnapshotsList is part of blockstorage.Provider func (p *FcdProvider) SnapshotsList(ctx context.Context, tags map[string]string) ([]*blockstorage.Snapshot, error) { - return nil, errors.New("Not implemented") + if p.categoryID == "" { + log.Debug().Print("vSphere snapshot tagging is disabled (categoryID not set). Cannot list snapshots") + return nil, nil + } + + categoryTags, err := p.tagManager.GetTagsForCategory(ctx, p.categoryID) + if err != nil { + return nil, errors.Wrap(err, "Failed to list tags") + } + + snapshotIDs, err := p.getSnapshotIDsFromTags(categoryTags, tags) + if err != nil { + return nil, errors.Wrap(err, "Failed to get snapshotIDs from tags") + } + + var snapshots []*blockstorage.Snapshot + if len(snapshotIDs) > 0 { + for _, snapshotID := range snapshotIDs { + snapshot, err := p.SnapshotGet(ctx, snapshotID) + if err != nil { + return nil, err + } + snapshots = append(snapshots, snapshot) + } + } + return snapshots, nil +} + +func (p *FcdProvider) getSnapshotIDsFromTags(categoryTags []vapitags.Tag, tags map[string]string) ([]string, error) { + snapshotTagMap := map[string]map[string]string{} + for _, catTag := range categoryTags { + parsedTag := &snapshotTag{} + err := parsedTag.Parse(catTag.Name) + if err != nil { + return nil, errors.Wrapf(err, "Failed to parse tag") + } + if _, ok := snapshotTagMap[parsedTag.snapid]; !ok { + snapshotTagMap[parsedTag.snapid] = map[string]string{} + } + snapshotTagMap[parsedTag.snapid][parsedTag.key] = parsedTag.value + } + + snapshotIDs := []string{} + for snapshotID, snapshotTags := range snapshotTagMap { + tagsMatch := true + for k, v := range tags { + if val, ok := snapshotTags[k]; !ok || val != v { + tagsMatch = false + break + } + } + if tagsMatch { + snapshotIDs = append(snapshotIDs, snapshotID) + } + } + return snapshotIDs, nil } func getEnvAsIntOrDefault(envKey string, def int) int { @@ -379,3 +579,11 @@ func getEnvAsIntOrDefault(envKey string, def int) int { return def } + +type tagManager interface { + GetCategory(ctx context.Context, id string) (*vapitags.Category, error) + CreateCategory(ctx context.Context, category *vapitags.Category) (string, error) + CreateTag(ctx context.Context, tag *vapitags.Tag) (string, error) + GetTagsForCategory(ctx context.Context, id string) ([]vapitags.Tag, error) + DeleteTag(ctx context.Context, tag *vapitags.Tag) error +} diff --git a/pkg/blockstorage/vmware/vmware_test.go b/pkg/blockstorage/vmware/vmware_test.go index 7acae0198f..fce7fe4c2d 100644 --- a/pkg/blockstorage/vmware/vmware_test.go +++ b/pkg/blockstorage/vmware/vmware_test.go @@ -1,10 +1,15 @@ package vmware import ( + "context" + "fmt" "os" + "sort" "testing" "time" + "github.com/kanisterio/kanister/pkg/blockstorage" + vapitags "github.com/vmware/govmomi/vapi/tags" . "gopkg.in/check.v1" ) @@ -85,3 +90,245 @@ func (s *VMWareSuite) TestTimeoutEnvSetting(c *C) { os.Setenv(vmWareTimeoutMinEnv, tempEnv) } + +func (s *VMWareSuite) TestGetSnapshotIDsFromTags(c *C) { + for _, tc := range []struct { + catTags []vapitags.Tag + tags map[string]string + errChecker Checker + snapIDs []string + }{ + { + catTags: []vapitags.Tag{ + {Name: "v1:s1:k1:v1"}, + {Name: "v1:s1:k2:v2"}, + {Name: "v1:s2:k1:v1"}, + }, + tags: map[string]string{ + "k1": "v1", + "k2": "v2", + }, + snapIDs: []string{"s1"}, + errChecker: IsNil, + }, + { + catTags: []vapitags.Tag{ + {Name: "v1:s1:k1:v1"}, + {Name: "v1:s1:k2:v2"}, + {Name: "v1:s2:k1:v1"}, + }, + tags: map[string]string{ + "k1": "v1", + }, + snapIDs: []string{"s1", "s2"}, + errChecker: IsNil, + }, + { + catTags: []vapitags.Tag{ + {Name: "v1:s1:k1:v1"}, + {Name: "v1:s1:k2:v2"}, + {Name: "v1:s2:k1:v1"}, + }, + snapIDs: []string{"s1", "s2"}, + errChecker: IsNil, + }, + { + catTags: []vapitags.Tag{ + {Name: "v1:s1k1:v1"}, + }, + tags: map[string]string{ + "k1": "v1", + }, + errChecker: NotNil, + }, + } { + fp := &FcdProvider{} + snapIDs, err := fp.getSnapshotIDsFromTags(tc.catTags, tc.tags) + c.Assert(err, tc.errChecker) + if tc.errChecker == IsNil { + sort.Strings(snapIDs) + sort.Strings(tc.snapIDs) + c.Assert(snapIDs, DeepEquals, tc.snapIDs) + } + } +} + +func (s *VMWareSuite) TestSetTagsSnapshot(c *C) { + ctx := context.Background() + for _, tc := range []struct { + catID string + snapshot *blockstorage.Snapshot + tags map[string]string + errChecker Checker + expNumCreates int + + errCreateTag error + }{ + { // success + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volid:snapid"}, + tags: map[string]string{ + "t1": "v1", + "t2": "v2", + }, + expNumCreates: 2, + errChecker: IsNil, + }, + { // idempotent creates + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volid:snapid"}, + tags: map[string]string{ + "t1": "v1", + "t2": "v2", + }, + expNumCreates: 2, + errCreateTag: fmt.Errorf("ALREADY_EXISTS"), + errChecker: IsNil, + }, + { // create failure + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volid:snapid"}, + tags: map[string]string{ + "t1": "v1", + "t2": "v2", + }, + expNumCreates: 2, + errCreateTag: fmt.Errorf("bad create"), + errChecker: NotNil, + }, + { // malformed id + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volidsnapid"}, + errChecker: NotNil, + }, + { // nil snapshot + catID: "catid", + errChecker: NotNil, + }, + { // empty id, No error, not supported + catID: "", + errChecker: IsNil, + }, + } { + ftm := &fakeTagManager{ + errCreateTag: tc.errCreateTag, + } + provider := &FcdProvider{ + categoryID: tc.catID, + tagManager: ftm, + } + err := provider.setSnapshotTags(ctx, tc.snapshot, tc.tags) + c.Assert(err, tc.errChecker) + if tc.errChecker == IsNil { + c.Assert(ftm.numCreates, Equals, tc.expNumCreates) + } + } +} + +func (s *VMWareSuite) TestDeleteTagsSnapshot(c *C) { + ctx := context.Background() + for _, tc := range []struct { + catID string + snapshot *blockstorage.Snapshot + errChecker Checker + expNumDeletes int + + retGetTagsForCategory []vapitags.Tag + errGetTagsForCategory error + errDeleteTag error + }{ + { // success deleting tags + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volid:snapid"}, + retGetTagsForCategory: []vapitags.Tag{ + {Name: "volid:snapid:t1:v1"}, + {Name: "volid:snapid:t2:v2"}, + {Name: "volid:snapid2:t1:v1"}, + }, + expNumDeletes: 2, + errChecker: IsNil, + }, + { // error deleting tags + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volid:snapid"}, + retGetTagsForCategory: []vapitags.Tag{ + {Name: "volid:snapid:t1:v1"}, + {Name: "volid:snapid:t2:v2"}, + }, + errDeleteTag: fmt.Errorf("Failed to delete tag"), + errChecker: NotNil, + }, + { // error parsing tags + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volid:snapid"}, + retGetTagsForCategory: []vapitags.Tag{ + {Name: "volid:snapidt1v1"}, + {Name: "volid:snapid:t2:v2"}, + }, + errChecker: NotNil, + }, + { // error fetching tags + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volid:snapid"}, + errGetTagsForCategory: fmt.Errorf("Failed to get tags"), + errChecker: NotNil, + }, + { // malformed id + catID: "catid", + snapshot: &blockstorage.Snapshot{ID: "volidsnapid"}, + errChecker: NotNil, + }, + { // nil snapshot + catID: "catid", + errChecker: NotNil, + }, + { // empty id, No error, not supported + catID: "", + errChecker: IsNil, + }, + } { + ftm := &fakeTagManager{ + retGetTagsForCategory: tc.retGetTagsForCategory, + errGetTagsForCategory: tc.errGetTagsForCategory, + errDeleteTag: tc.errDeleteTag, + } + provider := &FcdProvider{ + categoryID: tc.catID, + tagManager: ftm, + } + err := provider.deleteSnapshotTags(ctx, tc.snapshot) + c.Assert(err, tc.errChecker) + if tc.errChecker == IsNil { + c.Assert(ftm.numDeletes, Equals, tc.expNumDeletes) + } + } +} + +type fakeTagManager struct { + retGetTagsForCategory []vapitags.Tag + errGetTagsForCategory error + + numDeletes int + errDeleteTag error + + numCreates int + errCreateTag error +} + +func (f *fakeTagManager) GetCategory(ctx context.Context, id string) (*vapitags.Category, error) { + return nil, nil +} +func (f *fakeTagManager) CreateCategory(ctx context.Context, category *vapitags.Category) (string, error) { + return "", nil +} +func (f *fakeTagManager) CreateTag(ctx context.Context, tag *vapitags.Tag) (string, error) { + f.numCreates++ + return "", f.errCreateTag +} +func (f *fakeTagManager) GetTagsForCategory(ctx context.Context, id string) ([]vapitags.Tag, error) { + return f.retGetTagsForCategory, f.errGetTagsForCategory +} +func (f *fakeTagManager) DeleteTag(ctx context.Context, tag *vapitags.Tag) error { + f.numDeletes++ + return f.errDeleteTag +}