From b04e430cc4dc0389967754eaee00013ce4bf1780 Mon Sep 17 00:00:00 2001 From: hippo-an Date: Tue, 9 Jul 2024 14:44:53 +0900 Subject: [PATCH 01/56] setup gcp DriverCapabilityInfo --- cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go b/cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go index 4e4a473e7..2f5dc9fa6 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go @@ -18,6 +18,7 @@ import ( gcpcon "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/gcp/connect" gcps "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/gcp/resources" + ires "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" icon "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/connect" @@ -62,6 +63,8 @@ func (GCPDriver) GetDriverCapability() idrv.DriverCapabilityInfo { drvCapabilityInfo.RegionZoneHandler = true drvCapabilityInfo.ClusterHandler = true drvCapabilityInfo.PriceInfoHandler = true + drvCapabilityInfo.TagHandler = true + drvCapabilityInfo.TagSupportResourceType = []ires.RSType{ires.VM, ires.DISK, ires.CLUSTER} return drvCapabilityInfo } From fad495aea5476a34da64b54215fa01178b318e8f Mon Sep 17 00:00:00 2001 From: hippo-an Date: Thu, 11 Jul 2024 14:00:15 +0900 Subject: [PATCH 02/56] gcp tag handler implements except find tags --- .../drivers/gcp/resources/TagHandler.go | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go new file mode 100644 index 000000000..d0b7a0b16 --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go @@ -0,0 +1,418 @@ +// Proof of Concepts of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// This is a Cloud Driver + +package resources + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + compute "google.golang.org/api/compute/v1" + container "google.golang.org/api/container/v1" + + idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" + irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" +) + +type GCPTagHandler struct { + Region idrv.RegionInfo + Ctx context.Context + Credential idrv.CredentialInfo + + ComputeClient *compute.Service + ContainerClient *container.Service +} + +var ( + supportRSType = map[irs.RSType]interface{}{ + irs.VM: nil, irs.DISK: nil, irs.CLUSTER: nil, + } +) + +func validateSupportRS(resType irs.RSType) error { + if _, ok := supportRSType[resType]; !ok { + return errors.New("unsupported resources type") + } + return nil +} + +func (t *GCPTagHandler) getVm(resIID irs.IID) (*compute.Instance, error) { + vm, err := t.ComputeClient.Instances.Get(t.Credential.ProjectID, t.Region.Zone, resIID.SystemId).Do() + if err != nil { + return nil, err + } + + return vm, nil +} + +func (t *GCPTagHandler) getDisk(resIID irs.IID) (*compute.Disk, error) { + disk, err := GetDiskInfo(t.ComputeClient, t.Credential, t.Region, resIID.SystemId) + if err != nil { + return nil, err + } + + return disk, nil +} + +func (t *GCPTagHandler) getCluster(resIID irs.IID) (*container.Cluster, error) { + parent := getParentClusterAtContainer(t.Credential.ProjectID, t.Region.Zone, resIID.SystemId) + cluster, err := t.ContainerClient.Projects.Locations.Clusters.Get(parent).Do() + if err != nil { + return nil, err + } + + return cluster, nil +} + +func (t *GCPTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag KeyValue) (KeyValue, error) { + err := validateSupportRS(resType) + errRes := KeyValue{} + if err != nil { + return errRes, err + } + + projectId := t.Credential.ProjectID + zone := t.Region.Zone + switch resType { + case irs.VM: + vm, err := t.getVm(resIID) + if err != nil { + return errRes, err + } + + existLabels := vm.Labels + existLabels[tag.Key] = tag.Value + + req := &compute.InstancesSetLabelsRequest{ + LabelFingerprint: vm.Fingerprint, + Labels: existLabels, + } + + op, err := t.ComputeClient.Instances.SetLabels(projectId, zone, resIID.SystemId, req).Do() + + if err != nil { + return errRes, err + } + + if op.Error != nil { + return errRes, fmt.Errorf("operation failed: %v", op.Error.Errors) + } + + return tag, nil + case irs.DISK: + + disk, err := t.getDisk(resIID) + if err != nil { + return errRes, err + } + + existLabels := disk.Labels + existLabels[tag.Key] = tag.Value + + req := &compute.ZoneSetLabelsRequest{ + LabelFingerprint: disk.LabelFingerprint, + Labels: existLabels, + } + + op, err := t.ComputeClient.Disks.SetLabels(projectId, zone, resIID.SystemId, req).Do() + + if err != nil { + return errRes, err + } + + if op.Error != nil { + return errRes, fmt.Errorf("operation failed: %v", op.Error.Errors) + } + + return tag, nil + case irs.CLUSTER: + cluster, err := t.getCluster(resIID) + if err != nil { + return errRes, err + } + + existLabels := cluster.ResourceLabels + existLabels[tag.Key] = tag.Value + + name := getParentClusterAtContainer(projectId, zone, resIID.SystemId) + req := &container.SetLabelsRequest{ + ClusterId: resIID.SystemId, + LabelFingerprint: cluster.LabelFingerprint, + Name: name, + ProjectId: projectId, + Zone: zone, + ResourceLabels: existLabels, + } + op, err := t.ContainerClient.Projects.Locations.Clusters.SetResourceLabels(name, req).Do() + + if err != nil { + return errRes, err + } + + if op.Error != nil { + return errRes, fmt.Errorf("operation failed: %v", op.Error.Message) + } + + return tag, nil + default: + return tag, errors.New("unsupported resource type") + } +} + +func (t *GCPTagHandler) waitForOperation(o *compute.Operation) error { + cnt := 10 + projectID := t.Credential.ProjectID + zone := t.Region.Zone + for cnt < 0 { + if strings.ToUpper(o.Status) == "DONE" { + if o.Error != nil { + return fmt.Errorf("operation failed: %v", o.Error.Errors) + } + return nil + } + + time.Sleep(2 * time.Second) + op, err := t.ComputeClient.ZoneOperations.Get(projectID, zone, o.Name).Do() + if err != nil { + return fmt.Errorf("failed to get operation status: %v", err) + } + cnt-- + o = op + } + + return errors.New("operation has not been finished.") +} + +func (t *GCPTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]KeyValue, error) { + err := validateSupportRS(resType) + res := []KeyValue{} + if err != nil { + return res, err + } + + projectID := t.Credential.ProjectID + zone := t.Region.Zone + switch resType { + case irs.VM: + vm, err := t.ComputeClient.Instances.Get(projectID, zone, resIID.SystemId).Do() + if err != nil { + return res, err + } + for k, v := range vm.Labels { + kv := KeyValue{ + Key: k, + Value: v, + } + res = append(res, kv) + } + return res, nil + case irs.DISK: + disk, err := GetDiskInfo(t.ComputeClient, t.Credential, t.Region, resIID.SystemId) + if err != nil { + return res, err + } + + for k, v := range disk.Labels { + kv := KeyValue{ + Key: k, + Value: v, + } + res = append(res, kv) + } + return res, nil + case irs.CLUSTER: + parent := getParentClusterAtContainer(projectID, zone, resIID.SystemId) + cluster, err := t.ContainerClient.Projects.Locations.Clusters.Get(parent).Do() + if err != nil { + return res, err + } + + for k, v := range cluster.ResourceLabels { + kv := KeyValue{ + Key: k, + Value: v, + } + res = append(res, kv) + } + return res, nil + default: + return res, errors.New("unsupport resources type") + } +} +func (t *GCPTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (KeyValue, error) { + labels, err := t.ListTag(resType, resIID) + res := KeyValue{} + if err != nil { + return res, err + } + + for _, l := range labels { + if l.Key == key { + res.Key = l.Key + res.Value = l.Value + return res, nil + } + } + + return res, nil +} +func (t *GCPTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string) (bool, error) { + err := validateSupportRS(resType) + if err != nil { + return false, err + } + + projectId := t.Credential.ProjectID + zone := t.Region.Zone + switch resType { + case irs.VM: + vm, err := t.getVm(resIID) + if err != nil { + return false, err + } + + existLabels := vm.Labels + if _, ok := existLabels[key]; ok { + delete(existLabels, key) + } + + req := &compute.InstancesSetLabelsRequest{ + LabelFingerprint: vm.Fingerprint, + Labels: existLabels, + } + + op, err := t.ComputeClient.Instances.SetLabels(projectId, zone, resIID.SystemId, req).Do() + + if err != nil { + return false, err + } + + if op.Error != nil { + return false, fmt.Errorf("operation failed: %v", op.Error.Errors) + } + + return true, nil + case irs.DISK: + + disk, err := t.getDisk(resIID) + if err != nil { + return false, err + } + + existLabels := disk.Labels + if _, ok := existLabels[key]; ok { + delete(existLabels, key) + } + req := &compute.ZoneSetLabelsRequest{ + LabelFingerprint: disk.LabelFingerprint, + Labels: existLabels, + } + + op, err := t.ComputeClient.Disks.SetLabels(projectId, zone, resIID.SystemId, req).Do() + + if err != nil { + return false, err + } + + if op.Error != nil { + return false, fmt.Errorf("operation failed: %v", op.Error.Errors) + } + + return true, nil + case irs.CLUSTER: + cluster, err := t.getCluster(resIID) + if err != nil { + return false, err + } + + existLabels := cluster.ResourceLabels + if _, ok := existLabels[key]; ok { + delete(existLabels, key) + } + + name := getParentClusterAtContainer(projectId, zone, resIID.SystemId) + req := &container.SetLabelsRequest{ + ClusterId: resIID.SystemId, + LabelFingerprint: cluster.LabelFingerprint, + Name: name, + ProjectId: projectId, + Zone: zone, + ResourceLabels: existLabels, + } + op, err := t.ContainerClient.Projects.Locations.Clusters.SetResourceLabels(name, req).Do() + + if err != nil { + return false, err + } + + if op.Error != nil { + return false, fmt.Errorf("operation failed: %v", op.Error.Message) + } + + return true, nil + default: + return false, errors.New("unsupported resource type") + } +} +func (t *GCPTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + // err := validateSupportRS(resType) + // errRes := []*irs.TagInfo{} + // if err != nil { + // return errRes, err + // } + + // projectId := t.Credential.ProjectID + // zone := t.Region.Zone + // switch resType { + // case irs.VM: + // vms, err := t.ComputeClient.Instances.List(projectId, zone).Do() + // if err != nil { + // return errRes, err + // } + + // for _, i := range vms.Items { + // irs.TagInfo{ + // ResType: resType, + // ResIId: irs.IID{ + // NameId: "", + // SystemId: "", + // }, + // } + // for k, v := range i.Labels { + // if strings.Contains(k, keyword) || strings.Contains(v, keyword) { + + // irs.KeyValue{ + + // } + + // } + // } + // } + + // case irs.DISK: + // disks, err := t.ComputeClient.Disks.List(projectId, zone).Do() + // if err != nil { + // return errRes, err + // } + + // case irs.CLUSTER: + // parent := getParentAtContainer(projectId, zone) + // clusters, err := t.ContainerClient.Projects.Locations.Clusters.List(parent).Do() + // if err != nil { + // return errRes, err + // } + + // default: + + // } + + return []*irs.TagInfo{}, nil +} From 29243fce605d87e10e1510e5d35b7c9edcb591a6 Mon Sep 17 00:00:00 2001 From: hippo-an Date: Mon, 15 Jul 2024 11:06:34 +0900 Subject: [PATCH 03/56] update test and execute test for vm and disk --- .../gcp/connect/GCP_CloudConnection.go | 12 +- .../drivers/gcp/main/Test_Resources.go | 141 +++++++++++++++++- .../drivers/gcp/main/conf/Test_Config.go | 4 +- .../drivers/gcp/resources/TagHandler.go | 49 ++++-- 4 files changed, 192 insertions(+), 14 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/connect/GCP_CloudConnection.go b/cloud-control-manager/cloud-driver/drivers/gcp/connect/GCP_CloudConnection.go index 3d47b877b..8528cad4b 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/connect/GCP_CloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/connect/GCP_CloudConnection.go @@ -169,5 +169,15 @@ func (cloudConn *GCPCloudConnection) CreatePriceInfoHandler() (irs.PriceInfoHand } func (cloudConn *GCPCloudConnection) CreateTagHandler() (irs.TagHandler, error) { - return nil, errors.New("GCP Cloud Driver: not implemented") + cblogger.Info("GCP Cloud Driver: called CreateTagHandler()!") + + tagHandler := gcprs.GCPTagHandler{ + Region: cloudConn.Region, + Ctx: cloudConn.Ctx, + Credential: cloudConn.Credential, + ComputeClient: cloudConn.VMClient, + ContainerClient: cloudConn.ContainerClient, + } + + return &tagHandler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go index 25754eb33..fbd3f2fa4 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go @@ -11,7 +11,11 @@ package main import ( + "bufio" + "errors" "fmt" + "os" + "strings" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" "github.com/davecgh/go-spew/spew" @@ -1979,6 +1983,140 @@ func handlePriceInfo() { } } } + +func handleTags() { + ResourceHandler, err := testconf.GetResourceHandler("Tag") + if err != nil { + panic(err) + } + handler := ResourceHandler.(irs.TagHandler) + + for { + fmt.Println("Tag Management") + fmt.Println("0. quit") + fmt.Println("1. list tags") + fmt.Println("2. add tags") + fmt.Println("3. remove tags") + fmt.Println("4. get tags") + + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + panic(err) + } + + reader := bufio.NewReader(os.Stdin) + + fmt.Println("resource type [v(m)/d(isk)/c(luster - temporary not usable)]: ") + key, err := reader.ReadString('\n') + + if err != nil { + panic(err) + } + key = strings.TrimSpace(key) + + var sampleId string + var sampleType irs.RSType + if strings.EqualFold(strings.ToLower(key), "v") { + sampleId = "2504669692882076487" + sampleType = irs.VM + } else if strings.EqualFold(strings.ToLower(key), "d") { + sampleId = "mcmp-demo" + sampleType = irs.DISK + } else { + fmt.Println(errors.New("chose vm or disk currently")) + continue + } + + if inputCnt == 1 { + switch commandNum { + case 0: + return + + case 1: + r, err := handler.ListTag(sampleType, irs.IID{NameId: sampleId, SystemId: sampleId}) + if err != nil { + fmt.Println("error while call list tag", err) + continue + } + + fmt.Printf("%+v\n", r) + case 2: + + reader := bufio.NewReader(os.Stdin) + + fmt.Println("key name : ") + key, err := reader.ReadString('\n') + + if err != nil { + panic(err) + } + key = strings.TrimSpace(key) + fmt.Println("value name : ") + val, err := reader.ReadString('\n') + + if err != nil { + panic(err) + } + val = strings.TrimSpace(val) + + r, err := handler.AddTag(sampleType, + irs.IID{NameId: sampleId, SystemId: sampleId}, + irs.KeyValue{Key: key, Value: val}) + if err != nil { + fmt.Println("error while call add tag", err) + continue + } + + fmt.Printf("%+v\n", r) + case 3: + + reader := bufio.NewReader(os.Stdin) + + fmt.Println("key name : ") + key, err := reader.ReadString('\n') + + if err != nil { + panic(err) + } + key = strings.TrimSpace(key) + + r, err := handler.RemoveTag(sampleType, + irs.IID{NameId: sampleId, SystemId: sampleId}, + key) + if err != nil { + fmt.Println("error while call remove tag", err) + continue + } + + fmt.Printf("%+v\n", r) + + case 4: + + reader := bufio.NewReader(os.Stdin) + + fmt.Println("key name : ") + key, err := reader.ReadString('\n') + + if err != nil { + panic(err) + } + key = strings.TrimSpace(key) + + r, err := handler.GetTag(sampleType, + irs.IID{NameId: sampleId, SystemId: sampleId}, + key) + if err != nil { + fmt.Println("error while call get tag", err) + continue + } + + fmt.Printf("%+v\n", r) + + } + } + } +} func main() { cblogger.Info("GCP Resource Test") //handleVPC() @@ -1991,7 +2129,8 @@ func main() { //handleDisk() //handleMyImage() //handleRegionZone() - handlePriceInfo() + // handlePriceInfo() //cblogger.Info(filepath.Join("a/b", "\\cloud-driver-libs\\.ssh-gcp\\")) //cblogger.Info(filepath.Join("\\cloud-driver-libs\\.ssh-gcp\\", "/b/c/d")) + handleTags() } diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/main/conf/Test_Config.go b/cloud-control-manager/cloud-driver/drivers/gcp/main/conf/Test_Config.go index dc40f1bed..60f172829 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/main/conf/Test_Config.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/main/conf/Test_Config.go @@ -57,7 +57,7 @@ func GetResourceHandler(handlerType string) (interface{}, error) { config, _ := readFileConfig(credentialFilePath) //region := "europe-west1" region := "asia-northeast3" - zone := "asia-northeast3-a" + zone := "asia-northeast3-c" connectionInfo := idrv.ConnectionInfo{ CredentialInfo: idrv.CredentialInfo{ @@ -104,6 +104,8 @@ func GetResourceHandler(handlerType string) (interface{}, error) { resourceHandler, err = cloudConnection.CreateRegionZoneHandler() case "PriceInfo": resourceHandler, err = cloudConnection.CreatePriceInfoHandler() + case "Tag": + resourceHandler, err = cloudConnection.CreateTagHandler() } if err != nil { diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go index d0b7a0b16..f4e9d4909 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go @@ -72,9 +72,9 @@ func (t *GCPTagHandler) getCluster(resIID irs.IID) (*container.Cluster, error) { return cluster, nil } -func (t *GCPTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag KeyValue) (KeyValue, error) { +func (t *GCPTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { err := validateSupportRS(resType) - errRes := KeyValue{} + errRes := irs.KeyValue{} if err != nil { return errRes, err } @@ -89,10 +89,13 @@ func (t *GCPTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag KeyValue) } existLabels := vm.Labels + if existLabels == nil { + existLabels = make(map[string]string) + } existLabels[tag.Key] = tag.Value req := &compute.InstancesSetLabelsRequest{ - LabelFingerprint: vm.Fingerprint, + LabelFingerprint: vm.LabelFingerprint, Labels: existLabels, } @@ -115,6 +118,9 @@ func (t *GCPTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag KeyValue) } existLabels := disk.Labels + if existLabels == nil { + existLabels = make(map[string]string) + } existLabels[tag.Key] = tag.Value req := &compute.ZoneSetLabelsRequest{ @@ -140,6 +146,9 @@ func (t *GCPTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag KeyValue) } existLabels := cluster.ResourceLabels + if existLabels == nil { + existLabels = make(map[string]string) + } existLabels[tag.Key] = tag.Value name := getParentClusterAtContainer(projectId, zone, resIID.SystemId) @@ -191,9 +200,9 @@ func (t *GCPTagHandler) waitForOperation(o *compute.Operation) error { return errors.New("operation has not been finished.") } -func (t *GCPTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]KeyValue, error) { +func (t *GCPTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]irs.KeyValue, error) { err := validateSupportRS(resType) - res := []KeyValue{} + res := []irs.KeyValue{} if err != nil { return res, err } @@ -207,7 +216,7 @@ func (t *GCPTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]KeyValue, return res, err } for k, v := range vm.Labels { - kv := KeyValue{ + kv := irs.KeyValue{ Key: k, Value: v, } @@ -221,7 +230,7 @@ func (t *GCPTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]KeyValue, } for k, v := range disk.Labels { - kv := KeyValue{ + kv := irs.KeyValue{ Key: k, Value: v, } @@ -236,7 +245,7 @@ func (t *GCPTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]KeyValue, } for k, v := range cluster.ResourceLabels { - kv := KeyValue{ + kv := irs.KeyValue{ Key: k, Value: v, } @@ -247,9 +256,9 @@ func (t *GCPTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]KeyValue, return res, errors.New("unsupport resources type") } } -func (t *GCPTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (KeyValue, error) { +func (t *GCPTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (irs.KeyValue, error) { labels, err := t.ListTag(resType, resIID) - res := KeyValue{} + res := irs.KeyValue{} if err != nil { return res, err } @@ -280,12 +289,17 @@ func (t *GCPTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string } existLabels := vm.Labels + if existLabels == nil { + return false, errors.New("key does not exist") + } if _, ok := existLabels[key]; ok { delete(existLabels, key) + } else { + return false, errors.New("key does not exist") } req := &compute.InstancesSetLabelsRequest{ - LabelFingerprint: vm.Fingerprint, + LabelFingerprint: vm.LabelFingerprint, Labels: existLabels, } @@ -308,8 +322,15 @@ func (t *GCPTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string } existLabels := disk.Labels + + if existLabels == nil { + return false, errors.New("key does not exist") + } + if _, ok := existLabels[key]; ok { delete(existLabels, key) + } else { + return false, errors.New("key does not exist") } req := &compute.ZoneSetLabelsRequest{ LabelFingerprint: disk.LabelFingerprint, @@ -334,8 +355,14 @@ func (t *GCPTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string } existLabels := cluster.ResourceLabels + if existLabels == nil { + return false, errors.New("key does not exist") + } + if _, ok := existLabels[key]; ok { delete(existLabels, key) + } else { + return false, errors.New("key does not exist") } name := getParentClusterAtContainer(projectId, zone, resIID.SystemId) From 916dafdc2a04c1c76bb158b9aa07149974d07e7a Mon Sep 17 00:00:00 2001 From: hippo-an Date: Mon, 15 Jul 2024 11:08:15 +0900 Subject: [PATCH 04/56] create vm, disk, cluster with label and query resources with labels --- .../drivers/gcp/resources/ClusterHandler.go | 25 +++++++++++++------ .../drivers/gcp/resources/DiskHandler.go | 16 +++++++++++- .../drivers/gcp/resources/VMHandler.go | 24 +++++++++++++++--- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/gcp/resources/ClusterHandler.go index fed3ee954..c4b29cb78 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/resources/ClusterHandler.go @@ -72,15 +72,19 @@ func (ClusterHandler *GCPClusterHandler) CreateCluster(clusterReqInfo irs.Cluste //projects/csta-349809/locations/asia-northeast3-a // Meta정보에 securityGroup 정보를 Key,Val 형태로 넣고 실제 값(val)은 nodeConfig 에 set하여 사용 - securityGroupMap := make(map[string]string) + labels := make(map[string]string) var sgTags []string if clusterReqInfo.Network.SecurityGroupIIDs != nil && len(clusterReqInfo.Network.SecurityGroupIIDs) > 0 { for idx, securityGroupIID := range clusterReqInfo.Network.SecurityGroupIIDs { - securityGroupMap[GCP_PMKS_SECURITYGROUP_TAG+strconv.Itoa(idx)] = securityGroupIID.NameId + labels[GCP_PMKS_SECURITYGROUP_TAG+strconv.Itoa(idx)] = securityGroupIID.NameId sgTags = append(sgTags, securityGroupIID.NameId) } } + for _, t := range clusterReqInfo.TagList { + labels[t.Key] = t.Value + } + reqCluster := container.Cluster{} reqCluster.Name = clusterReqInfo.IId.NameId @@ -96,7 +100,7 @@ func (ClusterHandler *GCPClusterHandler) CreateCluster(clusterReqInfo irs.Cluste rb := &container.CreateClusterRequest{} rb.Cluster = &reqCluster - rb.Cluster.ResourceLabels = securityGroupMap + rb.Cluster.ResourceLabels = labels // nodeGroup List set nodePools := []*container.NodePool{} @@ -761,12 +765,19 @@ func mappingClusterInfo(cluster *container.Cluster) (ClusterInfo irs.ClusterInfo // 3. Network NetworkInfo securityGroups := []irs.IID{} // SecurityGroup으로 정의된 Label추출 var metaSecurityGroupTags []string - for resourceKey, resourceVal := range cluster.ResourceLabels { - if strings.HasPrefix(resourceKey, GCP_PMKS_SECURITYGROUP_TAG) { - //securityGroups = append(securityGroups, irs.IID{NameId: resourceVal, SystemId: resourceVal}) - metaSecurityGroupTags = append(metaSecurityGroupTags, resourceVal) + + tags := make([]irs.KeyValue, 0) + if cluster.ResourceLabels != nil { + for resourceKey, resourceVal := range cluster.ResourceLabels { + if strings.HasPrefix(resourceKey, GCP_PMKS_SECURITYGROUP_TAG) { + //securityGroups = append(securityGroups, irs.IID{NameId: resourceVal, SystemId: resourceVal}) + metaSecurityGroupTags = append(metaSecurityGroupTags, resourceVal) + } + tags = append(tags, irs.KeyValue{Key: resourceKey, Value: resourceVal}) } } + clusterInfo.TagList = tags + cblogger.Info("metaSecurityGroupTags : ", metaSecurityGroupTags) // NodeConfig의 Tag가 SecurityGroup으로 사용하는 Tag인지 알려면 // Metadata에 Label이 정의되어있는지 여부로 확인 diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/gcp/resources/DiskHandler.go index 1938c6372..ffcf3cff5 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/resources/DiskHandler.go @@ -47,8 +47,14 @@ func (DiskHandler *GCPDiskHandler) CreateDisk(diskReqInfo irs.DiskInfo) (irs.Dis cblogger.Info("SetDisk zone after ", DiskHandler.Region) } + labels := make(map[string]string) + + for _, t := range diskReqInfo.TagList { + labels[t.Key] = t.Value + } disk := &compute.Disk{ - Name: diskName, + Name: diskName, + Labels: labels, } if diskReqInfo.DiskType != "" && diskReqInfo.DiskType != "default" { @@ -524,5 +530,13 @@ func convertDiskInfo(diskResp *compute.Disk) (irs.DiskInfo, error) { diskInfo.Status = diskStatus + tags := make([]irs.KeyValue, 0) + if diskResp.Labels != nil { + for k, v := range diskResp.Labels { + tags = append(tags, irs.KeyValue{Key: k, Value: v}) + } + } + diskInfo.TagList = tags + return diskInfo, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/gcp/resources/VMHandler.go index 3ffce3be7..690fb4afd 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/resources/VMHandler.go @@ -244,6 +244,17 @@ func (vmHandler *GCPVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, err cblogger.Info("networkURL 정보 : ", networkURL) cblogger.Info("subnetWorkURL 정보 : ", subnetWorkURL) + labels := map[string]string{ + "keypair": strings.ToLower(vmReqInfo.KeyPairIID.SystemId), + } + + for _, t := range vmReqInfo.TagList { + if t.Key == "keypair" { + continue + } + labels[t.Key] = t.Value + } + instance := &compute.Instance{ Name: vmName, Metadata: &compute.Metadata{ @@ -252,10 +263,7 @@ func (vmHandler *GCPVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, err Value: &pubKey}, }, }, - Labels: map[string]string{ - //"keypair": strings.ToLower(vmReqInfo.KeyPairIID.NameId), - "keypair": strings.ToLower(vmReqInfo.KeyPairIID.SystemId), - }, + Labels: labels, Description: "compute sample instance", MachineType: prefix + "/zones/" + zone + "/machineTypes/" + vmReqInfo.VMSpecName, Disks: []*compute.AttachedDisk{ @@ -1191,6 +1199,14 @@ func (vmHandler *GCPVMHandler) mappingServerInfo(server *compute.Instance) irs.V } } + tags := make([]irs.KeyValue, 0) + if server.Labels != nil { + for k, v := range server.Labels { + tags = append(tags, irs.KeyValue{Key: k, Value: v}) + } + } + vmInfo.TagList = tags + return vmInfo } From 160f94cf1a9e4eb2a8e4a74ab1e602570e2d8a7d Mon Sep 17 00:00:00 2001 From: dev4unet Date: Mon, 15 Jul 2024 08:35:43 +0000 Subject: [PATCH 05/56] Converting Korean output string to English --- .../drivers/aws/main/Test_Resources.go | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go index 47dd39da7..6f79a1a2d 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go @@ -78,9 +78,9 @@ func handleSecurity() { case 1: result, err := handler.ListSecurity() if err != nil { - cblogger.Infof(" Security 목록 조회 실패 : ", err) + cblogger.Infof(" Security List Lookup Failed : ", err) } else { - cblogger.Info("Security 목록 조회 결과") + cblogger.Info("Security List Lookup Result") cblogger.Info(result) if result != nil { @@ -401,9 +401,9 @@ func handlePublicIP() { fmt.Println("Start ListPublicIP() ...") result, err := handler.ListPublicIP() if err != nil { - cblogger.Error("PublicIP 목록 조회 실패 : ", err) + cblogger.Error("PublicIP List Lookup Failed : ", err) } else { - cblogger.Info("PublicIP 목록 조회 결과") + cblogger.Info("PublicIP List Lookup Result") spew.Dump(result) } @@ -491,9 +491,9 @@ func handleKeyPair() { case 1: result, err := KeyPairHandler.ListKey() if err != nil { - cblogger.Infof(" 키 페어 목록 조회 실패 : ", err) + cblogger.Infof(" 키 페어 List Lookup Failed : ", err) } else { - cblogger.Info("키 페어 목록 조회 결과") + cblogger.Info("키 페어 List Lookup Result") //cblogger.Info(result) spew.Dump(result) } @@ -575,9 +575,9 @@ func handleVNetwork() { case 1: result, err := VPCHandler.ListVNetwork() if err != nil { - cblogger.Infof(" VNetwork 목록 조회 실패 : ", err) + cblogger.Infof(" VNetwork List Lookup Failed : ", err) } else { - cblogger.Info("VNetwork 목록 조회 결과") + cblogger.Info("VNetwork List Lookup Result") //cblogger.Info(result) spew.Dump(result) @@ -690,9 +690,9 @@ func handleVPC() { case 1: result, err := VPCHandler.ListVPC() if err != nil { - cblogger.Infof(" VNetwork 목록 조회 실패 : ", err) + cblogger.Infof(" VNetwork List Lookup Failed : ", err) } else { - cblogger.Info("VNetwork 목록 조회 결과") + cblogger.Info("VNetwork List Lookup Result") //cblogger.Info(result) spew.Dump(result) @@ -798,11 +798,11 @@ func handleImage() { case 1: result, err := handler.ListImage() if err != nil { - cblogger.Infof(" Image 목록 조회 실패 : ", err) + cblogger.Infof(" Image List Lookup Failed : ", err) } else { - cblogger.Info("Image 목록 조회 결과") + cblogger.Info("Image List Lookup Result") cblogger.Debug(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) cblogger.Info("출력 결과 수 : ", len(result)) @@ -887,9 +887,9 @@ func handleVNic() { case 1: result, err := handler.ListVNic() if err != nil { - cblogger.Infof(" VNic 목록 조회 실패 : ", err) + cblogger.Infof(" VNic List Lookup Failed : ", err) } else { - cblogger.Info("VNic 목록 조회 결과") + cblogger.Info("VNic List Lookup Result") spew.Dump(result) if len(result) > 0 { reqVnicID = result[0].Id // 조회 및 삭제 편의를 위해 목록의 첫번째 ID로 변경 @@ -1153,9 +1153,9 @@ func handleVMSpec() { fmt.Println("Start ListVMSpec() ...") result, err := handler.ListVMSpec() if err != nil { - cblogger.Error("VMSpec 목록 조회 실패 : ", err) + cblogger.Error("VMSpec List Lookup Failed : ", err) } else { - cblogger.Debug("VMSpec 목록 조회 결과") + cblogger.Debug("VMSpec List Lookup Result") //spew.Dump(result) cblogger.Debug(result) cblogger.Infof("전체 목록 개수 : [%d]", len(result)) @@ -1179,9 +1179,9 @@ func handleVMSpec() { fmt.Println("Start ListOrgVMSpec() ...") result, err := handler.ListOrgVMSpec() if err != nil { - cblogger.Error("VMSpec Org 목록 조회 실패 : ", err) + cblogger.Error("VMSpec Org List Lookup Failed : ", err) } else { - cblogger.Debug("VMSpec Org 목록 조회 결과") + cblogger.Debug("VMSpec Org List Lookup Result") //spew.Dump(result) cblogger.Debug(result) //spew.Dump(result) @@ -1283,11 +1283,11 @@ func handleNLB() { case 1: result, err := handler.ListNLB() if err != nil { - cblogger.Infof(" NLB 목록 조회 실패 : ", err) + cblogger.Infof(" NLB List Lookup Failed : ", err) } else { - cblogger.Info("NLB 목록 조회 결과") + cblogger.Info("NLB List Lookup Result") cblogger.Debug(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) cblogger.Info("출력 결과 수 : ", len(result)) @@ -1497,11 +1497,11 @@ func handleCluster() { case 1: result, err := handler.ListCluster() if err != nil { - cblogger.Infof(" Cluster 목록 조회 실패 : ", err) + cblogger.Infof(" Cluster List Lookup Failed : ", err) } else { - cblogger.Info("Cluster 목록 조회 결과") + cblogger.Info("Cluster List Lookup Result") cblogger.Debug(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) cblogger.Info("출력 결과 수 : ", len(result)) @@ -1657,12 +1657,12 @@ func handleRegionZone() { case 1: result, err := handler.ListRegionZone() if err != nil { - cblogger.Infof("ListRegionZone 목록 조회 실패 : %s", err) + cblogger.Infof("ListRegionZone List Lookup Failed : %s", err) } else { - cblogger.Info("ListRegionZone 목록 조회 결과") + cblogger.Info("ListRegionZone List Lookup Result") // cblogger.Debugf("결과 %s", result[0]) spew.Dump(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) cblogger.Info("출력 결과 수 : ", len(result)) } @@ -1674,29 +1674,29 @@ func handleRegionZone() { } else { cblogger.Info("GetRegionZone 조회 결과") cblogger.Debug(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) // spew.Dump(result) } case 3: result, err := handler.ListOrgRegion() if err != nil { - cblogger.Infof("ListOrgRegion 목록 조회 실패 : ", err) + cblogger.Infof("ListOrgRegion List Lookup Failed : ", err) } else { - cblogger.Info("ListOrgRegion 목록 조회 결과") + cblogger.Info("ListOrgRegion List Lookup Result") cblogger.Debug(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) cblogger.Info("출력 결과 수 : ", len(result)) } case 4: result, err := handler.ListOrgZone() if err != nil { - cblogger.Infof("ListOrgZone 목록 조회 실패 : %s", err) + cblogger.Infof("ListOrgZone List Lookup Failed : %s", err) } else { - cblogger.Info("ListOrgZone 목록 조회 결과") + cblogger.Info("ListOrgZone List Lookup Result") cblogger.Debug(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) cblogger.Info("출력 결과 수 : ", len(result)) } @@ -1736,12 +1736,12 @@ func handlePriceInfo() { case 1: result, err := handler.ListProductFamily("us-west-1") if err != nil { - cblogger.Infof("ListProductFamily 목록 조회 실패 : %s", err) + cblogger.Infof("ListProductFamily List Lookup Failed : %s", err) } else { - cblogger.Info("ListProductFamily 목록 조회 결과") + cblogger.Info("ListProductFamily List Lookup Result") // cblogger.Debugf("결과 %s", result[0]) spew.Dump(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) cblogger.Info("출력 결과 수 : ", len(result)) } @@ -1770,7 +1770,7 @@ func handlePriceInfo() { } else { cblogger.Info("GetPriceInfo 조회 결과") cblogger.Info(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) } } } @@ -1815,18 +1815,18 @@ func handleTag() { return case 1: - cblogger.Infof("조회 요청 태그 타입 : [%s]", reqType) + cblogger.Infof("Lookup request tag type : [%s]", reqType) if reqType == irs.VM { - cblogger.Debug("VM 요청됨") + cblogger.Debug("VM Requested") } result, err := handler.ListTag(reqType, reqIID) if err != nil { - cblogger.Info(" Tag 목록 조회 실패 : ", err) + cblogger.Info(" Tag List Lookup Failed : ", err) } else { - cblogger.Info("Tag 목록 조회 결과") + cblogger.Info("Tag List Lookup Result") cblogger.Debug(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) cblogger.Info("출력 결과 수 : ", len(result)) From 8bcbad2210e4f2898bc7251016e8e621bb76b4ec Mon Sep 17 00:00:00 2001 From: dev4unet Date: Mon, 15 Jul 2024 09:29:43 +0000 Subject: [PATCH 06/56] Edit Korean messages to English --- .../drivers/aws/main/Test_Resources.go | 383 +++++++++--------- 1 file changed, 191 insertions(+), 192 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go index 6f79a1a2d..798186302 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go @@ -89,7 +89,7 @@ func handleSecurity() { } case 2: - cblogger.Infof("[%s] Security 생성 테스트", securityName) + cblogger.Infof("[%s] Security Create Test", securityName) securityReqInfo := irs.SecurityReqInfo{ IId: irs.IID{NameId: securityName}, @@ -170,34 +170,34 @@ func handleSecurity() { result, err := handler.CreateSecurity(securityReqInfo) if err != nil { - cblogger.Infof(securityName, " Security 생성 실패 : ", err) + cblogger.Infof(securityName, " Security Create Failed : ", err) } else { - cblogger.Infof("[%s] Security 생성 결과 : [%v]", securityName, result) + cblogger.Infof("[%s] Security Create Result : [%v]", securityName, result) securityId = result.IId.SystemId spew.Dump(result) } case 3: - cblogger.Infof("[%s] Security 조회 테스트", securityId) + cblogger.Infof("[%s] Security Lookup Test", securityId) result, err := handler.GetSecurity(irs.IID{SystemId: securityId}) if err != nil { - cblogger.Infof(securityId, " Security 조회 실패 : ", err) + cblogger.Infof(securityId, " Security Lookup Failed : ", err) } else { - cblogger.Infof("[%s] Security 조회 결과 : [%v]", securityId, result) + cblogger.Infof("[%s] Security Lookup Result : [%v]", securityId, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] Security 삭제 테스트", securityId) + cblogger.Infof("[%s] Security Delete Test", securityId) result, err := handler.DeleteSecurity(irs.IID{SystemId: securityId}) if err != nil { - cblogger.Infof(securityId, " Security 삭제 실패 : ", err) + cblogger.Infof(securityId, " Security Delete Failed : ", err) } else { - cblogger.Infof("[%s] Security 삭제 결과 : [%s]", securityId, result) + cblogger.Infof("[%s] Security Delete Result : [%s]", securityId, result) } case 5: - cblogger.Infof("[%s] Security 그룹 룰 추가 테스트", securityId) + cblogger.Infof("[%s] Security Group Rule - Add Test", securityId) result, err := handler.AddRules(irs.IID{SystemId: securityId}, &[]irs.SecurityRuleInfo{ { FromPort: "80", @@ -229,13 +229,13 @@ func handleSecurity() { }, }) if err != nil { - cblogger.Infof(securityId, " Security 그룹 룰 추가 실패 : ", err) + cblogger.Infof(securityId, " Security Group Rule - Add Failed : ", err) } else { - cblogger.Infof("[%s] Security 그룹 룰 추가 결과 : [%s]", securityId, result) + cblogger.Infof("[%s] Security Group Rule - Add Result : [%s]", securityId, result) } case 6: - cblogger.Infof("[%s] Security 그룹 룰 제거 테스트", securityId) + cblogger.Infof("[%s] Security Group Rule - Delete Test", securityId) result, err := handler.RemoveRules(irs.IID{SystemId: securityId}, &[]irs.SecurityRuleInfo{ { FromPort: "80", @@ -267,9 +267,9 @@ func handleSecurity() { }, }) if err != nil { - cblogger.Infof(securityId, " Security 그룹 룰 제거 실패 : ", err) + cblogger.Infof(securityId, " Security Group Rule - Delete Failed : ", err) } else { - cblogger.Infof("[%s] Security 그룹 룰 제거 결과 : [%s]", securityId, result) + cblogger.Infof("[%s] Security Group Rule - Delete Result : [%s]", securityId, result) } } } @@ -348,9 +348,9 @@ func handleSecurityOld() { //result, err := handler.CreateSecurity(securityReqInfo) if err != nil { - cblogger.Infof("보안 그룹 조회 실패 : ", err) + cblogger.Infof("Security Group Lookup Failed : ", err) } else { - cblogger.Info("보안 그룹 조회 결과") + cblogger.Info("Security Group Lookup Result") //cblogger.Info(result) spew.Dump(result) } @@ -413,9 +413,9 @@ func handlePublicIP() { fmt.Println("Start GetPublicIP() ...") result, err := handler.GetPublicIP(reqPublicIP) if err != nil { - cblogger.Error(reqPublicIP, " PublicIP 정보 조회 실패 : ", err) + cblogger.Error(reqPublicIP, " PublicIP 정보 Lookup Failed : ", err) } else { - cblogger.Infof("PublicIP[%s] 정보 조회 결과", reqPublicIP) + cblogger.Infof("PublicIP[%s] 정보 Lookup Result", reqPublicIP) spew.Dump(result) } fmt.Println("Finish GetPublicIP()") @@ -425,9 +425,9 @@ func handlePublicIP() { reqInfo := irs.PublicIPReqInfo{Name: "mcloud-barista-eip-test"} result, err := handler.CreatePublicIP(reqInfo) if err != nil { - cblogger.Error("PublicIP 생성 실패 : ", err) + cblogger.Error("PublicIP Create Failed : ", err) } else { - cblogger.Info("PublicIP 생성 성공 ", result) + cblogger.Info("PublicIP 생성 Success ", result) spew.Dump(result) } fmt.Println("Finish CreatePublicIP()") @@ -436,12 +436,12 @@ func handlePublicIP() { fmt.Println("Start DeletePublicIP() ...") result, err := handler.DeletePublicIP(reqPublicIP) if err != nil { - cblogger.Error(reqDelIP, " PublicIP 삭제 실패 : ", err) + cblogger.Error(reqDelIP, " PublicIP Delete Failed : ", err) } else { if result { cblogger.Infof("PublicIP[%s] 삭제 완료", reqDelIP) } else { - cblogger.Errorf("PublicIP[%s] 삭제 실패", reqDelIP) + cblogger.Errorf("PublicIP[%s] Delete Failed", reqDelIP) } } fmt.Println("Finish DeletePublicIP()") @@ -491,41 +491,41 @@ func handleKeyPair() { case 1: result, err := KeyPairHandler.ListKey() if err != nil { - cblogger.Infof(" 키 페어 List Lookup Failed : ", err) + cblogger.Infof(" KeyPair List Lookup Failed : ", err) } else { - cblogger.Info("키 페어 List Lookup Result") + cblogger.Info("KeyPair List Lookup Result") //cblogger.Info(result) spew.Dump(result) } case 2: - cblogger.Infof("[%s] 키 페어 생성 테스트", keyPairName) + cblogger.Infof("[%s] KeyPair Create Test", keyPairName) keyPairReqInfo := irs.KeyPairReqInfo{ IId: irs.IID{NameId: keyPairName}, //Name: keyPairName, } result, err := KeyPairHandler.CreateKey(keyPairReqInfo) if err != nil { - cblogger.Infof(keyPairName, " 키 페어 생성 실패 : ", err) + cblogger.Infof(keyPairName, " KeyPair Create Failed : ", err) } else { - cblogger.Infof("[%s] 키 페어 생성 결과 : [%s]", keyPairName, result) + cblogger.Infof("[%s] KeyPair Create Result : [%s]", keyPairName, result) spew.Dump(result) } case 3: - cblogger.Infof("[%s] 키 페어 조회 테스트", keyPairName) + cblogger.Infof("[%s] KeyPair Lookup Test", keyPairName) result, err := KeyPairHandler.GetKey(irs.IID{SystemId: keyPairName}) if err != nil { - cblogger.Infof(keyPairName, " 키 페어 조회 실패 : ", err) + cblogger.Infof(keyPairName, " KeyPair Lookup Failed : ", err) } else { - cblogger.Infof("[%s] 키 페어 조회 결과 : [%s]", keyPairName, result) + cblogger.Infof("[%s] KeyPair Lookup Result : [%s]", keyPairName, result) } case 4: - cblogger.Infof("[%s] 키 페어 삭제 테스트", keyPairName) + cblogger.Infof("[%s] KeyPair Delete Test", keyPairName) result, err := KeyPairHandler.DeleteKey(irs.IID{SystemId: keyPairName}) if err != nil { - cblogger.Infof(keyPairName, " 키 페어 삭제 실패 : ", err) + cblogger.Infof(keyPairName, " KeyPair Delete Failed : ", err) } else { - cblogger.Infof("[%s] 키 페어 삭제 결과 : [%s]", keyPairName, result) + cblogger.Infof("[%s] KeyPair Delete Result : [%s]", keyPairName, result) } } } @@ -582,41 +582,41 @@ func handleVNetwork() { spew.Dump(result) // 내부적으로 1개만 존재함. - //조회및 삭제 테스트를 위해 리스트의 첫번째 서브넷 ID를 요청ID로 자동 갱신함. + //조회및 Delete Test를 위해 리스트의 첫번째 서브넷 ID를 요청ID로 자동 갱신함. if result != nil { reqSubnetId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 } } case 2: - cblogger.Infof("[%s] VNetwork 생성 테스트", vNetworkReqInfo.IId.NameId) + cblogger.Infof("[%s] VNetwork Create Test", vNetworkReqInfo.IId.NameId) //vNetworkReqInfo := irs.VNetworkReqInfo{} result, err := VPCHandler.CreateVNetwork(vNetworkReqInfo) if err != nil { - cblogger.Infof(reqSubnetId.NameId, " VNetwork 생성 실패 : ", err) + cblogger.Infof(reqSubnetId.NameId, " VNetwork Create Failed : ", err) } else { - cblogger.Infof("VNetwork 생성 결과 : ", result) + cblogger.Infof("VNetwork Create Result : ", result) reqSubnetId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 spew.Dump(result) } case 3: - cblogger.Infof("[%s] VNetwork 조회 테스트", reqSubnetId) + cblogger.Infof("[%s] VNetwork Lookup Test", reqSubnetId) result, err := VPCHandler.GetVNetwork(reqSubnetId) if err != nil { - cblogger.Infof("[%s] VNetwork 조회 실패 : ", reqSubnetId, err) + cblogger.Infof("[%s] VNetwork Lookup Failed : ", reqSubnetId, err) } else { - cblogger.Infof("[%s] VNetwork 조회 결과 : [%s]", reqSubnetId, result) + cblogger.Infof("[%s] VNetwork Lookup Result : [%s]", reqSubnetId, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] VNetwork 삭제 테스트", reqSubnetId) + cblogger.Infof("[%s] VNetwork Delete Test", reqSubnetId) result, err := VPCHandler.DeleteVNetwork(reqSubnetId) if err != nil { - cblogger.Infof("[%s] VNetwork 삭제 실패 : ", reqSubnetId, err) + cblogger.Infof("[%s] VNetwork Delete Failed : ", reqSubnetId, err) } else { - cblogger.Infof("[%s] VNetwork 삭제 결과 : [%s]", reqSubnetId, result) + cblogger.Infof("[%s] VNetwork Delete Result : [%s]", reqSubnetId, result) } } } @@ -697,62 +697,62 @@ func handleVPC() { spew.Dump(result) // 내부적으로 1개만 존재함. - //조회및 삭제 테스트를 위해 리스트의 첫번째 서브넷 ID를 요청ID로 자동 갱신함. + //조회및 Delete Test를 위해 리스트의 첫번째 서브넷 ID를 요청ID로 자동 갱신함. if result != nil { reqSubnetId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 - subnetReqVpcInfo = reqSubnetId //Subnet 추가/삭제 테스트용 + subnetReqVpcInfo = reqSubnetId //Subnet 추가/Delete Test용 } } case 2: - cblogger.Infof("[%s] VNetwork 생성 테스트", vpcReqInfo.IId.NameId) + cblogger.Infof("[%s] VNetwork Create Test", vpcReqInfo.IId.NameId) //vpcReqInfo := irs.VPCReqInfo{} result, err := VPCHandler.CreateVPC(vpcReqInfo) if err != nil { - cblogger.Infof(reqSubnetId.NameId, " VNetwork 생성 실패 : ", err) + cblogger.Infof(reqSubnetId.NameId, " VNetwork Create Failed : ", err) } else { - cblogger.Infof("VNetwork 생성 결과 : ", result) + cblogger.Infof("VNetwork Create Result : ", result) reqSubnetId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 spew.Dump(result) } case 3: - cblogger.Infof("[%s] VNetwork 조회 테스트", reqSubnetId) + cblogger.Infof("[%s] VNetwork Lookup Test", reqSubnetId) result, err := VPCHandler.GetVPC(reqSubnetId) if err != nil { - cblogger.Infof("[%s] VNetwork 조회 실패 : ", reqSubnetId, err) + cblogger.Infof("[%s] VNetwork Lookup Failed : ", reqSubnetId, err) } else { - cblogger.Infof("[%s] VNetwork 조회 결과 : [%s]", reqSubnetId, result) + cblogger.Infof("[%s] VNetwork Lookup Result : [%s]", reqSubnetId, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] VNetwork 삭제 테스트", reqSubnetId) + cblogger.Infof("[%s] VNetwork Delete Test", reqSubnetId) result, err := VPCHandler.DeleteVPC(reqSubnetId) if err != nil { - cblogger.Infof("[%s] VNetwork 삭제 실패 : ", reqSubnetId, err) + cblogger.Infof("[%s] VNetwork Delete Failed : ", reqSubnetId, err) } else { - cblogger.Infof("[%s] VNetwork 삭제 결과 : [%s]", reqSubnetId, result) + cblogger.Infof("[%s] VNetwork Delete Result : [%s]", reqSubnetId, result) } case 5: - cblogger.Infof("[%s] Subnet 추가 테스트", vpcReqInfo.IId.NameId) + cblogger.Infof("[%s] Subnet Add Test", vpcReqInfo.IId.NameId) result, err := VPCHandler.AddSubnet(subnetReqVpcInfo, subnetReqInfo) if err != nil { - cblogger.Infof(reqSubnetId.NameId, " VNetwork 생성 실패 : ", err) + cblogger.Infof(reqSubnetId.NameId, " VNetwork Create Failed : ", err) } else { - cblogger.Infof("VNetwork 생성 결과 : ", result) + cblogger.Infof("VNetwork Create Result : ", result) //reqSubnetId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 spew.Dump(result) } case 6: - cblogger.Infof("[%s] Subnet 삭제 테스트", reqSubnetId.SystemId) + cblogger.Infof("[%s] Subnet Delete Test", reqSubnetId.SystemId) result, err := VPCHandler.RemoveSubnet(subnetReqVpcInfo, reqSubnetId) if err != nil { - cblogger.Infof("[%s] Subnet 삭제 실패 : ", reqSubnetId.SystemId, err) + cblogger.Infof("[%s] Subnet Delete Failed : ", reqSubnetId.SystemId, err) } else { - cblogger.Infof("[%s] Subnet 삭제 결과 : [%s]", reqSubnetId.SystemId, result) + cblogger.Infof("[%s] Subnet Delete Result : [%s]", reqSubnetId.SystemId, result) } } } @@ -804,42 +804,42 @@ func handleImage() { cblogger.Debug(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) - //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. + //조회및 Delete Test를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. if result != nil { imageReqInfo.IId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 } } case 2: - cblogger.Infof("[%s] Image 생성 테스트", imageReqInfo.IId.NameId) + cblogger.Infof("[%s] Image Create Test", imageReqInfo.IId.NameId) result, err := handler.CreateImage(imageReqInfo) if err != nil { - cblogger.Infof(imageReqInfo.IId.NameId, " Image 생성 실패 : ", err) + cblogger.Infof(imageReqInfo.IId.NameId, " Image Create Failed : ", err) } else { - cblogger.Infof("Image 생성 결과 : ", result) + cblogger.Infof("Image Create Result : ", result) imageReqInfo.IId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 spew.Dump(result) } case 3: - cblogger.Infof("[%s] Image 조회 테스트", imageReqInfo.IId) + cblogger.Infof("[%s] Image Lookup Test", imageReqInfo.IId) result, err := handler.GetImage(imageReqInfo.IId) if err != nil { - cblogger.Infof("[%s] Image 조회 실패 : ", imageReqInfo.IId.NameId, err) + cblogger.Infof("[%s] Image Lookup Failed : ", imageReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] Image 조회 결과 : [%s]", imageReqInfo.IId.NameId, result) + cblogger.Infof("[%s] Image Lookup Result : [%s]", imageReqInfo.IId.NameId, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] Image 삭제 테스트", imageReqInfo.IId.NameId) + cblogger.Infof("[%s] Image Delete Test", imageReqInfo.IId.NameId) result, err := handler.DeleteImage(imageReqInfo.IId) if err != nil { - cblogger.Infof("[%s] Image 삭제 실패 : ", imageReqInfo.IId.NameId, err) + cblogger.Infof("[%s] Image Delete Failed : ", imageReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] Image 삭제 결과 : [%s]", imageReqInfo.IId.NameId, result) + cblogger.Infof("[%s] Image Delete Result : [%s]", imageReqInfo.IId.NameId, result) } } } @@ -897,33 +897,33 @@ func handleVNic() { } case 2: - cblogger.Infof("[%s] VNic 생성 테스트", vNicReqInfo.Name) + cblogger.Infof("[%s] VNic Create Test", vNicReqInfo.Name) result, err := handler.CreateVNic(vNicReqInfo) if err != nil { - cblogger.Infof(reqVnicID, " VNic 생성 실패 : ", err) + cblogger.Infof(reqVnicID, " VNic Create Failed : ", err) } else { - cblogger.Infof("VNic 생성 결과 : ", result) + cblogger.Infof("VNic Create Result : ", result) reqVnicID = result.Id // 조회 및 삭제를 위해 생성된 ID로 변경 spew.Dump(result) } case 3: - cblogger.Infof("[%s] VNic 조회 테스트", reqVnicID) + cblogger.Infof("[%s] VNic Lookup Test", reqVnicID) result, err := handler.GetVNic(reqVnicID) if err != nil { - cblogger.Infof("[%s] VNic 조회 실패 : ", reqVnicID, err) + cblogger.Infof("[%s] VNic Lookup Failed : ", reqVnicID, err) } else { - cblogger.Infof("[%s] VNic 조회 결과 : [%s]", reqVnicID, result) + cblogger.Infof("[%s] VNic Lookup Result : [%s]", reqVnicID, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] VNic 삭제 테스트", reqVnicID) + cblogger.Infof("[%s] VNic Delete Test", reqVnicID) result, err := handler.DeleteVNic(reqVnicID) if err != nil { - cblogger.Infof("[%s] VNic 삭제 실패 : ", reqVnicID, err) + cblogger.Infof("[%s] VNic Delete Failed : ", reqVnicID, err) } else { - cblogger.Infof("[%s] VNic 삭제 결과 : [%s]", reqVnicID, result) + cblogger.Infof("[%s] VNic Delete Result : [%s]", reqVnicID, result) } } } @@ -934,7 +934,7 @@ func handleVNic() { func testErr() error { //return awserr.Error("") //return errors.New("") - return awserr.New("504", "찾을 수 없음", nil) + return awserr.New("504", "not found", nil) } // Test VM Lifecycle Management (Create/Suspend/Resume/Reboot/Terminate) @@ -1006,7 +1006,7 @@ func handleVM() { //panic(err) cblogger.Error(err) } else { - cblogger.Info("VM 생성 완료!!", vmInfo) + cblogger.Info("VM Created!!", vmInfo) spew.Dump(vmInfo) VmID = vmInfo.IId } @@ -1017,10 +1017,10 @@ func handleVM() { case 2: vmInfo, err := vmHandler.GetVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM 정보 조회 실패", VmID) + cblogger.Errorf("[%s] VM Info Lookup Failed", VmID) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM 정보 조회 결과", VmID) + cblogger.Infof("[%s] VM Info Lookup Result", VmID) cblogger.Info(vmInfo) spew.Dump(vmInfo) } @@ -1029,60 +1029,60 @@ func handleVM() { cblogger.Info("Start Suspend VM ...") result, err := vmHandler.SuspendVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM Suspend 실패 - [%s]", VmID, result) + cblogger.Errorf("[%s] VM Suspend Fail - [%s]", VmID, result) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Suspend 성공 - [%s]", VmID, result) + cblogger.Infof("[%s] VM Suspend Success - [%s]", VmID, result) } case 4: cblogger.Info("Start Resume VM ...") result, err := vmHandler.ResumeVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM Resume 실패 - [%s]", VmID, result) + cblogger.Errorf("[%s] VM Resume Fail - [%s]", VmID, result) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Resume 성공 - [%s]", VmID, result) + cblogger.Infof("[%s] VM Resume Success - [%s]", VmID, result) } case 5: cblogger.Info("Start Reboot VM ...") result, err := vmHandler.RebootVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM Reboot 실패 - [%s]", VmID, result) + cblogger.Errorf("[%s] VM Reboot Fail - [%s]", VmID, result) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Reboot 성공 - [%s]", VmID, result) + cblogger.Infof("[%s] VM Reboot Success - [%s]", VmID, result) } case 6: cblogger.Info("Start Terminate VM ...") result, err := vmHandler.TerminateVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM Terminate 실패 - [%s]", VmID, result) + cblogger.Errorf("[%s] VM Terminate Fail - [%s]", VmID, result) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Terminate 성공 - [%s]", VmID, result) + cblogger.Infof("[%s] VM Terminate Success - [%s]", VmID, result) } case 7: cblogger.Info("Start Get VM Status...") vmStatus, err := vmHandler.GetVMStatus(VmID) if err != nil { - cblogger.Errorf("[%s] VM Get Status 실패", VmID) + cblogger.Errorf("[%s] VM Get Status Fail", VmID) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Get Status 성공 : [%s]", VmID, vmStatus) + cblogger.Infof("[%s] VM Get Status Success : [%s]", VmID, vmStatus) } case 8: cblogger.Info("Start ListVMStatus ...") vmStatusInfos, err := vmHandler.ListVMStatus() if err != nil { - cblogger.Error("ListVMStatus 실패") + cblogger.Error("ListVMStatus Fail") cblogger.Error(err) } else { - cblogger.Info("ListVMStatus 성공") + cblogger.Info("ListVMStatus Success") cblogger.Info(vmStatusInfos) spew.Dump(vmStatusInfos) } @@ -1091,14 +1091,14 @@ func handleVM() { cblogger.Info("Start ListVM ...") vmList, err := vmHandler.ListVM() if err != nil { - cblogger.Error("ListVM 실패") + cblogger.Error("ListVM Fail") cblogger.Error(err) } else { - cblogger.Info("ListVM 성공") - cblogger.Info("=========== VM 목록 ================") + cblogger.Info("ListVM Success") + cblogger.Info("=========== VM List ================") cblogger.Info(vmList) spew.Dump(vmList) - cblogger.Infof("=========== VM 목록 수 : [%d] ================", len(vmList)) + cblogger.Infof("=========== VM List count : [%d] ================", len(vmList)) if len(vmList) > 0 { VmID = vmList[0].IId } @@ -1158,7 +1158,7 @@ func handleVMSpec() { cblogger.Debug("VMSpec List Lookup Result") //spew.Dump(result) cblogger.Debug(result) - cblogger.Infof("전체 목록 개수 : [%d]", len(result)) + cblogger.Infof("Total number of lists : [%d]", len(result)) } fmt.Println("Finish ListVMSpec()") @@ -1167,9 +1167,9 @@ func handleVMSpec() { fmt.Println("Start GetVMSpec() ...") result, err := handler.GetVMSpec(reqVMSpec) if err != nil { - cblogger.Error(reqVMSpec, " VMSpec 정보 조회 실패 : ", err) + cblogger.Error(reqVMSpec, " VMSpec Info Lookup Failed : ", err) } else { - cblogger.Debugf("VMSpec[%s] 정보 조회 결과", reqVMSpec) + cblogger.Debugf("VMSpec[%s] Info Lookup Result", reqVMSpec) //spew.Dump(result) cblogger.Debug(result) } @@ -1188,7 +1188,7 @@ func handleVMSpec() { //fmt.Println(result) //fmt.Println("=========================") //fmt.Println(result) - cblogger.Infof("전체 목록 개수 : [%d]", len(result)) + cblogger.Infof("Total number of lists : [%d]", len(result)) } fmt.Println("Finish ListOrgVMSpec()") @@ -1197,9 +1197,9 @@ func handleVMSpec() { fmt.Println("Start GetOrgVMSpec() ...") result, err := handler.GetOrgVMSpec(reqVMSpec) if err != nil { - cblogger.Error(reqVMSpec, " VMSpec Org 정보 조회 실패 : ", err) + cblogger.Error(reqVMSpec, " VMSpec Org Info Lookup Failed : ", err) } else { - cblogger.Debugf("VMSpec[%s] Org 정보 조회 결과", reqVMSpec) + cblogger.Debugf("VMSpec[%s] Org Info Lookup Result", reqVMSpec) //spew.Dump(result) cblogger.Debug(result) //fmt.Println(result) @@ -1289,21 +1289,21 @@ func handleNLB() { cblogger.Debug(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) - //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. + //조회및 Delete Test를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. if result != nil { nlbReqInfo.IId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 } } case 2: - cblogger.Infof("[%s] NLB 생성 테스트", nlbReqInfo.IId.NameId) + cblogger.Infof("[%s] NLB Create Test", nlbReqInfo.IId.NameId) result, err := handler.CreateNLB(nlbReqInfo) if err != nil { - cblogger.Infof(nlbReqInfo.IId.NameId, " NLB 생성 실패 : ", err) + cblogger.Infof(nlbReqInfo.IId.NameId, " NLB Create Failed : ", err) } else { - cblogger.Infof("NLB 생성 성공 : ", result) + cblogger.Infof("NLB Create Success : ", result) nlbReqInfo.IId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 if cblogger.Level.String() == "debug" { spew.Dump(result) @@ -1311,29 +1311,29 @@ func handleNLB() { } case 3: - cblogger.Infof("[%s] NLB 조회 테스트", nlbReqInfo.IId) + cblogger.Infof("[%s] NLB Lookup Test", nlbReqInfo.IId) result, err := handler.GetNLB(nlbReqInfo.IId) if err != nil { - cblogger.Infof("[%s] NLB 조회 실패 : ", nlbReqInfo.IId.NameId, err) + cblogger.Infof("[%s] NLB Lookup Failed : ", nlbReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] NLB 조회 성공 : [%s]", nlbReqInfo.IId.NameId, result) + cblogger.Infof("[%s] NLB Lookup Success : [%s]", nlbReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } } case 4: - cblogger.Infof("[%s] NLB 삭제 테스트", nlbReqInfo.IId.NameId) + cblogger.Infof("[%s] NLB Delete Test", nlbReqInfo.IId.NameId) result, err := handler.DeleteNLB(nlbReqInfo.IId) if err != nil { - cblogger.Infof("[%s] NLB 삭제 실패 : ", nlbReqInfo.IId.NameId, err) + cblogger.Infof("[%s] NLB Delete Failed : ", nlbReqInfo.IId.NameId, err) } else { - cblogger.Info("성공") - cblogger.Infof("[%s] NLB 삭제 성공 : [%s]", nlbReqInfo.IId.NameId, result) + cblogger.Info("Success") + cblogger.Infof("[%s] NLB Delete Success : [%s]", nlbReqInfo.IId.NameId, result) } case 5: - cblogger.Infof("[%s] 리스너 변경 테스트", nlbReqInfo.IId) + cblogger.Infof("[%s] Change listener Test", nlbReqInfo.IId) reqListenerInfo := irs.ListenerInfo{ Protocol: "TCP", // AWS NLB : TCP, TLS, UDP, or TCP_UDP //IP: "", @@ -1341,64 +1341,64 @@ func handleNLB() { } result, err := handler.ChangeListener(nlbReqInfo.IId, reqListenerInfo) if err != nil { - cblogger.Infof("[%s] 리스너 변경 실패 : ", nlbReqInfo.IId.NameId, err) + cblogger.Infof("[%s] Change listener Fail : ", nlbReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] 리스너 변경 성공 : [%s]", nlbReqInfo.IId.NameId, result) + cblogger.Infof("[%s] Change listener Success : [%s]", nlbReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } } case 7: - cblogger.Infof("[%s] AddVMs 테스트", nlbReqInfo.IId.NameId) + cblogger.Infof("[%s] AddVMs Test", nlbReqInfo.IId.NameId) cblogger.Info(reqAddVMs) result, err := handler.AddVMs(nlbReqInfo.IId, reqAddVMs) if err != nil { - cblogger.Infof("[%s] AddVMs 실패 : ", nlbReqInfo.IId.NameId, err) + cblogger.Infof("[%s] AddVMs Fail : ", nlbReqInfo.IId.NameId, err) } else { - cblogger.Info("성공") - cblogger.Infof("[%s] AddVMs 성공 : [%s]", nlbReqInfo.IId.NameId, result) + cblogger.Info("Success") + cblogger.Infof("[%s] AddVMs Success : [%s]", nlbReqInfo.IId.NameId, result) } case 8: - cblogger.Infof("[%s] RemoveVMs 테스트", nlbReqInfo.IId.NameId) + cblogger.Infof("[%s] RemoveVMs Test", nlbReqInfo.IId.NameId) cblogger.Info(reqRemoveVMs) result, err := handler.RemoveVMs(nlbReqInfo.IId, reqRemoveVMs) if err != nil { - cblogger.Infof("[%s] RemoveVMs 실패 : ", nlbReqInfo.IId.NameId, err) + cblogger.Infof("[%s] RemoveVMs Fail : ", nlbReqInfo.IId.NameId, err) } else { - cblogger.Info("성공") - cblogger.Infof("[%s] RemoveVMs 성공 : [%s]", nlbReqInfo.IId.NameId, result) + cblogger.Info("Success") + cblogger.Infof("[%s] RemoveVMs Success : [%s]", nlbReqInfo.IId.NameId, result) } case 9: - cblogger.Infof("[%s] GetVMGroupHealthInfo 테스트", nlbReqInfo.IId) + cblogger.Infof("[%s] GetVMGroupHealthInfo Test", nlbReqInfo.IId) result, err := handler.GetVMGroupHealthInfo(nlbReqInfo.IId) if err != nil { - cblogger.Infof("[%s] GetVMGroupHealthInfo 실패 : ", nlbReqInfo.IId.NameId, err) + cblogger.Infof("[%s] GetVMGroupHealthInfo Fail : ", nlbReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] GetVMGroupHealthInfo 성공 : [%s]", nlbReqInfo.IId.NameId, result) + cblogger.Infof("[%s] GetVMGroupHealthInfo Success : [%s]", nlbReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } } case 6: - cblogger.Infof("[%s] NLB VM Group 변경 테스트", nlbReqInfo.IId.NameId) + cblogger.Infof("[%s] NLB VM Group Change Test", nlbReqInfo.IId.NameId) result, err := handler.ChangeVMGroupInfo(nlbReqInfo.IId, irs.VMGroupInfo{ Protocol: "TCP", Port: "8080", }) if err != nil { - cblogger.Infof("[%s] NLB VM Group 변경 실패 : ", nlbReqInfo.IId.NameId, err) + cblogger.Infof("[%s] NLB VM Group Change Fail : ", nlbReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] NLB VM Group 변경 성공 : [%s]", nlbReqInfo.IId.NameId, result) + cblogger.Infof("[%s] NLB VM Group Change Success : [%s]", nlbReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } } case 10: - cblogger.Infof("[%s] NLB Health Checker 변경 테스트", nlbReqInfo.IId.NameId) + cblogger.Infof("[%s] NLB Health Checker Change Test", nlbReqInfo.IId.NameId) result, err := handler.ChangeHealthCheckerInfo(nlbReqInfo.IId, irs.HealthCheckerInfo{ Protocol: "TCP", Port: "22", @@ -1407,9 +1407,9 @@ func handleNLB() { Threshold: 5, }) if err != nil { - cblogger.Infof("[%s] NLB Health Checker 변경 실패 : ", nlbReqInfo.IId.NameId, err) + cblogger.Infof("[%s] NLB Health Checker Change Fail : ", nlbReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] NLB Health Checker 변경 성공 : [%s]", nlbReqInfo.IId.NameId, result) + cblogger.Infof("[%s] NLB Health Checker Change Success : [%s]", nlbReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } @@ -1503,21 +1503,21 @@ func handleCluster() { cblogger.Debug(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. if len(result) > 0 { clusterReqInfo.IId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 - cblogger.Info("---> Req IID 변경 : ", clusterReqInfo.IId) + cblogger.Info("---> Req IID Change : ", clusterReqInfo.IId) } } case 2: - cblogger.Infof("[%s] Cluster Create 테스트", clusterReqInfo.IId.NameId) + cblogger.Infof("[%s] Cluster Create Test", clusterReqInfo.IId.NameId) result, err := handler.CreateCluster(clusterReqInfo) if err != nil { - cblogger.Infof(clusterReqInfo.IId.NameId, " Cluster Create 실패 : ", err) + cblogger.Infof(clusterReqInfo.IId.NameId, " Cluster Create Fail : ", err) } else { - cblogger.Infof("Cluster Create 성공 : ", result) + cblogger.Infof("Cluster Create Success : ", result) clusterReqInfo.IId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 if cblogger.Level.String() == "debug" { spew.Dump(result) @@ -1527,42 +1527,42 @@ func handleCluster() { //eks-cb-eks-node-test02a-aws-9cc2876a-d3cb-2c25-55a8-9a19c431e716 case 3: - cblogger.Infof("[%s] Cluster Get 테스트", clusterReqInfo.IId) + cblogger.Infof("[%s] Cluster Get Test", clusterReqInfo.IId) result, err := handler.GetCluster(clusterReqInfo.IId) if err != nil { - cblogger.Infof("[%s] Cluster Get 실패 : ", clusterReqInfo.IId.NameId, err) + cblogger.Infof("[%s] Cluster Get Fail : ", clusterReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] Cluster Get 성공 : [%s]", clusterReqInfo.IId.NameId, result) + cblogger.Infof("[%s] Cluster Get Success : [%s]", clusterReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } } case 4: - cblogger.Infof("[%s] Cluster Delete 테스트", clusterReqInfo.IId.NameId) + cblogger.Infof("[%s] Cluster Delete Test", clusterReqInfo.IId.NameId) result, err := handler.DeleteCluster(clusterReqInfo.IId) if err != nil { - cblogger.Infof("[%s] Cluster Delete 실패 : ", clusterReqInfo.IId.NameId, err) + cblogger.Infof("[%s] Cluster Delete Fail : ", clusterReqInfo.IId.NameId, err) } else { - cblogger.Info("성공") - cblogger.Infof("[%s] Cluster Delete 성공 : [%s]", clusterReqInfo.IId.NameId, result) + cblogger.Info("Success") + cblogger.Infof("[%s] Cluster Delete Success : [%s]", clusterReqInfo.IId.NameId, result) } /* case 5: - cblogger.Infof("[%s] ListNodeGroup 테스트", clusterReqInfo.IId) + cblogger.Infof("[%s] ListNodeGroup Test", clusterReqInfo.IId) result, err := handler.ListNodeGroup(clusterReqInfo.IId) if err != nil { - cblogger.Infof("[%s] ListNodeGroup 실패 : ", clusterReqInfo.IId.NameId, err) + cblogger.Infof("[%s] ListNodeGroup Fail : ", clusterReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] ListNodeGroup 성공 : [%s]", clusterReqInfo.IId.NameId, result) + cblogger.Infof("[%s] ListNodeGroup Success : [%s]", clusterReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) - //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. + //조회및 삭제 Test를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. if len(result) > 0 { reqNodeGroupInfo.IId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 cblogger.Info("---> Req IID 변경 : ", reqNodeGroupInfo.IId) @@ -1571,49 +1571,49 @@ func handleCluster() { */ case 6: - cblogger.Infof("[%s] AddNodeGroup 테스트", clusterReqInfo.IId) + cblogger.Infof("[%s] AddNodeGroup Test", clusterReqInfo.IId) result, err := handler.AddNodeGroup(clusterReqInfo.IId, reqNodeGroupInfo) if err != nil { - cblogger.Infof("[%s] AddNodeGroup 실패 : ", clusterReqInfo.IId.NameId, err) + cblogger.Infof("[%s] AddNodeGroup Fail : ", clusterReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] AddNodeGroup 성공 : [%s]", clusterReqInfo.IId.NameId, result) + cblogger.Infof("[%s] AddNodeGroup Success : [%s]", clusterReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } } case 7: - cblogger.Infof("[%s] RemoveNodeGroup 테스트", clusterReqInfo.IId) + cblogger.Infof("[%s] RemoveNodeGroup Test", clusterReqInfo.IId) result, err := handler.RemoveNodeGroup(clusterReqInfo.IId, reqNodeGroupInfo.IId) if err != nil { - cblogger.Infof("[%s] RemoveNodeGroup 실패 : ", reqNodeGroupInfo.IId.SystemId, err) + cblogger.Infof("[%s] RemoveNodeGroup Fail : ", reqNodeGroupInfo.IId.SystemId, err) } else { - cblogger.Infof("[%s] RemoveNodeGroup 성공", reqNodeGroupInfo.IId.SystemId) + cblogger.Infof("[%s] RemoveNodeGroup Success", reqNodeGroupInfo.IId.SystemId) if cblogger.Level.String() == "debug" { spew.Dump(result) } } case 8: - cblogger.Infof("[%s] UpgradeCluster 테스트", clusterReqInfo.IId) + cblogger.Infof("[%s] UpgradeCluster Test", clusterReqInfo.IId) result, err := handler.UpgradeCluster(clusterReqInfo.IId, "1.24") if err != nil { - cblogger.Infof("[%s] UpgradeCluster 실패 : ", clusterReqInfo.IId.NameId, err) + cblogger.Infof("[%s] UpgradeCluster Fail : ", clusterReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] UpgradeCluster 성공 : [%s]", clusterReqInfo.IId.NameId, result) + cblogger.Infof("[%s] UpgradeCluster Success : [%s]", clusterReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } } case 9: - cblogger.Infof("[%s] ChangeNodeGroupScaling 테스트", clusterReqInfo.IId) + cblogger.Infof("[%s] ChangeNodeGroupScaling Test", clusterReqInfo.IId) //원하는 크기 / 최소 크기 / 최대 크기 result, err := handler.ChangeNodeGroupScaling(clusterReqInfo.IId, reqNodeGroupInfo.IId, 2, 2, 4) if err != nil { - cblogger.Infof("[%s] ChangeNodeGroupScaling 실패 : ", clusterReqInfo.IId.NameId, err) + cblogger.Infof("[%s] ChangeNodeGroupScaling Fail : ", clusterReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] ChangeNodeGroupScaling 성공 : [%s]", clusterReqInfo.IId.NameId, result) + cblogger.Infof("[%s] ChangeNodeGroupScaling Success : [%s]", clusterReqInfo.IId.NameId, result) if cblogger.Level.String() == "debug" { spew.Dump(result) } @@ -1664,15 +1664,15 @@ func handleRegionZone() { spew.Dump(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) } case 2: result, err := handler.GetRegionZone("us-west-1") if err != nil { - cblogger.Infof("GetRegionZone 조회 실패 : ", err) + cblogger.Infof("GetRegionZone Lookup Failed : ", err) } else { - cblogger.Info("GetRegionZone 조회 결과") + cblogger.Info("GetRegionZone Lookup Result") cblogger.Debug(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) // spew.Dump(result) @@ -1687,7 +1687,7 @@ func handleRegionZone() { cblogger.Debug(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) } case 4: result, err := handler.ListOrgZone() @@ -1698,7 +1698,7 @@ func handleRegionZone() { cblogger.Debug(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) } } } @@ -1743,7 +1743,7 @@ func handlePriceInfo() { spew.Dump(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) } case 2: @@ -1766,9 +1766,9 @@ func handlePriceInfo() { result, err := handler.GetPriceInfo("Compute Instance", "us-west-1", filterList) if err != nil { - cblogger.Infof("GetPriceInfo 조회 실패 : ", err) + cblogger.Infof("GetPriceInfo Lookup Failed : ", err) } else { - cblogger.Info("GetPriceInfo 조회 결과") + cblogger.Info("GetPriceInfo Lookup Result") cblogger.Info(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) } @@ -1789,7 +1789,7 @@ func handleTag() { var reqType irs.RSType = irs.VM reqIID := irs.IID{SystemId: "i-02ac1c4ff1d40815c"} - reqTag := irs.KeyValue{Key: "tag3", Value: "태그3"} + reqTag := irs.KeyValue{Key: "tag3", Value: "tag3 test"} reqKey := "tag3" reqKey = "" reqType = irs.ALL @@ -1828,7 +1828,7 @@ func handleTag() { cblogger.Debug(result) cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. if result != nil { @@ -1837,45 +1837,44 @@ func handleTag() { } case 2: - cblogger.Infof("[%s] Tag 추가 테스트", reqIID.SystemId) + cblogger.Infof("[%s] Tag Add Test", reqIID.SystemId) result, err := handler.AddTag(reqType, reqIID, reqTag) if err != nil { - cblogger.Infof(reqIID.SystemId, " Tag 생성 실패 : ", err) + cblogger.Infof(reqIID.SystemId, " Tag Create Failed : ", err) } else { - cblogger.Info("Tag 생성 결과 : ", result) + cblogger.Info("Tag Create Result : ", result) reqKey = result.Key - cblogger.Infof("요청 대상 Tag Key가 [%s]로 변경 됨", reqKey) + cblogger.Infof("Request Target Tag Key changed to [%s]", reqKey) spew.Dump(result) } case 3: - cblogger.Infof("[%s] Tag 조회 테스트 - Key[%s]", reqIID.SystemId, reqKey) + cblogger.Infof("[%s] Tag Lookup Test - Key[%s]", reqIID.SystemId, reqKey) result, err := handler.GetTag(reqType, reqIID, reqKey) if err != nil { - cblogger.Infof("[%s] Tag 조회 실패 : [%v]", reqKey, err) + cblogger.Infof("[%s] Tag Lookup Failed : [%v]", reqKey, err) } else { - cblogger.Infof("[%s] Tag 조회 결과 : [%s]", reqKey, result) + cblogger.Infof("[%s] Tag Lookup Result : [%s]", reqKey, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] Tag 삭제 테스트 - Key[%s]", reqIID.SystemId, reqKey) + cblogger.Infof("[%s] Tag Delete Test - Key[%s]", reqIID.SystemId, reqKey) result, err := handler.RemoveTag(reqType, reqIID, reqKey) if err != nil { - cblogger.Infof("[%s] Tag 삭제 실패 : [%v]", reqKey, err) + cblogger.Infof("[%s] Tag Delete Failed : [%v]", reqKey, err) } else { - cblogger.Infof("[%s] Tag 삭제 결과 : [%v]", reqKey, result) + cblogger.Infof("[%s] Tag Delete Result : [%v]", reqKey, result) } case 5: - cblogger.Infof("[%s] Tag 찾기 테스트 - Key[%s]", reqType, reqKey) + cblogger.Infof("[%s] Tag Find Test - Key[%s]", reqType, reqKey) result, err := handler.FindTag(reqType, reqKey) if err != nil { - cblogger.Infof("[%s] Tag 검색 실패 : [%s]", reqKey, err) + cblogger.Infof("[%s] Tag Find Failed : [%s]", reqKey, err) } else { - cblogger.Infof("[%s] Tag 검색 결과 : [%d]건", reqKey, len(result)) spew.Dump(result) - cblogger.Infof("Tag 검색 결과 : [%d]건", len(result)) + cblogger.Infof("[%s] Tag Find Result : [%d] count", reqKey, len(result)) } } } From 55132f06579ae6990b14c56603deda821cd1ce53 Mon Sep 17 00:00:00 2001 From: dogfootman Date: Tue, 16 Jul 2024 11:25:01 +0900 Subject: [PATCH 07/56] add Alibaba Tag function --- .../alibaba/connect/AlibabaCloudConnection.go | 4 +- .../alibaba/resources/ClusterHandler.go | 4 +- .../alibaba/resources/CommonAlibabaFunc.go | 110 +++ .../alibaba/resources/CommonHandler.go | 551 +++++++++++++- .../drivers/alibaba/resources/DiskHandler.go | 14 + .../drivers/alibaba/resources/TagHandler.go | 703 ++++++++++++++++++ .../drivers/alibaba/resources/VPCHandler.go | 10 + 7 files changed, 1392 insertions(+), 4 deletions(-) create mode 100644 cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/connect/AlibabaCloudConnection.go b/cloud-control-manager/cloud-driver/drivers/alibaba/connect/AlibabaCloudConnection.go index e537a80cf..9314262f7 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/connect/AlibabaCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/connect/AlibabaCloudConnection.go @@ -174,5 +174,7 @@ func (cloudConn *AlibabaCloudConnection) CreatePriceInfoHandler() (irs.PriceInfo } func (cloudConn *AlibabaCloudConnection) CreateTagHandler() (irs.TagHandler, error) { - return nil, errors.New("Alibaba Driver: not implemented") + cblogger.Info("Start") + handler := alirs.AlibabaTagHandler{cloudConn.Region, cloudConn.VMClient, cloudConn.Cs2015Client} + return &handler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/ClusterHandler.go index 2f146a39e..9887807d1 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/ClusterHandler.go @@ -1024,12 +1024,12 @@ func aliDescribeClustersV1(csClient *cs2015.Client, regionId string) ([]*cs2015. ClusterType: tea.String("ManagedKubernetes"), RegionId: tea.String(regionId), } - //cblogger.Debug(describeClustersV1Request) + cblogger.Debug(describeClustersV1Request) describeClustersV1Response, err := csClient.DescribeClustersV1(describeClustersV1Request) if err != nil { return make([]*cs2015.DescribeClustersV1ResponseBodyClusters, 0), err } - //cblogger.Debug(describeClustersV1Response.Body) + cblogger.Debug(describeClustersV1Response.Body) return describeClustersV1Response.Body.Clusters, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go index eb67d08ea..04ec05622 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go @@ -273,3 +273,113 @@ func HasKey(key string, keyValueList []irs.KeyValue) bool { } return false } + +// Endpoint format:[product_code].[region_id].aliyuncs.com +func GetAlibabaApiEndPoint(regionId string, productCode string) string { + return productCode + "." + regionId + ".aliyuncs.com" +} + +func GetAlibabaApiVPCEndpoint(regionId string, productCode string) string { + return productCode + "-vpc" + "." + regionId + ".aliyuncs.com" +} + +// Alibaba에서 사용되는 리소스별 api product type +func GetAlibabaProductCode(resType irs.RSType) (string, error) { + switch resType { + case irs.RSType("VM"): + return "ecs", nil + case irs.RSType("VPC"): + return "vpc", nil + case irs.RSType("SUBNET"): + return "ecs", nil + case irs.RSType("SG"): + return "ecs", nil + case irs.RSType("KEY"): + return "ecs", nil + case irs.RSType("NLB"): + return "slb", nil + case irs.RSType("DISK"): + return "ecs", nil + case irs.RSType("MYIMAGE"): + return "ecs", nil + case irs.RSType("CLUSTER"): + return "ack", nil + case irs.RSType("NODEGROUP"): + return "ack", nil + default: + //return "", nil + } + return "", errors.New("not found productCode " + string(resType)) +} + +// cb-spider의 resourceType 을 alibaba의 resourceType으로 +func GetAlibabaResourceType(resType irs.RSType) (string, error) { + switch resType { + case irs.RSType("VM"): + return "instance", nil + // case irs.RSType("VPC"): + // return "vpc", nil + // case irs.RSType("SUBNET"): + // return "ecs", nil + case irs.RSType("SG"): + return "securitygroup", nil + case irs.RSType("KEY"): + return "keypair", nil + // case irs.RSType("NLB"): + // return "slb", nil + case irs.RSType("DISK"): + return "disk", nil + case irs.RSType("MYIMAGE"): + return "snapshot", nil + case irs.RSType("CLUSTER"): + return "CLUSTER", nil + // case irs.RSType("NODEGROUP"): + // return "", nil + default: + //return "", nil + } + //image: image. + //volume: storage volume. + //eni: elastic network interface (ENI). + //ddh: dedicated host. + //launchtemplate: launch template. + //reservedinstance: reserved instance. + //snapshotpolicy: automatic snapshot policy. + return "", errors.New("not found ResourceType " + string(resType)) +} + +// resource Type별로 바로보는 api가 다름. ( ecs, bss, ... ) +func GetAliTargetApi(resType irs.RSType) (string, error) { + switch resType { + case irs.RSType("VM"): + return "ecs", nil + // case irs.RSType("VPC"): + // return "vpc", nil + // case irs.RSType("SUBNET"): + // return "ecs", nil + case irs.RSType("SG"): + return "ecs", nil + case irs.RSType("KEY"): + return "ecs", nil + // case irs.RSType("NLB"): + // return "slb", nil + case irs.RSType("DISK"): + return "ecs", nil + case irs.RSType("MYIMAGE"): + return "ecs", nil + case irs.RSType("CLUSTER"): + return "cs", nil + // case irs.RSType("NODEGROUP"): + // return "", nil + default: + //return "", nil + } + //image: image. + //volume: storage volume. + //eni: elastic network interface (ENI). + //ddh: dedicated host. + //launchtemplate: launch template. + //reservedinstance: reserved instance. + //snapshotpolicy: automatic snapshot policy. + return "", errors.New("not found ResourceType " + string(resType)) +} diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go index c8f9a8f60..6d627b90c 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go @@ -7,9 +7,14 @@ import ( "strings" "time" + "github.com/alibabacloud-go/tea/tea" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses" + + cs "github.com/alibabacloud-go/cs-20151215/v4/client" // cs : container service bssopenapi "github.com/aliyun/alibaba-cloud-sdk-go/services/bssopenapi" - "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" // ecs : elastic compute service + "github.com/aliyun/alibaba-cloud-sdk-go/services/vpc" // vpc call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" @@ -710,3 +715,547 @@ func QueryProductList(bssClient *bssopenapi.Client) (*bssopenapi.QueryProductLis return productListresponse, nil } + +func AddEcsTags(client *ecs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (*responses.CommonResponse, error) { + apiName := "AddTags" + regionID := regionInfo.Region + + cblogger.Info("Start Add EcsTag : ", tag) + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, resIID.NameId, apiName) + + // 생성된 Tag 정보 획득 후, Tag 정보 리턴 + //tagInfo := irs.TagInfo{} + + // 지원하는 resource Type인지 확인 + alibabaResourceType, err := GetAlibabaResourceType(resType) + if err != nil { + return nil, err + } + + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType + queryParams["ResourceId"] = resIID.SystemId + queryParams["Tag.1.Key"] = tag.Key + queryParams["Tag.1.Value"] = tag.Value + + start := call.Start() + response, err := CallEcsRequest(resType, client, regionInfo, apiName, queryParams) + LoggingInfo(hiscallInfo, start) + + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + + expectStatus := true // 예상되는 상태 : 있어야 하므로 true + result, err := WaitForEcsTagExist(client, regionInfo, resType, resIID, tag.Key, expectStatus) + if err != nil { + return nil, err + } + cblogger.Debug("Expect Status ", expectStatus, ", result Status ", result) + if !result { + return nil, errors.New("waitForTagExist Error ") + } + + return response, nil +} + +// 해당 ECS Resource에 특정 tag가 있는지 조회 : +func WaitForEcsTagExist(client *ecs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, tag string, expectStatus bool) (bool, error) { + + //waitStatus := false + curRetryCnt := 0 + maxRetryCnt := 3 // 최대 10초 기다림 + for { + + // 해당 resource의 tag를 가져온다. + response, err := DescribeDescribeEcsTags(client, regionInfo, resType, resIID, "") //tag.Key + if err != nil { + return false, err + } + + // tag들 추출 + resTags := ecs.DescribeTagsResponse{} + tagResponseStr := response.GetHttpContentString() + err = json.Unmarshal([]byte(tagResponseStr), &resTags) + if err != nil { + cblogger.Error(err.Error()) + return false, err + } + + // extract Tag + existTag := false + for _, aliTag := range resTags.Tags.Tag { + //cblogger.Info(aliTag) + cblogger.Info(aliTag.TagKey + ":" + tag) + if aliTag.TagKey == tag { + existTag = true + break + } + } + + // expectStatus : 예상되는 상태. + if expectStatus == existTag { // tag가 존재할 때까지 반복 + return true, nil + } + + //if curStatus != irs.VMStatus(waitStatus) { + curRetryCnt++ + cblogger.Errorf("Waiting for 1 second and then querying") + time.Sleep(time.Second * 1) + if curRetryCnt > maxRetryCnt { + return false, errors.New("After waiting for a long time") + } + } + + //return false, nil +} + +func DescribeDescribeEcsTags(client *ecs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, key string) (*responses.CommonResponse, error) { + apiName := "DescribeTags" + regionID := regionInfo.Region + + // call logger set + callogger := call.GetLogger("HISCALL") + callLogInfo := call.CLOUDLOGSCHEMA{ + CloudOS: call.ALIBABA, + RegionZone: "", + ResourceType: call.TAG, + ResourceName: "", + CloudOSAPI: apiName, + ElapsedTime: "", + ErrorMSG: "", + } + + apiProductCode, err := GetAlibabaProductCode(irs.RSType(resType)) + if err != nil { + return nil, err + } + + alibabaResourceType, err2 := GetAlibabaResourceType(resType) + if err2 != nil { + return nil, err2 + } + + request := requests.NewCommonRequest() + + request.Method = "POST" + request.Scheme = "https" // https | http + //request.Domain = "ecs.cn-hongkong.aliyuncs.com" + request.Domain = GetAlibabaApiEndPoint(regionID, apiProductCode) + request.Version = "2014-05-26" + request.ApiName = apiName + request.QueryParams["RegionId"] = regionID + + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType //string(resType) + queryParams["ResourceId"] = resIID.SystemId + if key != "" { + queryParams["Tag.1.Key"] = key // 한번에 1개씩만 가져온다. + } + + callLogStart := call.Start() + response, err := client.ProcessCommonRequest(request) + + callLogInfo.ElapsedTime = call.Elapsed(callLogStart) + callogger.Info(call.String(callLogInfo)) + if err != nil { + cblogger.Error(err.Error()) + return nil, err + } + cblogger.Debug(response.GetHttpContentString()) + + return response, nil +} + +// //////// VPC begin ///////////// +func AddVpcTags(client *vpc.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (*responses.CommonResponse, error) { + apiName := "AddTags" + regionID := regionInfo.Region + + cblogger.Info("Start Add EcsTag : ", tag) + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, resIID.NameId, apiName) + + // 생성된 Tag 정보 획득 후, Tag 정보 리턴 + //tagInfo := irs.TagInfo{} + + // 지원하는 resource Type인지 확인 + alibabaResourceType, err := GetAlibabaResourceType(resType) + if err != nil { + return nil, err + } + + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType + queryParams["ResourceId"] = resIID.SystemId + queryParams["Tag.1.Key"] = tag.Key + queryParams["Tag.1.Value"] = tag.Value + + start := call.Start() + response, err := CallVpcRequest(resType, client, regionInfo, apiName, queryParams) + LoggingInfo(hiscallInfo, start) + + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + + expectStatus := true // 예상되는 상태 : 있어야 하므로 true + result, err := WaitForVpcTagExist(client, regionInfo, resType, resIID, tag.Key, expectStatus) + if err != nil { + return nil, err + } + cblogger.Debug("Expect Status ", expectStatus, ", result Status ", result) + if !result { + return nil, errors.New("waitForTagExist Error ") + } + + return response, nil +} + +// 해당 ECS Resource에 특정 tag가 있는지 조회 : +func WaitForVpcTagExist(client *vpc.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, tag string, expectStatus bool) (bool, error) { + + //waitStatus := false + curRetryCnt := 0 + maxRetryCnt := 3 // 최대 10초 기다림 + for { + + // 해당 resource의 tag를 가져온다. + response, err := DescribeDescribeVpcTags(client, regionInfo, resType, resIID, "") //tag.Key + if err != nil { + return false, err + } + + // tag들 추출 + resTags := ecs.DescribeTagsResponse{} + tagResponseStr := response.GetHttpContentString() + err = json.Unmarshal([]byte(tagResponseStr), &resTags) + if err != nil { + cblogger.Error(err.Error()) + return false, err + } + + // extract Tag + existTag := false + for _, aliTag := range resTags.Tags.Tag { + //cblogger.Info(aliTag) + cblogger.Info(aliTag.TagKey + ":" + tag) + if aliTag.TagKey == tag { + existTag = true + break + } + } + + // expectStatus : 예상되는 상태. + if expectStatus == existTag { // tag가 존재할 때까지 반복 + return true, nil + } + + //if curStatus != irs.VMStatus(waitStatus) { + curRetryCnt++ + cblogger.Errorf("Waiting for 1 second and then querying") + time.Sleep(time.Second * 1) + if curRetryCnt > maxRetryCnt { + return false, errors.New("After waiting for a long time") + } + } + + //return false, nil +} + +func DescribeDescribeVpcTags(client *vpc.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, key string) (*responses.CommonResponse, error) { + apiName := "DescribeTags" + regionID := regionInfo.Region + + // call logger set + callogger := call.GetLogger("HISCALL") + callLogInfo := call.CLOUDLOGSCHEMA{ + CloudOS: call.ALIBABA, + RegionZone: "", + ResourceType: call.TAG, + ResourceName: "", + CloudOSAPI: apiName, + ElapsedTime: "", + ErrorMSG: "", + } + + apiProductCode, err := GetAlibabaProductCode(irs.RSType(resType)) + if err != nil { + return nil, err + } + + alibabaResourceType, err2 := GetAlibabaResourceType(resType) + if err2 != nil { + return nil, err2 + } + + request := requests.NewCommonRequest() + + request.Method = "POST" + request.Scheme = "https" // https | http + //request.Domain = "ecs.cn-hongkong.aliyuncs.com" + request.Domain = GetAlibabaApiEndPoint(regionID, apiProductCode) + request.Version = "2014-05-26" + request.ApiName = apiName + request.QueryParams["RegionId"] = regionID + + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType //string(resType) + queryParams["ResourceId"] = resIID.SystemId + if key != "" { + queryParams["Tag.1.Key"] = key // 한번에 1개씩만 가져온다. + } + + callLogStart := call.Start() + response, err := client.ProcessCommonRequest(request) + + callLogInfo.ElapsedTime = call.Elapsed(callLogStart) + callogger.Info(call.String(callLogInfo)) + if err != nil { + cblogger.Error(err.Error()) + return nil, err + } + cblogger.Debug(response.GetHttpContentString()) + + return response, nil +} + +////////// VPC end ///////// + +// //// Call 공통 /////////// +// EcsRequest : Elastic Compute Service(ECS) +func CallEcsRequest(resType irs.RSType, client *ecs.Client, regionInfo idrv.RegionInfo, apiName string, queryParams map[string]string) (*responses.CommonResponse, error) { + regionID := regionInfo.Region + + apiProductCode, err := GetAlibabaProductCode(irs.RSType(resType)) + + // call logger set + callogger := call.GetLogger("HISCALL") + callLogInfo := call.CLOUDLOGSCHEMA{ + CloudOS: call.ALIBABA, + RegionZone: "", + ResourceType: call.TAG, + ResourceName: "", + CloudOSAPI: apiName, + ElapsedTime: "", + ErrorMSG: "", + } + + request := requests.NewCommonRequest() + + request.Method = "POST" + request.Scheme = "https" // https | http + //request.Domain = "ecs.cn-hongkong.aliyuncs.com" + request.Domain = GetAlibabaApiEndPoint(regionID, apiProductCode) + request.Version = "2014-05-26" + request.ApiName = apiName + request.QueryParams["RegionId"] = regionID + + // Tag가 있으면 + if queryParams != nil { + request.QueryParams = queryParams + } + + callLogStart := call.Start() + response, err := client.ProcessCommonRequest(request) + + callLogInfo.ElapsedTime = call.Elapsed(callLogStart) + callogger.Info(call.String(callLogInfo)) + if err != nil { + cblogger.Error(err.Error()) + return nil, err + } + cblogger.Debug(response.GetHttpContentString()) + return response, nil +} + +func CallVpcRequest(resType irs.RSType, client *vpc.Client, regionInfo idrv.RegionInfo, apiName string, queryParams map[string]string) (*responses.CommonResponse, error) { + regionID := regionInfo.Region + + apiProductCode, err := GetAlibabaProductCode(irs.RSType(resType)) + + // call logger set + callogger := call.GetLogger("HISCALL") + callLogInfo := call.CLOUDLOGSCHEMA{ + CloudOS: call.ALIBABA, + RegionZone: "", + ResourceType: call.TAG, + ResourceName: "", + CloudOSAPI: apiName, + ElapsedTime: "", + ErrorMSG: "", + } + + request := requests.NewCommonRequest() + + request.Method = "POST" + request.Scheme = "https" // https | http + //request.Domain = "ecs.cn-hongkong.aliyuncs.com" + request.Domain = GetAlibabaApiEndPoint(regionID, apiProductCode) + request.Version = "2014-05-26" + request.ApiName = apiName + request.QueryParams["RegionId"] = regionID + + // Tag가 있으면 + if queryParams != nil { + request.QueryParams = queryParams + } + + callLogStart := call.Start() + response, err := client.ProcessCommonRequest(request) + + callLogInfo.ElapsedTime = call.Elapsed(callLogStart) + callogger.Info(call.String(callLogInfo)) + if err != nil { + cblogger.Error(err.Error()) + return nil, err + } + cblogger.Debug(response.GetHttpContentString()) + return response, nil +} + +///// Call 공통 end ///////// + +// Container service TagList : 원래는 ResourceIds이나 받는 Param이 1개이므로 1개Resource의 Tags +func aliCsListTag(csClient *cs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID) (*cs.ListTagResourcesResponseBodyTagResources, error) { + regionID := regionInfo.Region + + alibabaResourceType, err2 := GetAlibabaResourceType(resType) + if err2 != nil { + return nil, err2 + } + + // reqTag := &cs.Tag{ + // Key: tea.String("tk"), + // Value: tea.String("tv"), + // } + + listTagResourcesRequest := &cs.ListTagResourcesRequest{ + RegionId: tea.String(regionID), + ResourceType: tea.String(alibabaResourceType), + ResourceIds: []*string{tea.String(resIID.SystemId)}, // clusterId + //Tags: []*cs.Tag{tag0}, + } + //cblogger.Debug(describeClustersV1Request) + listTagResponse, err := csClient.ListTagResources(listTagResourcesRequest) + //describeClustersV1Response, err := csClient.ListTagResourcesWithOptions(listTagResourcesRequest, headers, runtime) + if err != nil { + return nil, err + } + cblogger.Debug(listTagResponse.Body) + + return listTagResponse.Body.TagResources, nil +} + +// Container service Tag : 호출자체는 csListTag와 같으나 tagKey를 filter조건으로 추가 +func aliCsTag(csClient *cs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, tagKey string) (*cs.ListTagResourcesResponseBodyTagResources, error) { + regionID := regionInfo.Region + + alibabaResourceType, err2 := GetAlibabaResourceType(resType) + if err2 != nil { + return nil, err2 + } + + // reqTag := &cs.Tag{ + // Key: tea.String("tk"), + // Value: tea.String("tv"), + // } + + listTagResourcesRequest := &cs.ListTagResourcesRequest{ + RegionId: tea.String(regionID), + ResourceType: tea.String(alibabaResourceType), + ResourceIds: []*string{tea.String(resIID.SystemId)}, // clusterId + //Tags: []*cs.Tag{tag0}, + } + + if tagKey != "" { + reqTag := &cs.Tag{ + Key: tea.String(tagKey), + } + listTagResourcesRequest.Tags = []*cs.Tag{reqTag} + } + //cblogger.Debug(describeClustersV1Request) + listTagResponse, err := csClient.ListTagResources(listTagResourcesRequest) + //describeClustersV1Response, err := csClient.ListTagResourcesWithOptions(listTagResourcesRequest, headers, runtime) + if err != nil { + return nil, err + } + cblogger.Debug(listTagResponse.Body) + + return listTagResponse.Body.TagResources, nil +} + +// Container service Tag : TagResources를 호출하면 PUT으로 추가 됨 +func aliAddCsTag(csClient *cs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (string, error) { + regionID := regionInfo.Region + + alibabaResourceType, err2 := GetAlibabaResourceType(resType) + if err2 != nil { + return "", err2 + } + + // reqTag := &cs.Tag{ + // Key: tea.String("tk"), + // Value: tea.String("tv"), + // } + + tagResourcesRequest := &cs.TagResourcesRequest{ + RegionId: tea.String(regionID), + ResourceType: tea.String(alibabaResourceType), // CLUSTER" + ResourceIds: []*string{tea.String(resIID.SystemId)}, // clusterId + //Tags: []*cs.Tag{tag0}, + } + + reqTag := &cs.Tag{ + Key: tea.String(tag.Key), + Value: tea.String(tag.Value), + } + tagResourcesRequest.Tags = []*cs.Tag{reqTag} + + //cblogger.Debug(describeClustersV1Request) + tagResourcesResponse, err := csClient.TagResources(tagResourcesRequest) + //describeClustersV1Response, err := csClient.ListTagResourcesWithOptions(listTagResourcesRequest, headers, runtime) + if err != nil { + return "", err + } + cblogger.Debug(tagResourcesResponse.Body) + + return *tagResourcesResponse.Body.RequestId, nil +} + +func aliRemoveCsTag(csClient *cs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, tagKey string) (bool, error) { + regionID := regionInfo.Region + + alibabaResourceType, err2 := GetAlibabaResourceType(resType) + if err2 != nil { + return false, err2 + } + + // reqTag := &cs.Tag{ + // Key: tea.String("tk"), + // Value: tea.String("tv"), + // } + + tagResourcesRequest := &cs.UntagResourcesRequest{ + RegionId: tea.String(regionID), + ResourceType: tea.String(alibabaResourceType), // CLUSTER" + ResourceIds: []*string{tea.String(resIID.SystemId)}, // clusterId + TagKeys: []*string{tea.String(tagKey)}, + } + + cblogger.Debug(tagResourcesRequest) + tagResourcesResponse, err := csClient.UntagResources(tagResourcesRequest) + if err != nil { + return false, err + } + cblogger.Debug(tagResourcesResponse.Body) + + return true, nil +} diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/DiskHandler.go index 554ea22df..7f7e889f4 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/DiskHandler.go @@ -100,6 +100,20 @@ func (diskHandler *AlibabaDiskHandler) CreateDisk(diskReqInfo irs.DiskInfo) (irs return irs.DiskInfo{}, err } + // Add Tag + if diskReqInfo.TagList != nil { + // TagHandler.AddTag + for _, diskTag := range diskReqInfo.TagList { + cblogger.Debug("aliTag ", diskTag) + response, err := AddEcsTags(diskHandler.Client, diskHandler.Region, irs.RSType("DISK"), diskInfo.IId, diskTag) + if err != nil { + cblogger.Error(err) + //return tag, err + } + cblogger.Debug("AddEcsTags response", response) + } + } + return diskInfo, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go new file mode 100644 index 000000000..82bdcce4c --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go @@ -0,0 +1,703 @@ +package resources + +import ( + "encoding/json" + "errors" + + cs "github.com/alibabacloud-go/cs-20151215/v4/client" // cs : container service + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" // ecs : elastic compute service + + call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" + idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" + irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" +) + +type AlibabaTagHandler struct { + Region idrv.RegionInfo + Client *ecs.Client + CsClient *cs.Client +} + +type AliTagResponse struct { + RequestId string `json:"RequestId"` + PageSize int `json:"PageSize"` + PageNumber int `json:"PageNumber"` + TotalCount int `json:"TotalCount"` + AliTags struct { + AliTag []AliTag `json:"Tag"` + } `json:"Tags"` +} + +type AliTag struct { + TagKey string `json:"TagKey"` + TagValue string `json:"TagValue"` + ResourceTypeCount AliResourceTypeCount `json:"ResourceTypeCount"` +} + +type AliTagResource struct { + RegionId string `json:"RegionId" xml:"RegionId"` + ResourceType string `json:"ResourceType" xml:"ResourceType"` + ResourceId string `json:"ResourceId" xml:"ResourceId"` + TagKey string `json:"TagKey" xml:"TagKey"` + TagValue string `json:"TagValue" xml:"TagValue"` +} +type AliTagResources struct { + Resources []AliTagResource `json:"Resource" xml:"Resource"` +} +type AliTagResourcesResponse struct { + RequestId string `json:"RequestId" xml:"RequestId"` + PageSize int `json:"PageSize" xml:"PageSize"` + PageNumber int `json:"PageNumber" xml:"PageNumber"` + TotalCount int `json:"TotalCount" xml:"TotalCount"` + AliTagResources AliTagResources `json:"Resources" xml:"Resources"` +} + +type AliResourceTypeCount struct { + Instance int `json:"Instance"` + Image int `json:"Image"` + Ddh int `json:"Ddh"` + SnapshotPolicy int `json:"SnapshotPolicy"` + Snapshot int `json:"Snapshot"` + ReservedInstance int `json:"ReservedInstance"` + LaunchTemplate int `json:"LaunchTemplate"` + Eni int `json:"Eni"` + Disk int `json:"Disk"` + KeyPair int `json:"KeyPair"` + Volume int `json:"Volume"` +} + +type DescribeTagsResponse struct { + RequestId string `json:"RequestId" xml:"RequestId"` + TotalCount int `json:"TotalCount" xml:"TotalCount"` + PageSize int `json:"PageSize" xml:"PageSize"` + PageNumber int `json:"PageNumber" xml:"PageNumber"` + Tags ecs.TagsInDescribeTags `json:"Tags" xml:"Tags"` +} + +/* +* ECS Instances 아래에 Tags and ResourceGroup이 있음. +AddTag(resType RSType, resIID IID, tag KeyValue) (KeyValue, error) +*/ +func (tagHandler *AlibabaTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + //hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.NameId, "CreateTag()") + + cblogger.Info("Start AddTag : ", tag) + //regionID := tagHandler.Region.Region + + // 생성된 Tag 정보 획득 후, Tag 정보 리턴 + //tagInfo := irs.TagInfo{} + + // 지원하는 resource Type인지 확인 + alibabaResourceType, err := GetAlibabaResourceType(resType) + if err != nil { + //return tagInfo, err + return tag, err + } + cblogger.Debug(alibabaResourceType) + + alibabaApiType, err := GetAliTargetApi(resType) + if err != nil { + return tag, err + } + + switch alibabaApiType { + case "ecs": + // queryParams := map[string]string{} + // queryParams["RegionId"] = regionID + // queryParams["ResourceType"] = alibabaResourceType + // queryParams["ResourceId"] = resIID.SystemId + // queryParams["Tag.1.Key"] = tag.Key + // queryParams["Tag.1.Value"] = tag.Value + + // start := call.Start() + // response, err := CallEcsRequest(resType, tagHandler.Client, tagHandler.Region, "AddTags", queryParams) + // LoggingInfo(hiscallInfo, start) + + // if err != nil { + // cblogger.Error(err.Error()) + // LoggingError(hiscallInfo, err) + // } + // cblogger.Debug(response.GetHttpContentString()) + + // expectStatus := true // 예상되는 상태 : 있어야 하므로 true + // result, err := waitForTagExist(tagHandler.Client, tagHandler.Region, resType, resIID, tag.Key, expectStatus) + // if err != nil { + // return tag, err + // } + // cblogger.Debug("Expect Status ", expectStatus, ", result Status ", result) + // if !result { + // return tag, errors.New("waitForTagExist Error ") + // } + + response, err := AddEcsTags(tagHandler.Client, tagHandler.Region, resType, resIID, tag) + if err != nil { + return tag, err + } + cblogger.Debug("AddEcsTags response", response) + + // 성공했으면 해당 Tag return. + case "cs": + response, err := aliAddCsTag(tagHandler.CsClient, tagHandler.Region, resType, resIID, tag) + if err != nil { + return tag, err + } + cblogger.Debug("AddCsTags response", response) + } + + // request Tag를 retrun하므로 해당 Tag 정보조회 필요없음 + // // 해당 resource의 tag를 가져온다. + // tagResponse, err := DescribeDescribeTags(tagHandler.Client, tagHandler.Region, resType, resIID, tag.Key) + // if err != nil { + // return tag, err + // } + + // // tag들 추출 + // resTags := ecs.DescribeTagsResponse{} + // tagResponseStr := tagResponse.GetHttpContentString() + // err = json.Unmarshal([]byte(tagResponseStr), &resTags) + // if err != nil { + // cblogger.Error(err.Error()) + // return tag, err + // } + + // // extract Tag + // for _, aliTag := range resTags.Tags.Tag { + // cblogger.Debug("aliTag ", aliTag) + // aTagInfo, err := ExtractTagsDescribeInfo(&aliTag) + // if err != nil { + // cblogger.Error(err.Error()) + // continue + // } + + // aTagInfo.ResType = resType + // aTagInfo.ResIId = resIID + + // //break // TagList와 같은 function을 호출하나 1개만 사용 + // return aTagInfo, err + // } + return tag, nil +} + +/* +* +Tag 목록을 제공한다. +ListTag(resType RSType, resIID IID) ([]KeyValue, error) +*/ +func (tagHandler *AlibabaTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]irs.KeyValue, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, "TAG", "ListTag()") + + var tagInfoList []irs.KeyValue + + alibabaApiType, err := GetAliTargetApi(resType) + if err != nil { + return tagInfoList, err + } + + switch alibabaApiType { + case "ecs": + response, err := DescribeDescribeEcsTags(tagHandler.Client, tagHandler.Region, resType, resIID, "") + + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response) + + resTags := ecs.DescribeTagsResponse{} + + tagResponseStr := response.GetHttpContentString() + err = json.Unmarshal([]byte(tagResponseStr), &resTags) + if err != nil { + cblogger.Error(err.Error()) + return tagInfoList, nil + } + + cblogger.Debug("resTags ", resTags) + + for _, aliTag := range resTags.Tags.Tag { + cblogger.Debug("aliTag ", aliTag) + // aTagInfo, err := ExtractTagsDescribeInfo(&aliTag) + // if err != nil { + // cblogger.Error(err.Error()) + // continue + // } + + // aTagInfo.ResType = resType + // aTagInfo.ResIId = resIID + + // cblogger.Debug("tag ", aliTag) + // cblogger.Debug("TagKey ", aliTag.TagKey) + // cblogger.Debug("TagValue ", aliTag.TagValue) + aTagInfo := irs.KeyValue{Key: aliTag.TagKey, Value: aliTag.TagValue} + cblogger.Debug("tagInfo ", aTagInfo) + tagInfoList = append(tagInfoList, aTagInfo) + } + case "cs": + response, err := aliCsListTag(tagHandler.CsClient, tagHandler.Region, resType, resIID) + if err != nil { + cblogger.Error(err.Error()) + return tagInfoList, nil + } + cblogger.Debug("aliCsListTag ", response) + + resTagResources := AliTagResourcesResponse{} + + cblogger.Debug("resTagResources ", resTagResources) + cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) + cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) + + for _, aliTagResource := range response.TagResource { + cblogger.Debug("aliTagResource ", aliTagResource) + + aTagInfo := irs.KeyValue{Key: *aliTagResource.TagKey, Value: *aliTagResource.TagValue} + cblogger.Debug("tagInfo ", aTagInfo) + tagInfoList = append(tagInfoList, aTagInfo) + } + + // resTags := cs.ListTagResourcesResponseBodyTagResources{} + // for _, aliTag := range response.TagResources { + // // } + // // tagResponseStr := response.TagResource + // // err = json.Unmarshal([]byte(tagResponseStr), &resTags) + // // if err != nil { + // // cblogger.Error(err.Error()) + // // return tagInfoList, nil + // } + // // cblogger.Debug("resTags ", resTags) + + // for _, aliTag := range resTags.Tags.Tag { + // cblogger.Debug("aliTag ", aliTag) + // // aTagInfo, err := ExtractTagsDescribeInfo(&aliTag) + // // if err != nil { + // // cblogger.Error(err.Error()) + // // continue + // // } + + // // aTagInfo.ResType = resType + // // aTagInfo.ResIId = resIID + + // // cblogger.Debug("tag ", aliTag) + // // cblogger.Debug("TagKey ", aliTag.TagKey) + // // cblogger.Debug("TagValue ", aliTag.TagValue) + // aTagInfo := irs.KeyValue{Key: aliTag.TagKey, Value: aliTag.TagValue} + // cblogger.Debug("tagInfo ", aTagInfo) + // tagInfoList = append(tagInfoList, aTagInfo) + // } + } + + return tagInfoList, nil +} + +func (tagHandler *AlibabaTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (irs.KeyValue, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.NameId, "GetTag()") + + tagInfo := irs.KeyValue{} + + alibabaApiType, err := GetAliTargetApi(resType) + if err != nil { + return tagInfo, err + } + + switch alibabaApiType { + case "ecs": + response, err := DescribeDescribeEcsTags(tagHandler.Client, tagHandler.Region, resType, resIID, key) + + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + //spew.Dump(response) + + resTags := ecs.DescribeTagsResponse{} + tagResponseStr := response.GetHttpContentString() + err = json.Unmarshal([]byte(tagResponseStr), &resTags) + if err != nil { + cblogger.Error(err.Error()) + return tagInfo, nil + } + + // extract Tag + for _, aliTag := range resTags.Tags.Tag { + tagInfo = irs.KeyValue{Key: aliTag.TagKey, Value: aliTag.TagValue} + } + case "cs": // cs : container service + response, err := aliCsTag(tagHandler.CsClient, tagHandler.Region, resType, resIID, key) + //response, err := aliCsListTag(tagHandler.CsClient, tagHandler.Region, resType, resIID) + if err != nil { + cblogger.Error(err.Error()) + return tagInfo, nil + } + + cblogger.Debug("aliCsTag ", response) + + // TODO : 단건으로 parsing + resTagResources := AliTagResourcesResponse{} + + cblogger.Debug("resTagResources ", resTagResources) + cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) + cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) + + for _, aliTagResource := range response.TagResource { + cblogger.Debug("aliTagResource ", aliTagResource) + + tagInfo = irs.KeyValue{Key: *aliTagResource.TagKey, Value: *aliTagResource.TagValue} + cblogger.Debug("tagInfo ", tagInfo) + + } + } + + // for _, aliTag := range resTags.Tags.Tag { + // cblogger.Debug("aliTag ", aliTag) + // aTagInfo, err := ExtractTagsDescribeInfo(&aliTag) + // if err != nil { + // cblogger.Error(err.Error()) + // continue + // } + + // aTagInfo.ResType = resType + // aTagInfo.ResIId = resIID + + // //break // TagList와 같은 function을 호출하나 1개만 사용 + // return aTagInfo, err + // } + + return tagInfo, nil +} + +// 해당 Resource의 Tag 삭제. 요청이 비동기로 되므로 조회를 통해 삭제 될 때까지 대기. 확인되면 return true. +func (tagHandler *AlibabaTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string) (bool, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.NameId, "RemoveTag()") + + regionID := tagHandler.Region.Region + + alibabaResourceType, err := GetAlibabaResourceType(resType) + if err != nil { + return false, err + } + + alibabaApiType, err := GetAliTargetApi(resType) + if err != nil { + return false, err + } + + switch alibabaApiType { + case "ecs": + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType + queryParams["ResourceId"] = resIID.SystemId + queryParams["Tag.1.Key"] = key + + start := call.Start() + response, err := CallEcsRequest(resType, tagHandler.Client, tagHandler.Region, "RemoveTags", queryParams) + LoggingInfo(hiscallInfo, start) + + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + + cblogger.Infof("Successfully deleted %q Task\n", resIID.SystemId) + + expectStatus := false // 예상되는 상태 : 없어야 하므로 fasle + result, err := WaitForEcsTagExist(tagHandler.Client, tagHandler.Region, resType, resIID, key, expectStatus) + if err != nil { + return false, err + } + cblogger.Debug("Expect Status ", expectStatus, ", result Status ", result) + if !result { + return false, errors.New("waitForTagExist Error ") + } + case "cs": // cs : container service + + response, err := aliRemoveCsTag(tagHandler.CsClient, tagHandler.Region, resType, resIID, key) + if err != nil { + return false, err + } + cblogger.Debug("AddCsTags response", response) + + } + return true, nil +} + +// Find tags by tag key or value +// resType: ALL | VPC, SUBNET, etc.,. ecs기준 : Ddh, Disk, Eni, Image, Instance, KeyPair, LaunchTemplate, ReservedInstance, Securitygroup, Snapshot, SnapshotPolicy, Volume, +// keyword: The keyword to search for in the tag key or value. +// if you want to find all tags, set keyword to "" or "*". +// 해당 Resource Type에 tag가 있는 것들. ListTag는 resourceId가 있으나 당 function은 더 넒음 +func (tagHandler *AlibabaTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + var tagInfoList []*irs.TagInfo + + //start := call.Start() + //regionID := tagHandler.Region.Region + //regionID = "ap-northeast-1" // for the test + + // alibabaResourceType, err := GetAlibabaResourceType(resType) + // if err != nil { + // return tagInfoList, err + // } + + // alibabaApiType, err := GetAliTargetApi(resType) + // if err != nil { + // return tagInfoList, err + // } + + // switch alibabaApiType { + // case "ecs": + + // queryParams := map[string]string{} + // queryParams["RegionId"] = regionID + // queryParams["ResourceType"] = alibabaResourceType //string(resType) + // queryParams["Tag.1.Key"] = keyword + + // start := call.Start() + // response, err := CallEcsRequest(resType, tagHandler.Client, tagHandler.Region, "DescribeResourceByTags", queryParams) + // LoggingInfo(hiscallInfo, start) + + // if err != nil { + // cblogger.Error(err.Error()) + // LoggingError(hiscallInfo, err) + // } + // cblogger.Debug(response.GetHttpContentString()) + + // resTagResources := AliTagResourcesResponse{} + + // tagResponseStr := response.GetHttpContentString() + // err = json.Unmarshal([]byte(tagResponseStr), &resTagResources) + // if err != nil { + // cblogger.Error(err.Error()) + // return tagInfoList, nil + // } + // cblogger.Debug("resTagResources ", resTagResources) + // cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) + // cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) + + // for _, aliTagResource := range resTagResources.AliTagResources.Resources { + // cblogger.Debug("aliTagResource ", aliTagResource) + // aTagInfo, err := ExtractTagResourceInfo(&aliTagResource) + // if err != nil { + // cblogger.Error(err.Error()) + // continue + // } + + // aTagInfo.ResType = resType + + // cblogger.Debug("tagInfo ", aTagInfo) + // tagInfoList = append(tagInfoList, &aTagInfo) + // } + // case "cs": // cs : container service + // clusters, err := aliDescribeClustersV1(tagHandler.CsClient, regionID) + // if err != nil { + // cblogger.Error(err) + // LoggingError(hiscallInfo, err) + // return nil, err + // } + + // //cblogger.Debug("clusters ", clusters) + // // 모든 cluster를 돌면서 Tag 찾기 + // for _, cluster := range clusters { + // cblogger.Debug("inCluster ") + // for _, aliTag := range cluster.Tags { + // //cblogger.Debug("aliTag ", aliTag) + // //cblogger.Debug("keyword ", keyword) + // //cblogger.Debug("aliTag.Key ", *(aliTag.Key)) + // if *(aliTag.Key) == keyword { + // var tagInfo irs.TagInfo + // tagInfo.ResIId = irs.IID{SystemId: *cluster.ClusterId} + // tagInfo.ResType = resType + + // tagList := []irs.KeyValue{} + // tagList = append(tagList, irs.KeyValue{Key: "TagKey", Value: *aliTag.Key}) + // tagList = append(tagList, irs.KeyValue{Key: "TagValue", Value: *aliTag.Value}) + // tagInfo.TagList = tagList + // //cblogger.Debug("append Tag ", &tagInfo) + // tagInfoList = append(tagInfoList, &tagInfo) + // } + // } + // } + // } + + switch string(resType) { + case "ALL": + // for 모든 resource + default: + + tagInfo, err := FindTag(tagHandler, resType, keyword) + if err != nil { + cblogger.Error(err.Error()) + return tagInfoList, err + } + tagInfoList = append(tagInfoList, tagInfo) + } + + return tagInfoList, nil +} + +// 1개의 resource Type에 대한 Tag 정보 +func FindTag(tagHandler *AlibabaTagHandler, resType irs.RSType, keyword string) (*irs.TagInfo, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, keyword, "FindTag()") + var tagInfo *irs.TagInfo + + regionID := tagHandler.Region.Region + + alibabaResourceType, err := GetAlibabaResourceType(resType) + if err != nil { + return tagInfo, err + } + + alibabaApiType, err := GetAliTargetApi(resType) + if err != nil { + return tagInfo, err + } + + switch alibabaApiType { + case "ecs": + + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType //string(resType) + queryParams["Tag.1.Key"] = keyword + + start := call.Start() + response, err := CallEcsRequest(resType, tagHandler.Client, tagHandler.Region, "DescribeResourceByTags", queryParams) + LoggingInfo(hiscallInfo, start) + + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + + resTagResources := AliTagResourcesResponse{} + + tagResponseStr := response.GetHttpContentString() + err = json.Unmarshal([]byte(tagResponseStr), &resTagResources) + if err != nil { + cblogger.Error(err.Error()) + return tagInfo, nil + } + cblogger.Debug("resTagResources ", resTagResources) + cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) + cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) + + for _, aliTagResource := range resTagResources.AliTagResources.Resources { + cblogger.Debug("aliTagResource ", aliTagResource) + aTagInfo, err := ExtractTagResourceInfo(&aliTagResource) + if err != nil { + cblogger.Error(err.Error()) + continue + } + + aTagInfo.ResType = resType + + cblogger.Debug("tagInfo ", aTagInfo) + tagInfo = &aTagInfo + } + case "cs": // cs : container service + clusters, err := aliDescribeClustersV1(tagHandler.CsClient, regionID) + if err != nil { + cblogger.Error(err) + LoggingError(hiscallInfo, err) + return nil, err + } + + //cblogger.Debug("clusters ", clusters) + // 모든 cluster를 돌면서 Tag 찾기 + for _, cluster := range clusters { + cblogger.Debug("inCluster ") + for _, aliTag := range cluster.Tags { + //cblogger.Debug("aliTag ", aliTag) + //cblogger.Debug("keyword ", keyword) + //cblogger.Debug("aliTag.Key ", *(aliTag.Key)) + if *(aliTag.Key) == keyword { + var aTagInfo irs.TagInfo + aTagInfo.ResIId = irs.IID{SystemId: *cluster.ClusterId} + aTagInfo.ResType = resType + + tagList := []irs.KeyValue{} + tagList = append(tagList, irs.KeyValue{Key: "TagKey", Value: *aliTag.Key}) + tagList = append(tagList, irs.KeyValue{Key: "TagValue", Value: *aliTag.Value}) + aTagInfo.TagList = tagList + //cblogger.Debug("append Tag ", &tagInfo) + tagInfo = &aTagInfo + } + } + } + } + return tagInfo, nil +} + +/* +* + */ +func validateCreateTag(client *ecs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, resIID irs.IID, tag irs.KeyValue) error { + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, resIID.NameId, "GetTag()") + + regionID := regionInfo.Region + + // Check Tag Exists + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = string(resType) + queryParams["ResourceId"] = resIID.SystemId + queryParams["Tag.1.Key"] = tag.Key + + start := call.Start() + response, err := CallEcsRequest(resType, client, regionInfo, "DescribeTags", queryParams) + LoggingInfo(hiscallInfo, start) + + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + + return nil +} + +// tag 자체가 arr임. +func ExtractTagsDescribeInfo(aliTag *ecs.Tag) (irs.TagInfo, error) { + var tagInfo irs.TagInfo + //cblogger.Debug("tag ", aliTag) + //cblogger.Debug("TagKey ", aliTag.TagKey) + //cblogger.Debug("TagValue ", aliTag.TagValue) + + tagList := []irs.KeyValue{} + tagList = append(tagList, irs.KeyValue{Key: "TagKey", Value: aliTag.TagKey}) + tagList = append(tagList, irs.KeyValue{Key: "TagValue", Value: aliTag.TagValue}) + tagInfo.TagList = tagList + + // KeyValueList 추가 + keyValueList, errKeyValue := ConvertKeyValueList(aliTag) + if errKeyValue != nil { + cblogger.Error(errKeyValue) + } else { + tagInfo.KeyValueList = keyValueList + } + + cblogger.Debug("tagInfo ", tagInfo) + + return tagInfo, nil +} + +// FindTag의 경우 Tag가 아닌 TagResource 를 추출해야 함. +// func ExtractTagResourceInfo(tagResource *ecs.TagResource) (irs.TagInfo, error) { +func ExtractTagResourceInfo(tagResource *AliTagResource) (irs.TagInfo, error) { + + var tagInfo irs.TagInfo + //cblogger.Debug("tag ", aliTag) + //cblogger.Debug("TagKey ", aliTag.TagKey) + //cblogger.Debug("TagValue ", aliTag.TagValue) + + tagInfo.ResType = irs.RSType(tagResource.ResourceType) + tagInfo.ResIId = irs.IID{SystemId: tagResource.ResourceId} + + tagList := []irs.KeyValue{} + tagList = append(tagList, irs.KeyValue{Key: "TagKey", Value: tagResource.TagKey}) + tagList = append(tagList, irs.KeyValue{Key: "TagValue", Value: tagResource.TagValue}) + tagInfo.TagList = tagList + + return tagInfo, nil +} diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VPCHandler.go index 59e8b00b6..4eac1a0dc 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VPCHandler.go @@ -99,6 +99,16 @@ func (VPCHandler *AlibabaVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.V } retVpcInfo.IId.NameId = vpcReqInfo.IId.NameId // NameId는 요청 받은 값으로 리턴해야 함. + if vpcReqInfo.TagList != nil && len(vpcReqInfo.TagList) > 0 { + //tagHandler.Client, tagHandler.Region, resType, resIID, key + for _, vpcTag := range vpcReqInfo.TagList { + response, err := AddVpcTags(VPCHandler.Client, VPCHandler.Region, irs.RSType("VPC"), retVpcInfo.IId, vpcTag) + if err != nil { + cblogger.Error(errVpc) + } + cblogger.Debug("vpc add tag response ", response) + } + } return retVpcInfo, nil } From 5bd60dee86986b1ce239610d6fd96a46037d9fbb Mon Sep 17 00:00:00 2001 From: dogfootman Date: Tue, 16 Jul 2024 13:06:02 +0900 Subject: [PATCH 08/56] add tag test function --- .../drivers/alibaba/main/Test_Resources.go | 152 ++++++++++++++---- 1 file changed, 123 insertions(+), 29 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go index 1ae78f95e..30d9504c4 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go @@ -28,7 +28,7 @@ var cblogger *logrus.Logger func init() { // cblog is a global variable. cblogger = cblog.GetLogger("AlibabaCloud Resource Test") - cblog.SetLevel("info") + cblog.SetLevel("debug") } /* @@ -340,9 +340,9 @@ func handleSecurity() { //VmID := config.Aws.VmID //securityName := "CB-SecurityTestCidr" - securityName := "sg10" - securityId := "sg-6we0jr4qremmfu2wyd8q" - vpcId := "vpc-6weuepknbuvs90y6k1ss2" + securityName := "" + securityId := "" + vpcId := "" for { fmt.Println("Security Management") @@ -834,7 +834,7 @@ func handleKeyPair() { //config := readConfigFile() //VmID := config.Aws.VmID - keyPairName := "CB-KeyPairTest123123" + keyPairName := "" //keyPairName := config.Aws.KeyName for { @@ -1161,7 +1161,7 @@ func handleVM() { //config := readConfigFile() //VmID := irs.IID{NameId: config.Aws.BaseName, SystemId: config.Aws.VmID} - VmID := irs.IID{SystemId: "i-6weayupx7qvidhmyl48d"} + VmID := irs.IID{SystemId: ""} for { fmt.Println("VM Management") @@ -1190,20 +1190,20 @@ func handleVM() { case 1: vmReqInfo := irs.VMReqInfo{ - IId: irs.IID{NameId: "mcloud-barista-vm-test"}, + IId: irs.IID{NameId: ""}, //ImageIID: irs.IID{SystemId: "aliyun_3_x64_20G_alibase_20210425.vhd"}, //ImageIID: irs.IID{SystemId: "aliyun_2_1903_x64_20G_alibase_20200324.vhd"}, //ImageIID: irs.IID{SystemId: "ubuntu_18_04_x64_20G_alibase_20210318.vhd"}, ImageIID: irs.IID{SystemId: "ubuntu_18_04_x64_20G_alibase_20210420.vhd"}, //VpcIID: irs.IID{SystemId: "vpc-0jl4l19l51gn2exrohgci"}, //SubnetIID: irs.IID{SystemId: "vsw-0jlj155cbwhjumtipnm6d"}, - SubnetIID: irs.IID{SystemId: "vsw-6we8tac8w7dzqbxbyhj9o"}, //Tokyo Zone B - //SecurityGroupIIDs: []irs.IID{{SystemId: "sg-6we0rxnoai067qbkdkgw"}, {SystemId: "sg-6weeb9xaodr65g7bq10c"}}, - SecurityGroupIIDs: []irs.IID{{SystemId: "sg-6we7156yw8c8xbzi9f7v"}}, + SubnetIID: irs.IID{SystemId: ""}, //Tokyo Zone B + //SecurityGroupIIDs: []irs.IID{{SystemId: ""}, {SystemId: ""}}, + SecurityGroupIIDs: []irs.IID{{SystemId: ""}}, //VMSpecName: "ecs.t5-lc2m1.nano", //VMSpecName: "ecs.g6.large", //cn-wulanchabu 리전 VMSpecName: "ecs.t5-lc2m1.nano", //도쿄리전 - KeyPairIID: irs.IID{SystemId: "cb-japan"}, + KeyPairIID: irs.IID{SystemId: ""}, //VMUserId: "root", //root만 가능 //VMUserPasswd: "Cbuser!@#", //대문자 소문자 모두 사용되어야 함. 그리고 숫자나 특수 기호 중 하나가 포함되어야 함. @@ -1332,8 +1332,8 @@ func handleNLB() { cblogger.Info(handler) nlbReqInfo := irs.NLBInfo{ // TCP - IId: irs.IID{NameId: "New-CB-TCPNLB4"}, - VpcIID: irs.IID{SystemId: "vpc-t4naidq3kbofx4y09ignm"}, + IId: irs.IID{NameId: ""}, + VpcIID: irs.IID{SystemId: ""}, Type: "PUBLIC", Listener: irs.ListenerInfo{Protocol: "TCP", Port: "80"}, HealthChecker: irs.HealthCheckerInfo{Protocol: "HTTP", Port: "80", Interval: 5, Timeout: 2, Threshold: 3}, @@ -1341,12 +1341,12 @@ func handleNLB() { Protocol: "TCP", Port: "80", - VMs: &[]irs.IID{{SystemId: "i-t4ndzu5q27yow3xhk9ns"}, {SystemId: "i-t4ndzu5q27yow3xhk9nt"}}, + VMs: &[]irs.IID{{SystemId: ""}, {SystemId: ""}}, }, // UDP - //IId: irs.IID{NameId: "New-CB-UDPNLB2"}, - //VpcIID: irs.IID{SystemId: "vpc-t4naidq3kbofx4y09ignm"}, + //IId: irs.IID{NameId: ""}, + //VpcIID: irs.IID{SystemId: ""}, //Type: "PUBLIC", //Listener: irs.ListenerInfo{Protocol: "UDP", Port: "23"}, //HealthChecker: irs.HealthCheckerInfo{Protocol: "UDP", Port: "23", Interval: 5, Timeout: 2, Threshold: 3}, @@ -1354,11 +1354,11 @@ func handleNLB() { // //Protocol: "UDP", // //Port: "23", // - // VMs: &[]irs.IID{{SystemId: "i-t4ndzu5q27yow3xhk9ns"}, {SystemId: "i-t4ndzu5q27yow3xhk9nt"}}, + // VMs: &[]irs.IID{{SystemId: ""}, {SystemId: ""}}, //}, } - reqNLBId := irs.IID{SystemId: "lb-ecyd4pb5"} + reqNLBId := irs.IID{SystemId: ""} for { fmt.Println("Handler Management") @@ -1408,7 +1408,7 @@ func handleNLB() { } case 3: - reqNLBId = irs.IID{SystemId: "lb-gs5xf7uv5iiwzpwwpx39e"} + reqNLBId = irs.IID{SystemId: ""} cblogger.Infof("[%s] NLB 조회 테스트", reqNLBId) result, err := handler.GetNLB(reqNLBId) if err != nil { @@ -1419,7 +1419,7 @@ func handleNLB() { } case 4: - reqNLBId.SystemId = "lb-gs5x5ab796t61x95m7upp" + reqNLBId.SystemId = "" cblogger.Infof("[%s] NLB 삭제 테스트", reqNLBId) result, err := handler.DeleteNLB(reqNLBId) if err != nil { @@ -1430,8 +1430,8 @@ func handleNLB() { case 5: cblogger.Infof("[%s] VM 추가 테스트", reqNLBId) - reqNLBId.SystemId = "lb-gs5xf7uv5iiwzpwwpx39e" - vmIID := irs.IID{SystemId: "i-t4n771gareh1wzo7ghpe"} + reqNLBId.SystemId = "" + vmIID := irs.IID{SystemId: ""} result, err := handler.AddVMs(reqNLBId, &[]irs.IID{vmIID}) if err != nil { cblogger.Infof("VM 추가 실패 : ", err) @@ -1444,8 +1444,8 @@ func handleNLB() { case 6: cblogger.Infof("[%s] VM 삭제 테스트", reqNLBId.SystemId) - reqNLBId.SystemId = "lb-gs5xf7uv5iiwzpwwpx39e" - vmIID := irs.IID{SystemId: "i-t4n771gareh1wzo7ghpe"} + reqNLBId.SystemId = "" + vmIID := irs.IID{SystemId: ""} result, err := handler.RemoveVMs(reqNLBId, &[]irs.IID{vmIID}) if err != nil { cblogger.Infof("VM 삭제 실패 : ", err) @@ -1455,7 +1455,7 @@ func handleNLB() { case 7: cblogger.Infof("[%s] NLB VM Health 조회 테스트", reqNLBId) cblogger.Infof("[%s] VM 추가 테스트", reqNLBId) - reqNLBId.SystemId = "lb-gs5xf7uv5iiwzpwwpx39e" + reqNLBId.SystemId = "" result, err := handler.GetVMGroupHealthInfo(reqNLBId) if err != nil { cblogger.Infof("[%s] NLB VM Health 조회 실패 : ", reqNLBId.SystemId, err) @@ -1465,7 +1465,7 @@ func handleNLB() { } case 8: cblogger.Infof("[%s] NLB Listener 변경 테스트", reqNLBId) - reqNLBId.SystemId = "lb-gs5xf7uv5iiwzpwwpx39e" + reqNLBId.SystemId = "" changeListener := irs.ListenerInfo{} changeListener.Protocol = "tcp" changeListener.Port = "8080" // 포트만 변경 @@ -1489,7 +1489,7 @@ func handleNLB() { spew.Dump(result) } case 10: - reqNLBId.SystemId = "lb-gs5xf7uv5iiwzpwwpx39e" + reqNLBId.SystemId = "" reqHealthCheckInfo := irs.HealthCheckerInfo{ Protocol: "tcp", Port: "85", @@ -1634,11 +1634,104 @@ func handlePriceInfo() { } } +func handleTagInfo() { + cblogger.Debug("Start handleTagInfo Test") + ResourceHandler, err := testconf.GetResourceHandler("Tag") + if err != nil { + //panic(err) + cblogger.Error(err) + } + handler := ResourceHandler.(irs.TagHandler) + cblogger.Info(handler) + + for { + fmt.Println("Handler Management") + fmt.Println("0. Quit") + fmt.Println("1. ListTag List") + fmt.Println("2. GetTag ") + fmt.Println("3. FindTag ") + fmt.Println("4. AddTag ") + fmt.Println("5. RemoveTag ") + + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + panic(err) + } + + //resourceType := irs.RSType("VM") + // resourceIID := irs.IID{NameId: "vm-issue-test", SystemId: ""} + resourceType := irs.RSType("CLUSTER") + resourceIID := irs.IID{NameId: "cs-issue-test", SystemId: ""} + + if inputCnt == 1 { + switch commandNum { + case 0: + return + case 1: + + result, err := handler.ListTag(resourceType, resourceIID) + if err != nil { + cblogger.Infof(" Tag 목록 조회 실패 : ", err) + } else { + cblogger.Info("Tag 목록 조회 결과") + spew.Dump(result) + } + + case 2: + tagName := "tagntest3" + tagName = "" + result, err := handler.GetTag(resourceType, resourceIID, tagName) + if err != nil { + cblogger.Info(" Tag 조회 실패 : ", err) + } else { + cblogger.Info("GetTag 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + case 3: + tagName := "tagntest3" + tagName = "" + result, err := handler.FindTag(resourceType, tagName) + if err != nil { + cblogger.Info(" Tag 조회 실패 : ", err) + } else { + cblogger.Info("FindTag 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + case 4: + newTag := irs.KeyValue{} + newTag.Key = "addKeyT1" + newTag.Value = "addValueT1" + result, err := handler.AddTag(resourceType, resourceIID, newTag) + if err != nil { + cblogger.Info(" Tag 조회 실패 : ", err) + } else { + cblogger.Info("AddTag 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + case 5: + tagName := "addKeyT1" + result, err := handler.RemoveTag(resourceType, resourceIID, tagName) + if err != nil { + cblogger.Info(" Tag 조회 실패 : ", err) + } else { + cblogger.Info("RemoveTag 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + } + } + } +} + func main() { cblogger.Info("Alibaba Cloud Resource Test") cblogger.Debug("Debug mode") - //handleVPC() //VPC + handleVPC() //VPC //handleVMSpec() //handleImage() //AMI //handleSecurity() @@ -1649,7 +1742,8 @@ func main() { //handleVNic() //Lancard //handleRegionZone() - handlePriceInfo() + //handlePriceInfo() + //handleTagInfo() /* //StartTime := "2020-05-07T01:35:00Z" StartTime := "2020-05-07T01:35Z" From 88897cfdb2b0797f040d025e2c0dafc5502a3363 Mon Sep 17 00:00:00 2001 From: innodreamer Date: Tue, 16 Jul 2024 17:02:22 +0900 Subject: [PATCH 09/56] Change cblogger.Error to cblogger.Debug --- .../cloud-driver/drivers/ncpvpc/resources/VPCHandler.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VPCHandler.go index fa72f493a..c00b5acc6 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VPCHandler.go @@ -366,7 +366,7 @@ func (vpcHandler *NcpVpcVPCHandler) DeleteVPC(vpcIID irs.IID) (bool, error) { subnetStatus, err := vpcHandler.WaitForDeleteSubnet(vpcIID.SystemId, lastSubentIID) if err != nil { newErr := fmt.Errorf("Failed to Wait for Subnet Deletion : [%v]", err) - cblogger.Error(newErr.Error()) + cblogger.Debug(newErr.Error()) // For Termination Completion of a Subnet LoggingError(callLogInfo, newErr) // return false, newErr } @@ -611,7 +611,7 @@ func (vpcHandler *NcpVpcVPCHandler) GetNcpSubnetInfo(sunbnetId *string) (*vpc.Su if len(result.SubnetList) < 1 { newErr := fmt.Errorf("Failed to Get any Subnet Info with the ID!!") - cblogger.Error(newErr.Error()) + cblogger.Debug(newErr.Error()) // For Termination Completion of a Subnet LoggingError(callLogInfo, newErr) return nil, newErr } else { @@ -843,7 +843,7 @@ func (vpcHandler *NcpVpcVPCHandler) WaitForDeleteSubnet(vpcNo string, subnetIID ncpSubnetInfo, getErr := vpcHandler.GetNcpSubnetInfo(&subnetIID.SystemId) if getErr != nil { newErr := fmt.Errorf("Failed to Get the Subnet Info : [%v]", getErr) - cblogger.Error(newErr.Error()) + cblogger.Debug(newErr.Error()) // For Termination Completion of a Subnet return "", newErr } else { cblogger.Infof("Succeeded in Getting the Subnet Info of [%s]", subnetIID.SystemId) From 9d8fc09b92c242159eda2430accc8a67a878005c Mon Sep 17 00:00:00 2001 From: innodreamer Date: Tue, 16 Jul 2024 17:09:10 +0900 Subject: [PATCH 10/56] Remove Unnecessary Parameter on RegionZoneHandler (NCP VPC) --- .../cloud-driver/drivers/ncpvpc/resources/RegionZoneHandler.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/RegionZoneHandler.go b/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/RegionZoneHandler.go index 4f0a3107c..288851f9d 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/RegionZoneHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/RegionZoneHandler.go @@ -27,7 +27,6 @@ import ( ) type NcpRegionZoneHandler struct { - CredentialInfo idrv.CredentialInfo RegionInfo idrv.RegionInfo VMClient *vserver.APIClient } From c52d140de895112fe1f6b9a3c564b8bc35651354 Mon Sep 17 00:00:00 2001 From: innodreamer Date: Tue, 16 Jul 2024 17:13:56 +0900 Subject: [PATCH 11/56] Update for VM Status check when running GetVM(), ListVM() --- .../drivers/ncp/resources/VMHandler.go | 38 ++++++++----------- .../drivers/ncpvpc/resources/VMHandler.go | 38 ++++++++----------- 2 files changed, 32 insertions(+), 44 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go index 433dd0524..5cf94d903 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go @@ -533,11 +533,11 @@ func (vmHandler *NcpVMHandler) GetVM(vmIID irs.IID) (irs.VMInfo, error) { // Since it's impossible to get VM info. during Creation, ... switch string(curStatus) { - case "Creating", "Booting": + case "Creating": cblogger.Infof("Wait for the VM creation before inquiring VM info. The VM status : [%s]", string(curStatus)) - return irs.VMInfo{}, errors.New("The VM status is 'Creating' or 'Booting', wait for the VM creation before inquiring VM info. : " + vmIID.SystemId) + return irs.VMInfo{}, errors.New("The VM status is 'Creating', wait for the VM creation before inquiring VM info. : " + vmIID.SystemId) default: - cblogger.Infof("===> The VM status not 'Creating' or 'Booting', you can get the VM info.") + cblogger.Infof("===> The VM status not 'Creating', you can get the VM info.") } regionNo, err := vmHandler.GetRegionNo(vmHandler.RegionInfo.Region) @@ -969,7 +969,7 @@ func ConvertVMStatusString(vmStatus string) (irs.VMStatus, error) { //Caution!! resultStatus = "Booting" } else if strings.EqualFold(vmStatus, "setting up") { - resultStatus = "Creating" + resultStatus = "Setting_up" } else if strings.EqualFold(vmStatus, "running") { resultStatus = "Running" } else if strings.EqualFold(vmStatus, "shutting down") { @@ -1136,29 +1136,23 @@ func (vmHandler *NcpVMHandler) ListVM() ([]*irs.VMInfo, error) { cblogger.Debug(newErr.Error()) return nil, nil // Not returns Error message } else { - cblogger.Info("Succeeded in Getting ServerInstanceList!!") + cblogger.Info("Succeeded in Getting ServerInstanceList from NCP!!") } var vmInfoList []*irs.VMInfo for _, vm := range result.ServerInstanceList { - cblogger.Info("NCP VM Instance Info. inquiry : ", *vm.ServerInstanceNo) - - curStatus, errStatus := vmHandler.GetVMStatus(irs.IID{SystemId: *vm.ServerInstanceNo}) - if errStatus != nil { - rtnErr := logAndReturnError(callLogInfo, "Failed to Get the VM Status : ", errStatus) - return nil, rtnErr + curStatus, statusErr := vmHandler.GetVMStatus(irs.IID{SystemId: *vm.ServerInstanceNo}) + if statusErr != nil { + newErr := fmt.Errorf("Failed to Get the Status of VM : [%s], [%v]", *vm.ServerInstanceNo, statusErr.Error()) + cblogger.Error(newErr.Error()) + return nil, newErr } else { - cblogger.Infof("Succeeded to Get the VM Status of [%s] : [%s]", irs.IID{SystemId: *vm.ServerInstanceNo}, curStatus) + cblogger.Infof("Succeeded to Get the Status of VM [%s] : [%s]", *vm.ServerInstanceNo, string(curStatus)) } - cblogger.Info("===> VM Status : ", curStatus) + cblogger.Infof("===> VM Status : [%s]", string(curStatus)) - switch string(curStatus) { - case "Creating", "Booting": - cblogger.Errorf("The VM status : [%s], Can Not Get the VM info.", string(curStatus)) - return nil, nil - - default: - cblogger.Infof("===> The VM status not 'Creating' or 'Booting', you can get the VM info.") + if (string(curStatus) != "Creating") && (string(curStatus) != "Terminating") { + cblogger.Infof("===> The VM Status not 'Creating' or 'Terminating', you can get the VM info.") vmInfo, error := vmHandler.GetVM(irs.IID{SystemId: *vm.ServerInstanceNo}) if error != nil { cblogger.Error(error.Error()) @@ -1189,9 +1183,9 @@ func (vmHandler *NcpVMHandler) WaitToGetInfo(vmIID irs.IID) (irs.VMStatus, error cblogger.Infof("===> VM Status : [%s]", curStatus) switch string(curStatus) { - case "Creating", "Booting": + case "Creating", "Booting", "Setting_up": curRetryCnt++ - cblogger.Infof("The VM is still 'Creating', so wait for a second more before inquiring the VM info.") + cblogger.Infof("The VM is still 'Creating' and 'Booting', so wait for a second more before inquiring the VM info.") time.Sleep(time.Second * 5) if curRetryCnt > maxRetryCnt { cblogger.Errorf("Despite waiting for a long time(%d sec), the VM status is %s, so it is forcibly finishied.", maxRetryCnt, curStatus) diff --git a/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VMHandler.go index 5db0e66d5..e5d0d6e8c 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VMHandler.go @@ -62,7 +62,7 @@ func init() { func (vmHandler *NcpVpcVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, error) { cblogger.Info("NCPVPC Cloud driver: called StartVM()!!") InitLog() - callLogInfo := GetCallLogScheme(vmHandler.CredentialInfo.ClientId, call.VM, vmReqInfo.IId.NameId, "StartVM()") + callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Region, call.VM, vmReqInfo.IId.NameId, "StartVM()") if strings.EqualFold(vmReqInfo.IId.NameId, "") { newErr := fmt.Errorf("Invalid VM Name required") @@ -334,11 +334,11 @@ func (vmHandler *NcpVpcVMHandler) GetVM(vmIID irs.IID) (irs.VMInfo, error) { // Since it's impossible to get VM info. during Creation, ... switch string(curStatus) { - case "Creating", "Booting": + case "Creating": cblogger.Infof("The VM status is '%s', so wait for the VM creation before inquiring the info.", string(curStatus)) - return irs.VMInfo{}, errors.New("The VM status is 'Creating' or 'Booting', so wait for the VM creation before inquiring the info. : " + vmIID.SystemId) + return irs.VMInfo{}, errors.New("The VM status is 'Creating', so wait for the VM creation before inquiring the info. : " + vmIID.SystemId) default: - cblogger.Infof("===> The VM status is not 'Creating' or 'Booting', you can get the VM info.") + cblogger.Infof("===> The VM status is not 'Creating', you can get the VM info.") } /* @@ -776,7 +776,7 @@ func ConvertVMStatusString(vmStatus string) (irs.VMStatus, error) { //Caution!! resultStatus = "Booting" } else if strings.EqualFold(vmStatus, "setting up") { - resultStatus = "Creating" + resultStatus = "Setting_up" } else if strings.EqualFold(vmStatus, "running") { resultStatus = "Running" } else if strings.EqualFold(vmStatus, "shutting down") { @@ -901,36 +901,30 @@ func (vmHandler *NcpVpcVMHandler) ListVM() ([]*irs.VMInfo, error) { return nil, newErr } LoggingInfo(callLogInfo, callLogStart) - cblogger.Info("Succeeded in Getting ServerInstanceList from NCP VPC!!") + cblogger.Info("Succeeded in Getting ServerInstanceList from NCP!!") var vmInfoList []*irs.VMInfo for _, vm := range result.ServerInstanceList { - cblogger.Infof("Inquiry of NCP VM Instance info : [%s]", *vm.ServerInstanceNo) - curStatus, statusErr := vmHandler.GetVMStatus(irs.IID{SystemId: *vm.ServerInstanceNo}) if statusErr != nil { - cblogger.Errorf("Failed to Get the VM Status of VM : [%s]", *vm.ServerInstanceNo) - cblogger.Error(statusErr.Error()) + newErr := fmt.Errorf("Failed to Get the Status of VM : [%s], [%v]", *vm.ServerInstanceNo, statusErr.Error()) + cblogger.Error(newErr.Error()) + return nil, newErr } else { - cblogger.Infof("Succeed in Getting the VM Status of [%s] : [%s]", *vm.ServerInstanceNo, curStatus) + cblogger.Infof("Succeeded in Getting the Status of VM [%s] : [%s]", *vm.ServerInstanceNo, string(curStatus)) } - cblogger.Infof("===> VM Status : [%s]", curStatus) - - switch string(curStatus) { - case "Creating", "Booting": - return []*irs.VMInfo{}, nil + cblogger.Infof("===> VM Status : [%s]", string(curStatus)) - default: - cblogger.Infof("===> The VM status not 'Creating' or 'Booting', you can get the VM info.") + if (string(curStatus) != "Creating") && (string(curStatus) != "Terminating") { + cblogger.Infof("===> The VM Status not 'Creating' or 'Terminating', you can get the VM info.") vmInfo, error := vmHandler.GetVM(irs.IID{SystemId: *vm.ServerInstanceNo}) if error != nil { cblogger.Error(error.Error()) - return []*irs.VMInfo{}, error + return nil, error } vmInfoList = append(vmInfoList, &vmInfo) } } - return vmInfoList, nil } @@ -1349,9 +1343,9 @@ func (vmHandler *NcpVpcVMHandler) WaitToGetInfo(vmIID irs.IID) (irs.VMStatus, er cblogger.Infof("===> VM Status : [%s]", curStatus) switch string(curStatus) { - case "Creating", "Booting": + case "Creating", "Booting", "Setting_up": curRetryCnt++ - cblogger.Infof("The VM is 'Creating', so wait for a second more before inquiring the VM info.") + cblogger.Infof("The VM is 'Creating' and 'Booting', so wait for a second more before inquiring the VM info.") time.Sleep(time.Second * 5) if curRetryCnt > maxRetryCnt { cblogger.Errorf("Despite waiting for a long time(%d sec), the VM status is '%s', so it is forcibly finishied.", maxRetryCnt, curStatus) From 7dc385e4fe3062c6929f97a01eb4f30f967fed35 Mon Sep 17 00:00:00 2001 From: innodreamer Date: Tue, 16 Jul 2024 17:17:38 +0900 Subject: [PATCH 12/56] Update drvCapabilityInfo --- .../cloud-driver/drivers/ncp/NcpDriver.go | 14 ++++++++++---- .../cloud-driver/drivers/ncpvpc/NcpVpcDriver.go | 13 +++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ncp/NcpDriver.go b/cloud-control-manager/cloud-driver/drivers/ncp/NcpDriver.go index a95af3481..419574fa4 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncp/NcpDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/ncp/NcpDriver.go @@ -46,14 +46,20 @@ func (NcpDriver) GetDriverCapability() idrv.DriverCapabilityInfo { var drvCapabilityInfo idrv.DriverCapabilityInfo // NOTE Temporary Setting - drvCapabilityInfo.ImageHandler = true drvCapabilityInfo.VPCHandler = true - drvCapabilityInfo.SecurityHandler = true - drvCapabilityInfo.KeyPairHandler = true drvCapabilityInfo.VNicHandler = false + drvCapabilityInfo.ImageHandler = true + drvCapabilityInfo.VMSpecHandler = true + drvCapabilityInfo.SecurityHandler = true + drvCapabilityInfo.KeyPairHandler = true drvCapabilityInfo.PublicIPHandler = false drvCapabilityInfo.VMHandler = true - drvCapabilityInfo.VMSpecHandler = true + drvCapabilityInfo.DiskHandler = true + drvCapabilityInfo.MyImageHandler = true + drvCapabilityInfo.NLBHandler = true + drvCapabilityInfo.PriceInfoHandler = true + drvCapabilityInfo.RegionZoneHandler = true + drvCapabilityInfo.TagHandler = true return drvCapabilityInfo } diff --git a/cloud-control-manager/cloud-driver/drivers/ncpvpc/NcpVpcDriver.go b/cloud-control-manager/cloud-driver/drivers/ncpvpc/NcpVpcDriver.go index 72f444780..1542c82ad 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncpvpc/NcpVpcDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/ncpvpc/NcpVpcDriver.go @@ -46,15 +46,20 @@ func (NcpVpcDriver) GetDriverCapability() idrv.DriverCapabilityInfo { var drvCapabilityInfo idrv.DriverCapabilityInfo // NOTE Temporary Setting - drvCapabilityInfo.ImageHandler = true drvCapabilityInfo.VPCHandler = true - drvCapabilityInfo.SecurityHandler = true - drvCapabilityInfo.KeyPairHandler = true drvCapabilityInfo.VNicHandler = false + drvCapabilityInfo.ImageHandler = true + drvCapabilityInfo.VMSpecHandler = true + drvCapabilityInfo.SecurityHandler = true + drvCapabilityInfo.KeyPairHandler = true drvCapabilityInfo.PublicIPHandler = false drvCapabilityInfo.VMHandler = true - drvCapabilityInfo.VMSpecHandler = true + drvCapabilityInfo.DiskHandler = true + drvCapabilityInfo.MyImageHandler = true drvCapabilityInfo.NLBHandler = true + drvCapabilityInfo.PriceInfoHandler = true + drvCapabilityInfo.RegionZoneHandler = true + drvCapabilityInfo.TagHandler = false return drvCapabilityInfo } From 6847687e6305d593c28a796d14dd9b964de6ee4c Mon Sep 17 00:00:00 2001 From: innodreamer Date: Tue, 16 Jul 2024 17:18:46 +0900 Subject: [PATCH 13/56] Remove the Duplicate Codes --- .../drivers/ncp-plugin/NcpDriver-lib.go | 99 +------------- .../drivers/ncpvpc-plugin/NcpVpcDriver-lib.go | 124 +----------------- 2 files changed, 8 insertions(+), 215 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ncp-plugin/NcpDriver-lib.go b/cloud-control-manager/cloud-driver/drivers/ncp-plugin/NcpDriver-lib.go index 030afd1c8..828af5ab8 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncp-plugin/NcpDriver-lib.go +++ b/cloud-control-manager/cloud-driver/drivers/ncp-plugin/NcpDriver-lib.go @@ -7,104 +7,13 @@ // This is a Cloud Driver Example for PoC Test. // // by ETRI, 2020.08. +// by ETRI, 2024.07. package main import ( - //cblog "github.com/cloud-barista/cb-log" - - idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" - icon "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/connect" - - // "github.com/davecgh/go-spew/spew" - // unused import "github.com/sirupsen/logrus" - - ncloud "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" - server "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/server" - lb "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/loadbalancer" - - // ncpcon "github.com/cloud-barista/ncp/ncp/connect" - ncpcon "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/ncp/connect" //To be built in the container + "C" + ncp "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/ncp" ) -/* -var cblogger *logrus.Logger - -func init() { - // cblog is a global variable. - cblogger = cblog.GetLogger("NCP VMHandler") -} -*/ - -type NcpDriver struct { -} - -func (NcpDriver) GetDriverVersion() string { - return "TEST NCP DRIVER Version 1.0" -} - -func (NcpDriver) GetDriverCapability() idrv.DriverCapabilityInfo { - var drvCapabilityInfo idrv.DriverCapabilityInfo - - // NOTE Temporary Setting - drvCapabilityInfo.ImageHandler = true - drvCapabilityInfo.VPCHandler = true - drvCapabilityInfo.SecurityHandler = true - drvCapabilityInfo.KeyPairHandler = true - drvCapabilityInfo.VNicHandler = false - drvCapabilityInfo.PublicIPHandler = false - drvCapabilityInfo.VMHandler = true - drvCapabilityInfo.VMSpecHandler = true - - return drvCapabilityInfo -} - -// func getVMClient(credential idrv.CredentialInfo) (*server.APIClient, error) { -func getVMClient(connectionInfo idrv.ConnectionInfo) (*server.APIClient, error) { - // NOTE 주의!! - apiKeys := ncloud.APIKey{ - AccessKey: connectionInfo.CredentialInfo.ClientId, - SecretKey: connectionInfo.CredentialInfo.ClientSecret, - } - // Create NCP service client - client := server.NewAPIClient(server.NewConfiguration(&apiKeys)) - return client, nil -} - -func getLbClient(connectionInfo idrv.ConnectionInfo) (*lb.APIClient, error) { - apiKeys := ncloud.APIKey{ - AccessKey: connectionInfo.CredentialInfo.ClientId, - SecretKey: connectionInfo.CredentialInfo.ClientSecret, - } - // Create NCP Classic Load Balancer service client - client := lb.NewAPIClient(lb.NewConfiguration(&apiKeys)) - return client, nil -} - -func (driver *NcpDriver) ConnectCloud(connectionInfo idrv.ConnectionInfo) (icon.CloudConnection, error) { - // 1. get info of credential and region for Test A Cloud from connectionInfo. - // 2. create a client object(or service object) of Test A Cloud with credential info. - // 3. create CloudConnection Instance of "connect/TDA_CloudConnection". - // 4. return CloudConnection Interface of TDA_CloudConnection. - - vmClient, err := getVMClient(connectionInfo) - if err != nil { - return nil, err - } - - lbClient, err := getLbClient(connectionInfo) - if err != nil { - return nil, err - } - - iConn := ncpcon.NcpCloudConnection{ - CredentialInfo: connectionInfo.CredentialInfo, - RegionInfo: connectionInfo.RegionInfo, - VmClient: vmClient, - LbClient: lbClient, - } - - return &iConn, nil -} - -var CloudDriver NcpDriver +var CloudDriver ncp.NcpDriver diff --git a/cloud-control-manager/cloud-driver/drivers/ncpvpc-plugin/NcpVpcDriver-lib.go b/cloud-control-manager/cloud-driver/drivers/ncpvpc-plugin/NcpVpcDriver-lib.go index 7866929d5..007dc022f 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncpvpc-plugin/NcpVpcDriver-lib.go +++ b/cloud-control-manager/cloud-driver/drivers/ncpvpc-plugin/NcpVpcDriver-lib.go @@ -8,129 +8,13 @@ // // by ETRI, 2020.12. // by ETRI, 2022.03. updated +// by ETRI, 2024.07. package main import ( - // "github.com/davecgh/go-spew/spew" - "github.com/sirupsen/logrus" - - cblog "github.com/cloud-barista/cb-log" - idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" - icon "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/connect" - - ncloud "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" - vserver "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vserver" - vpc "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vpc" - vlb "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vloadbalancer" - - // ncpvpccon "github.com/cloud-barista/ncpvpc/ncpvpc/connect" // For local testing - ncpvpccon "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/ncpvpc/connect" + "C" + ncpvpc "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/ncpvpc" ) -var cblogger *logrus.Logger - -func init() { - // cblog is a global variable. - cblogger = cblog.GetLogger("NCPVPC Handler") -} - -type NcpVpcDriver struct { -} - -func (NcpVpcDriver) GetDriverVersion() string { - return "TEST NCP VPC DRIVER Version 1.0" -} - -func (NcpVpcDriver) GetDriverCapability() idrv.DriverCapabilityInfo { - var drvCapabilityInfo idrv.DriverCapabilityInfo - - // NOTE Temporary Setting - drvCapabilityInfo.ImageHandler = true - drvCapabilityInfo.VPCHandler = true - drvCapabilityInfo.SecurityHandler = true - drvCapabilityInfo.KeyPairHandler = true - drvCapabilityInfo.VNicHandler = false - drvCapabilityInfo.PublicIPHandler = false - drvCapabilityInfo.VMHandler = true - drvCapabilityInfo.VMSpecHandler = true - drvCapabilityInfo.NLBHandler = true - - return drvCapabilityInfo -} - -func getVmClient(connectionInfo idrv.ConnectionInfo) (*vserver.APIClient, error) { - - // NOTE 주의!! - apiKeys := ncloud.APIKey{ - AccessKey: connectionInfo.CredentialInfo.ClientId, - SecretKey: connectionInfo.CredentialInfo.ClientSecret, - } - - // NOTE for just test - // cblogger.Info(apiKeys.AccessKey) - // cblogger.Info(apiKeys.SecretKey) - - // Create NCPVPC service client - client := vserver.NewAPIClient(vserver.NewConfiguration(&apiKeys)) - - return client, nil -} - -func getVpcClient(connectionInfo idrv.ConnectionInfo) (*vpc.APIClient, error) { - apiKeys := ncloud.APIKey{ - AccessKey: connectionInfo.CredentialInfo.ClientId, - SecretKey: connectionInfo.CredentialInfo.ClientSecret, - } - - // Create NCP VPC service client - client := vpc.NewAPIClient(vpc.NewConfiguration(&apiKeys)) - - return client, nil -} - -func getVlbClient(connectionInfo idrv.ConnectionInfo) (*vlb.APIClient, error) { - apiKeys := ncloud.APIKey{ - AccessKey: connectionInfo.CredentialInfo.ClientId, - SecretKey: connectionInfo.CredentialInfo.ClientSecret, - } - - // Create NCP VPC Load Balancer service client - client := vlb.NewAPIClient(vlb.NewConfiguration(&apiKeys)) - - return client, nil -} - -func (driver *NcpVpcDriver) ConnectCloud(connectionInfo idrv.ConnectionInfo) (icon.CloudConnection, error) { - // 1. get info of credential and region for Test A Cloud from connectionInfo. - // 2. create a client object(or service object) of Test A Cloud with credential info. - // 3. create CloudConnection Instance of "connect/TDA_CloudConnection". - // 4. return CloudConnection Interface of TDA_CloudConnection. - - vmClient, err := getVmClient(connectionInfo) - if err != nil { - return nil, err - } - - vpcClient, err := getVpcClient(connectionInfo) - if err != nil { - return nil, err - } - - vlbClient, err := getVlbClient(connectionInfo) - if err != nil { - return nil, err - } - - iConn := ncpvpccon.NcpVpcCloudConnection{ - CredentialInfo: connectionInfo.CredentialInfo, - RegionInfo: connectionInfo.RegionInfo, - VmClient: vmClient, - VpcClient: vpcClient, - VlbClient: vlbClient, - } - - return &iConn, nil -} - -var CloudDriver NcpVpcDriver +var CloudDriver ncpvpc.NcpVpcDriver From 110e7f3a140779545519a07de50a47d9dad03095 Mon Sep 17 00:00:00 2001 From: hippo-an Date: Wed, 17 Jul 2024 10:50:49 +0900 Subject: [PATCH 14/56] update tag handler for FindTag method --- .../cloud-driver/drivers/gcp/GCPDriver.go | 2 +- .../drivers/gcp/main/Test_Resources.go | 25 +- .../drivers/gcp/resources/TagHandler.go | 232 ++++++++++++++---- 3 files changed, 205 insertions(+), 54 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go b/cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go index 2f5dc9fa6..43fc64929 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/GCPDriver.go @@ -64,7 +64,7 @@ func (GCPDriver) GetDriverCapability() idrv.DriverCapabilityInfo { drvCapabilityInfo.ClusterHandler = true drvCapabilityInfo.PriceInfoHandler = true drvCapabilityInfo.TagHandler = true - drvCapabilityInfo.TagSupportResourceType = []ires.RSType{ires.VM, ires.DISK, ires.CLUSTER} + drvCapabilityInfo.TagSupportResourceType = []ires.RSType{ires.ALL, ires.VM, ires.DISK, ires.CLUSTER} return drvCapabilityInfo } diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go index fbd3f2fa4..fd9c0eefe 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go @@ -1998,6 +1998,7 @@ func handleTags() { fmt.Println("2. add tags") fmt.Println("3. remove tags") fmt.Println("4. get tags") + fmt.Println("5. find tags") var commandNum int inputCnt, err := fmt.Scan(&commandNum) @@ -2007,7 +2008,7 @@ func handleTags() { reader := bufio.NewReader(os.Stdin) - fmt.Println("resource type [v(m)/d(isk)/c(luster - temporary not usable)]: ") + fmt.Println("resource type [a(ll)/v(m)/d(isk)/c(luster - temporary not usable)]: ") key, err := reader.ReadString('\n') if err != nil { @@ -2023,6 +2024,8 @@ func handleTags() { } else if strings.EqualFold(strings.ToLower(key), "d") { sampleId = "mcmp-demo" sampleType = irs.DISK + } else if strings.EqualFold(strings.ToLower(key), "a") { + sampleType = irs.ALL } else { fmt.Println(errors.New("chose vm or disk currently")) continue @@ -2112,7 +2115,27 @@ func handleTags() { } fmt.Printf("%+v\n", r) + case 5: + + reader := bufio.NewReader(os.Stdin) + + fmt.Println("key name : ") + keyword, err := reader.ReadString('\n') + if err != nil { + panic(err) + } + keyword = strings.TrimSpace(keyword) + + re, err := handler.FindTag(sampleType, keyword) + if err != nil { + fmt.Println("error while call get tag", err) + continue + } + + for _, r := range re { + fmt.Printf("%+v\n", *r) + } } } } diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go index f4e9d4909..914b76a02 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/resources/TagHandler.go @@ -389,57 +389,185 @@ func (t *GCPTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string return false, errors.New("unsupported resource type") } } +func (t *GCPTagHandler) getVms() ([]*compute.Instance, error) { + vms, err := t.ComputeClient.Instances.List(t.Credential.ProjectID, t.Region.Zone).Do() + if err != nil { + return nil, err + } + + return vms.Items, nil +} + +func (t *GCPTagHandler) getDisks() ([]*compute.Disk, error) { + disks, err := t.ComputeClient.Disks.List(t.Credential.ProjectID, t.Region.Zone).Do() + if err != nil { + return nil, err + } + return disks.Items, nil +} + +func (t *GCPTagHandler) getClusters() ([]*container.Cluster, error) { + parent := getParentAtContainer(t.Credential.ProjectID, t.Region.Zone) + clusters, err := t.ContainerClient.Projects.Locations.Clusters.List(parent).Do() + if err != nil { + return nil, err + } + + return clusters.Clusters, nil +} + func (t *GCPTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { - // err := validateSupportRS(resType) - // errRes := []*irs.TagInfo{} - // if err != nil { - // return errRes, err - // } - - // projectId := t.Credential.ProjectID - // zone := t.Region.Zone - // switch resType { - // case irs.VM: - // vms, err := t.ComputeClient.Instances.List(projectId, zone).Do() - // if err != nil { - // return errRes, err - // } - - // for _, i := range vms.Items { - // irs.TagInfo{ - // ResType: resType, - // ResIId: irs.IID{ - // NameId: "", - // SystemId: "", - // }, - // } - // for k, v := range i.Labels { - // if strings.Contains(k, keyword) || strings.Contains(v, keyword) { - - // irs.KeyValue{ - - // } - - // } - // } - // } - - // case irs.DISK: - // disks, err := t.ComputeClient.Disks.List(projectId, zone).Do() - // if err != nil { - // return errRes, err - // } - - // case irs.CLUSTER: - // parent := getParentAtContainer(projectId, zone) - // clusters, err := t.ContainerClient.Projects.Locations.Clusters.List(parent).Do() - // if err != nil { - // return errRes, err - // } - - // default: - - // } - - return []*irs.TagInfo{}, nil + var res []*irs.TagInfo + var err error + + if resType == irs.ALL { + res1, err := getResult( + keyword, + irs.VM, + t.getVms, + func(item *compute.Instance) string { + return item.Name + }, + func(item *compute.Instance) map[string]string { + return item.Labels + }, + ) + + if err != nil { + return res, err + } + + res = append(res, res1...) + res2, err := getResult( + keyword, + irs.DISK, + t.getDisks, + func(item *compute.Disk) string { + return item.Name + }, + func(item *compute.Disk) map[string]string { + return item.Labels + }, + ) + + if err != nil { + return res, err + } + res = append(res, res2...) + res3, err := getResult( + keyword, + irs.CLUSTER, + t.getClusters, + func(item *container.Cluster) string { + return item.Name + }, + func(item *container.Cluster) map[string]string { + return item.ResourceLabels + }, + ) + + if err != nil { + return res, err + } + res = append(res, res3...) + } else { + err = validateSupportRS(resType) + if err != nil { + return res, err + } + + switch resType { + case irs.VM: + res, err = getResult( + keyword, + resType, + t.getVms, + func(item *compute.Instance) string { + return item.Name + }, + func(item *compute.Instance) map[string]string { + return item.Labels + }, + ) + + if err != nil { + return res, err + } + case irs.DISK: + res, err = getResult( + keyword, + resType, + t.getDisks, + func(item *compute.Disk) string { + return item.Name + }, + func(item *compute.Disk) map[string]string { + return item.Labels + }, + ) + + if err != nil { + return res, err + } + case irs.CLUSTER: + res, err = getResult( + keyword, + resType, + t.getClusters, + func(item *container.Cluster) string { + return item.Name + }, + func(item *container.Cluster) map[string]string { + return item.ResourceLabels + }, + ) + + if err != nil { + return res, err + } + + default: + return nil, errors.New("unsupport resource type") + } + } + + return res, nil +} + +func getResult[T *compute.Instance | *compute.Disk | *container.Cluster]( + keyword string, + resType irs.RSType, + resultFn func() ([]T, error), + getNameFn func(item T) string, + getLabelFn func(item T) map[string]string, +) ([]*irs.TagInfo, error) { + var res []*irs.TagInfo + items, err := resultFn() + if err != nil { + return res, err + } + + for _, item := range items { + name := getNameFn(item) + var ti *irs.TagInfo + for k, v := range getLabelFn(item) { + if strings.Contains(k, keyword) || strings.Contains(v, keyword) { + if ti == nil { + ti = &irs.TagInfo{ + ResType: resType, + ResIId: irs.IID{ + NameId: name, + SystemId: name, + }, + } + } + ti.TagList = append(ti.TagList, irs.KeyValue{Key: k, Value: v}) + } + } + if ti != nil { + res = append(res, ti) + } + } + + return res, nil } From d3377951d8e131fbda229bf69b23139129b21fdb Mon Sep 17 00:00:00 2001 From: dev4unet Date: Wed, 17 Jul 2024 07:46:29 +0000 Subject: [PATCH 15/56] Handle Tag values in each handler's constructor --- .../drivers/aws/resources/ClusterHandler.go | 6 ++ .../drivers/aws/resources/CommonAwsFunc.go | 67 ++++++++++++++++- .../drivers/aws/resources/DiskHandler.go | 35 +++++---- .../drivers/aws/resources/KeyPairHandler.go | 13 +++- .../drivers/aws/resources/MyImageHandler.go | 44 ++++++++---- .../drivers/aws/resources/NLBHandler.go | 71 +++++++++++++++++-- .../drivers/aws/resources/SecurityHandler.go | 10 ++- .../drivers/aws/resources/VMHandler.go | 52 +++++++++----- .../drivers/aws/resources/VPCHandler.go | 28 ++++++-- 9 files changed, 263 insertions(+), 63 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go index 21913745e..c3c395584 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go @@ -67,6 +67,11 @@ func (ClusterHandler *AwsClusterHandler) CreateCluster(clusterReqInfo irs.Cluste reqK8sVersion := clusterReqInfo.Version + tagsMap, err := ConvertTagListToTagsMap(clusterReqInfo.TagList, clusterReqInfo.IId.NameId) + if err != nil { + return irs.ClusterInfo{}, fmt.Errorf("failed to convert tags map: %w", err) + } + // create cluster input := &eks.CreateClusterInput{ Name: aws.String(clusterReqInfo.IId.NameId), @@ -77,6 +82,7 @@ func (ClusterHandler *AwsClusterHandler) CreateCluster(clusterReqInfo irs.Cluste //RoleArn: aws.String("arn:aws:iam::012345678910:role/eks-service-role-AWSServiceRoleForAmazonEKS-J7ONKE3BQ4PI"), //RoleArn: aws.String(roleArn), RoleArn: roleArn, + Tags: tagsMap, } //EKS버전 처리(Spider 입력 값 형태 : "1.23.4" / AWS 버전 형태 : "1.23") diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonAwsFunc.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonAwsFunc.go index 683dd7de9..1038689ee 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonAwsFunc.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonAwsFunc.go @@ -386,7 +386,7 @@ func ConvertKeyValueList(v interface{}) ([]irs.KeyValue, error) { //cblogger.Debugf("Key[%s]의 값은 변환 불가 - [%s]", k, errString) //요구에 의해서 Error에서 Warn으로 낮춤 continue } - keyValueList = append(keyValueList, irs.KeyValue{k, value}) + keyValueList = append(keyValueList, irs.KeyValue{Key: k, Value: value}) /* _, ok := v.(string) @@ -504,3 +504,68 @@ func ReplaceEmptyWithNAforLoadBalancerNetwork(obj interface{}) { } } } + +// Function to convert tag list to tag specifications +// nameValue : Automatically adds the "Name" Tag when there is no "Name" Tag in the tagList, and specifies the value you want to use for the "Name" Tag. +func ConvertTagListToTagSpecifications(resourceType string, tagList []irs.KeyValue, nameValue ...string) ([]*ec2.TagSpecification, error) { + // Convert KeyValue list to ec2.Tag list + var ec2Tags []*ec2.Tag + for _, kv := range tagList { + ec2Tags = append(ec2Tags, &ec2.Tag{ + Key: aws.String(kv.Key), + Value: aws.String(kv.Value), + }) + } + + // Add a "Name" tag if nameValue is provided using a variable argument + if len(nameValue) > 0 && nameValue[0] != "" { + nameTagExists := false + for _, tag := range ec2Tags { + if *tag.Key == "Name" { + nameTagExists = true + break + } + } + if !nameTagExists { + ec2Tags = append(ec2Tags, &ec2.Tag{ + Key: aws.String("Name"), + Value: aws.String(nameValue[0]), + }) + } + } + + // If no tags are provided, return an empty slice + if len(ec2Tags) == 0 { + return []*ec2.TagSpecification{}, nil + } + + // Create TagSpecifications + tagSpecifications := []*ec2.TagSpecification{ + { + ResourceType: aws.String(resourceType), + Tags: ec2Tags, + }, + } + + return tagSpecifications, nil +} + +// Function to ConvertTagListToTagsMap +// nameValue : Automatically adds the "Name" Tag when there is no "Name" Tag in the tagList, and specifies the value you want to use for the "Name" Tag. +func ConvertTagListToTagsMap(tagList []irs.KeyValue, nameValue ...string) (map[string]*string, error) { + tagsMap := make(map[string]*string) + + // tagList Convert a list of tags to tagsMap + for _, kv := range tagList { + tagsMap[kv.Key] = aws.String(kv.Value) + } + + // If nameValue is provided using a variable argument, add a "Name" Tag if it doesn't already exist + if len(nameValue) > 0 && nameValue[0] != "" { + if _, exists := tagsMap["Name"]; !exists { + tagsMap["Name"] = aws.String(nameValue[0]) + } + } + + return tagsMap, nil +} diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go index 65a5cbde2..36f4057bb 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go @@ -4,6 +4,7 @@ package resources import ( "errors" + "fmt" "reflect" "strconv" "strings" @@ -71,25 +72,33 @@ func (DiskHandler *AwsDiskHandler) CreateDisk(diskReqInfo irs.DiskInfo) (irs.Dis volumeSize, _ := strconv.ParseInt(diskReqInfo.DiskSize, 10, 64) volumeType := diskReqInfo.DiskType - // volume 이름을 위해 Tag 지정. - tag := &ec2.Tag{ - Key: aws.String(VOLUME_TAG_DEFAULT), - Value: &diskReqInfo.IId.NameId, - } + /* + // volume 이름을 위해 Tag 지정. + tag := &ec2.Tag{ + Key: aws.String(VOLUME_TAG_DEFAULT), + Value: &diskReqInfo.IId.NameId, + } - var tags []*ec2.Tag - tags = append(tags, tag) - tagSpec := &ec2.TagSpecification{ - ResourceType: aws.String(RESOURCE_TYPE_VOLUME), - Tags: tags, + var tags []*ec2.Tag + tags = append(tags, tag) + tagSpec := &ec2.TagSpecification{ + ResourceType: aws.String(RESOURCE_TYPE_VOLUME), + Tags: tags, + } + var tagSpecs []*ec2.TagSpecification + tagSpecs = append(tagSpecs, tagSpec) + */ + + // Convert TagList to TagSpecifications + tagSpecifications, err := ConvertTagListToTagSpecifications(*aws.String(RESOURCE_TYPE_VOLUME), diskReqInfo.TagList, diskReqInfo.IId.NameId) + if err != nil { + return irs.DiskInfo{}, fmt.Errorf("failed to convert tag list: %w", err) } - var tagSpecs []*ec2.TagSpecification - tagSpecs = append(tagSpecs, tagSpec) input := &ec2.CreateVolumeInput{ AvailabilityZone: aws.String(zone), Size: aws.Int64(volumeSize), - TagSpecifications: tagSpecs, + TagSpecifications: tagSpecifications, } switch diskReqInfo.DiskType { diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go index 551ee0a71..337198191 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go @@ -106,6 +106,12 @@ func (keyPairHandler *AwsKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPairReq } */ + // Convert TagList to TagSpecifications + tagSpecifications, err := ConvertTagListToTagSpecifications("key-pair", keyPairReqInfo.TagList) + if err != nil { + return irs.KeyPairInfo{}, fmt.Errorf("failed to convert tag list: %w", err) + } + // logger for HisCall callogger := call.GetLogger("HISCALL") callLogInfo := call.CLOUDLOGSCHEMA{ @@ -121,7 +127,8 @@ func (keyPairHandler *AwsKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPairReq // Creates a new key pair with the given name result, err := keyPairHandler.Client.CreateKeyPair(&ec2.CreateKeyPairInput{ //KeyName: aws.String(keyPairReqInfo.Name), - KeyName: aws.String(keyPairReqInfo.IId.NameId), + KeyName: aws.String(keyPairReqInfo.IId.NameId), + TagSpecifications: tagSpecifications, }) callLogInfo.ElapsedTime = call.Elapsed(callLogStart) @@ -250,7 +257,7 @@ func (keyPairHandler *AwsKeyPairHandler) GetKey(keyIID irs.IID) (irs.KeyPairInfo cblogger.Debug("aerr.Code() : ", aerr.Code()) cblogger.Debug("ok : ", ok) switch aerr.Code() { - default: + default: cblogger.Error(aerr.Error()) return irs.KeyPairInfo{}, aerr } @@ -259,7 +266,7 @@ func (keyPairHandler *AwsKeyPairHandler) GetKey(keyIID irs.IID) (irs.KeyPairInfo cblogger.Error(err.Error()) return irs.KeyPairInfo{}, err } - return irs.KeyPairInfo{}, nil + //return irs.KeyPairInfo{}, nil } callogger.Info(call.String(callLogInfo)) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go index 52d15e2d0..8bdca4c6b 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go @@ -259,26 +259,43 @@ func (ImageHandler *AwsMyImageHandler) SnapshotVM(snapshotReqInfo irs.MyImageInf blockDeviceMappingList = append(blockDeviceMappingList, &blockDeviceMapping) } - var tags []*ec2.Tag - nameTag := &ec2.Tag{ - Key: aws.String(IMAGE_TAG_DEFAULT), - Value: aws.String(snapshotReqInfo.IId.NameId), + /* + var tags []*ec2.Tag + nameTag := &ec2.Tag{ + Key: aws.String(IMAGE_TAG_DEFAULT), + Value: aws.String(snapshotReqInfo.IId.NameId), + } + tags = append(tags, nameTag) + sourceVMTag := &ec2.Tag{ + Key: aws.String(IMAGE_TAG_SOURCE_VM), + Value: aws.String(snapshotReqInfo.SourceVM.SystemId), + } + tags = append(tags, sourceVMTag) + tagSpec := &ec2.TagSpecification{ + ResourceType: aws.String(RESOURCE_TYPE_MYIMAGE), + Tags: tags, + } + var tagSpecs []*ec2.TagSpecification + tagSpecs = append(tagSpecs, tagSpec) + */ + + // Convert TagList to TagSpecifications + tagSpecifications, err := ConvertTagListToTagSpecifications(RESOURCE_TYPE_MYIMAGE, snapshotReqInfo.TagList, snapshotReqInfo.IId.NameId) + if err != nil { + return irs.MyImageInfo{}, fmt.Errorf("failed to convert tag list: %w", err) } - tags = append(tags, nameTag) + + // Add new tags to results sourceVMTag := &ec2.Tag{ Key: aws.String(IMAGE_TAG_SOURCE_VM), Value: aws.String(snapshotReqInfo.SourceVM.SystemId), } - tags = append(tags, sourceVMTag) - tagSpec := &ec2.TagSpecification{ - ResourceType: aws.String(RESOURCE_TYPE_MYIMAGE), - Tags: tags, + + if len(tagSpecifications) > 0 { + tagSpecifications[0].Tags = append(tagSpecifications[0].Tags, sourceVMTag) } - var tagSpecs []*ec2.TagSpecification - tagSpecs = append(tagSpecs, tagSpec) // Image parameter set - input := &ec2.CreateImageInput{ //BlockDeviceMappings: []*ec2.BlockDeviceMapping{ // { @@ -300,7 +317,8 @@ func (ImageHandler *AwsMyImageHandler) SnapshotVM(snapshotReqInfo irs.MyImageInf InstanceId: aws.String(snapshotReqInfo.SourceVM.SystemId), Description: aws.String(snapshotReqInfo.IId.NameId), BlockDeviceMappings: blockDeviceMappingList, - TagSpecifications: tagSpecs, + //TagSpecifications: tagSpecs, + TagSpecifications: tagSpecifications, } cblogger.Debug(input) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go index f934915f9..b4e57b06d 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go @@ -44,7 +44,51 @@ type TargetGroupInfo struct { */ } +// Function to convert tag list to ec2 tags array +// nameValue : Automatically adds the "Name" Tag when there is no "Name" Tag in the tagList, and specifies the value you want to use for the "Name" Tag. +func ConvertTagListToTags(tagList []irs.KeyValue, nameValue ...string) ([]*elbv2.Tag, error) { + // Convert KeyValue list to ec2.Tag list + var elbTags []*elbv2.Tag + for _, kv := range tagList { + elbTags = append(elbTags, &elbv2.Tag{ + Key: aws.String(kv.Key), + Value: aws.String(kv.Value), + }) + } + + // Add a "Name" tag if nameValue is provided using a variable argument + if len(nameValue) > 0 && nameValue[0] != "" { + nameTagExists := false + for _, tag := range elbTags { + if *tag.Key == "Name" { + nameTagExists = true + break + } + } + if !nameTagExists { + elbTags = append(elbTags, &elbv2.Tag{ + Key: aws.String("Name"), + Value: aws.String(nameValue[0]), + }) + } + } + + // If no tags are provided, return an empty slice + if len(elbTags) == 0 { + return []*elbv2.Tag{}, nil + } + + return elbTags, nil +} + func (NLBHandler *AwsNLBHandler) CreateListener(nlbReqInfo irs.NLBInfo) (*elbv2.CreateListenerOutput, error) { + + // Convert TagList to ec2 Tags array + tags, err := ConvertTagListToTags(nlbReqInfo.TagList, nlbReqInfo.IId.NameId) + if err != nil { + return nil, fmt.Errorf("failed to convert tag list: %w", err) + } + input := &elbv2.CreateListenerInput{ DefaultActions: []*elbv2.Action{ { @@ -55,6 +99,7 @@ func (NLBHandler *AwsNLBHandler) CreateListener(nlbReqInfo irs.NLBInfo) (*elbv2. LoadBalancerArn: aws.String(nlbReqInfo.IId.SystemId), //생성된 NLB의 ARN 값 //Port: aws.Int64(80), //숫자 값 검증 후 적용 Protocol: aws.String(nlbReqInfo.Listener.Protocol), // AWS NLB : TCP, TLS, UDP, or TCP_UDP + Tags: tags, } //리스너 포트 포메팅 검증 및 셋팅 @@ -129,6 +174,12 @@ func (NLBHandler *AwsNLBHandler) CreateListener(nlbReqInfo irs.NLBInfo) (*elbv2. } func (NLBHandler *AwsNLBHandler) CreateTargetGroup(nlbReqInfo irs.NLBInfo) (*elbv2.CreateTargetGroupOutput, error) { + // Convert TagList to ec2 Tags array + tags, err := ConvertTagListToTags(nlbReqInfo.TagList, nlbReqInfo.IId.NameId) + if err != nil { + return nil, fmt.Errorf("failed to convert tag list: %w", err) + } + input := &elbv2.CreateTargetGroupInput{ Name: aws.String(nlbReqInfo.IId.NameId), TargetType: aws.String("instance"), // instance , ip, lambda @@ -142,6 +193,7 @@ func (NLBHandler *AwsNLBHandler) CreateTargetGroup(nlbReqInfo irs.NLBInfo) (*elb HealthCheckPort: aws.String(nlbReqInfo.HealthChecker.Port), //HealthCheckIntervalSeconds: aws.Int64(int64(nlbReqInfo.HealthChecker.Interval)), // 5초이상 // 0 이상의 값이 있을 때만 설정하도록 변경 //HealthCheckTimeoutSeconds: aws.Int64(int64(nlbReqInfo.HealthChecker.Timeout)), // 0 이상의 값이 있을 때만 설정하도록 변경 + Tags: tags, } //AWS TargetGroup 포트 포메팅 검증 및 셋팅 @@ -314,7 +366,7 @@ func (NLBHandler *AwsNLBHandler) ExtractVmSubnets(VMs *[]irs.IID) ([]*string, er //최종 사용할 서브넷 목록만 추출함. subnetList := []*string{} for key, val := range mapZone { - cblogger.Debug("AZ[%s] Subnet[%s]", key, val) + cblogger.Debugf("AZ[%s] Subnet[%s]", key, val) subnetList = append(subnetList, aws.String(val)) } @@ -427,6 +479,12 @@ func (NLBHandler *AwsNLBHandler) CreateNLB(nlbReqInfo irs.NLBInfo) (irs.NLBInfo, return irs.NLBInfo{}, errVmInfo } + // Convert TagList to ec2 Tags array + tags, err := ConvertTagListToTags(nlbReqInfo.TagList, nlbReqInfo.IId.NameId) + if err != nil { + return irs.NLBInfo{}, fmt.Errorf("failed to convert tag list: %w", err) + } + input := &elbv2.CreateLoadBalancerInput{ Name: aws.String(nlbReqInfo.IId.NameId), Type: aws.String("network"), //NLB 생성 @@ -443,6 +501,7 @@ func (NLBHandler *AwsNLBHandler) CreateNLB(nlbReqInfo irs.NLBInfo) (irs.NLBInfo, //aws.String("subnet-0cf7417f83fd0fd47"), //New-CB-Subnet-NLB-1d1 }, */ + Tags: tags, } if nlbReqInfo.Listener.IP == "" { @@ -874,7 +933,7 @@ func (NLBHandler *AwsNLBHandler) ExtractVMGroupInfo(nlbIID irs.IID) (TargetGroup // 헬스 상태별 VM 목록 처리 //========================= targetHealthInfo, errHealthInfo := NLBHandler.ExtractVMGroupHealthInfo(*result.TargetGroups[0].TargetGroupArn) - if err != nil { + if errHealthInfo != nil { return TargetGroupInfo{}, errHealthInfo } targetGroupInfo.VMGroup.VMs = targetHealthInfo.AllVMs @@ -1001,7 +1060,7 @@ func (NLBHandler *AwsNLBHandler) DeleteListener(listenerArn *string) (bool, erro result, err := NLBHandler.Client.DeleteListener(input) if err != nil { - cblogger.Errorf("Listener[%s] deleted failed", listenerArn) + cblogger.Errorf("Listener[%s] deleted failed", *listenerArn) if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case elbv2.ErrCodeListenerNotFoundException: @@ -1019,7 +1078,7 @@ func (NLBHandler *AwsNLBHandler) DeleteListener(listenerArn *string) (bool, erro return false, err } - cblogger.Infof("Listener[%s] deleted complate", listenerArn) + cblogger.Infof("Listener[%s] deleted complate", *listenerArn) cblogger.Debug(result) return true, nil @@ -1032,7 +1091,7 @@ func (NLBHandler *AwsNLBHandler) DeleteTargetGroup(targetGroupArn *string) (bool result, err := NLBHandler.Client.DeleteTargetGroup(input) if err != nil { - cblogger.Errorf("TargetGroup[%s] deleted failed", targetGroupArn) + cblogger.Errorf("TargetGroup[%s] deleted failed", *targetGroupArn) if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case elbv2.ErrCodeResourceInUseException: @@ -1048,7 +1107,7 @@ func (NLBHandler *AwsNLBHandler) DeleteTargetGroup(targetGroupArn *string) (bool return false, err } - cblogger.Infof("TargetGroup[%s] deleted complate", targetGroupArn) + cblogger.Infof("TargetGroup[%s] deleted complate", *targetGroupArn) cblogger.Debug(result) return true, nil diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go index 7b04a7bad..10aba285a 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go @@ -12,6 +12,7 @@ package resources import ( "errors" + "fmt" "reflect" "strconv" @@ -52,6 +53,12 @@ func (securityHandler *AwsSecurityHandler) CreateSecurity(securityReqInfo irs.Se */ vpcId := securityReqInfo.VpcIID.SystemId + // Convert TagList to TagSpecifications + tagSpecifications, err := ConvertTagListToTagSpecifications("security-group", securityReqInfo.TagList, securityReqInfo.IId.NameId) + if err != nil { + return irs.SecurityInfo{}, fmt.Errorf("failed to convert tag list: %w", err) + } + // Create the security group with the VPC, name and description. //createRes, err := securityHandler.Client.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{ input := ec2.CreateSecurityGroupInput{ @@ -60,7 +67,8 @@ func (securityHandler *AwsSecurityHandler) CreateSecurity(securityReqInfo irs.Se //Description: aws.String(securityReqInfo.Name), Description: aws.String(securityReqInfo.IId.NameId), // VpcId: aws.String(securityReqInfo.VpcId),awsCBNetworkInfo - VpcId: aws.String(vpcId), + VpcId: aws.String(vpcId), + TagSpecifications: tagSpecifications, } cblogger.Debugf("Security group creation request information", input) // logger for HisCall diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go index f250f5682..6bc0bfe0b 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go @@ -9,6 +9,7 @@ package resources import ( "encoding/base64" "errors" + "fmt" "io/ioutil" "os" "reflect" @@ -210,7 +211,7 @@ func (vmHandler *AwsVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, err minCount := aws.Int64(1) maxCount := aws.Int64(1) keyName := vmReqInfo.KeyPairIID.SystemId - baseName := vmReqInfo.IId.NameId + //baseName := vmReqInfo.IId.NameId subnetID := vmReqInfo.SubnetIID.SystemId /* 2021-10-26 이슈 #480에 의해 제거 @@ -329,6 +330,15 @@ func (vmHandler *AwsVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, err } */ + //============================= + // Tag + //============================= + // Convert TagList to TagSpecifications + tagSpecifications, err := ConvertTagListToTagSpecifications("instance", vmReqInfo.TagList, vmReqInfo.IId.NameId) + if err != nil { + return irs.VMInfo{}, fmt.Errorf("failed to convert tag list: %w", err) + } + //============================= // VM생성 처리 //============================= @@ -363,7 +373,8 @@ func (vmHandler *AwsVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, err }, //ec2.InstanceNetworkInterfaceSpecification - UserData: userDataBase64, + UserData: userDataBase64, + TagSpecifications: tagSpecifications, } //============================= @@ -466,25 +477,28 @@ func (vmHandler *AwsVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, err newVmId := *runResult.Instances[0].InstanceId cblogger.Infof("[%s] VM has been created.", newVmId) - if baseName != "" { - // Tag에 VM Name 설정 - _, errtag := vmHandler.Client.CreateTags(&ec2.CreateTagsInput{ - Resources: []*string{runResult.Instances[0].InstanceId}, - Tags: []*ec2.Tag{ - { - Key: aws.String("Name"), - Value: aws.String(baseName), + /* + if baseName != "" { + // Tag에 VM Name 설정 + _, errtag := vmHandler.Client.CreateTags(&ec2.CreateTagsInput{ + Resources: []*string{runResult.Instances[0].InstanceId}, + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(baseName), + }, }, - }, - }) - if errtag != nil { - cblogger.Errorf("Failed to set Name Tag for [%s] VM", newVmId) - cblogger.Error(errtag) - //return irs.VMInfo{}, errtag + }) + if errtag != nil { + cblogger.Errorf("Failed to set Name Tag for [%s] VM", newVmId) + cblogger.Error(errtag) + //return irs.VMInfo{}, errtag + } + } else { + cblogger.Error("Name Tag will not be set because vmReqInfo.IId.NameId is not provided.") } - } else { - cblogger.Error("Name Tag will not be set because vmReqInfo.IId.NameId is not provided.") - } + */ + //Public IP및 최신 정보 전달을 위해 부팅이 완료될 때까지 대기했다가 전달하는 것으로 변경 함. //cblogger.Info("Public IP 할당 및 VM의 최신 정보 획득을 위해 EC2가 Running 상태가 될때까지 대기") diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go index d302b0807..ce148c44e 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go @@ -12,6 +12,7 @@ package resources import ( "errors" + "fmt" "reflect" "strconv" @@ -38,8 +39,16 @@ func (VPCHandler *AwsVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.VPCIn return irs.VPCInfo{}, errors.New("Connection information does not contain Zone information.") } + // Convert TagList to TagSpecifications + tagSpecifications, err := ConvertTagListToTagSpecifications("vpc", vpcReqInfo.TagList, vpcReqInfo.IId.NameId) + if err != nil { + return irs.VPCInfo{}, fmt.Errorf("failed to convert tag list: %w", err) + } + + // Create VPC input with tag specifications input := &ec2.CreateVpcInput{ - CidrBlock: aws.String(vpcReqInfo.IPv4_CIDR), + CidrBlock: aws.String(vpcReqInfo.IPv4_CIDR), + TagSpecifications: tagSpecifications, } //cblogger.Debug(input) @@ -81,12 +90,17 @@ func (VPCHandler *AwsVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.VPCIn retVpcInfo := ExtractVpcDescribeInfo(result.Vpc) retVpcInfo.IId.NameId = vpcReqInfo.IId.NameId // NameId는 요청 받은 값으로 리턴해야 함. - //IGW Name Tag 설정 - if SetNameTag(VPCHandler.Client, *result.Vpc.VpcId, vpcReqInfo.IId.NameId) { - cblogger.Infof("set name %s to VPC", vpcReqInfo.IId.NameId) - } else { - cblogger.Errorf("set name %s to VPC failed", vpcReqInfo.IId.NameId) - } + /* + // 2024.07.16 Delete with Tag support + if len(tagSpecifications) == 0 { + //IGW Name Tag 설정 + if SetNameTag(VPCHandler.Client, *result.Vpc.VpcId, vpcReqInfo.IId.NameId) { + cblogger.Infof("set name %s to VPC", vpcReqInfo.IId.NameId) + } else { + cblogger.Errorf("set name %s to VPC failed", vpcReqInfo.IId.NameId) + } + } + */ //==================================== // PublicIP 할당을 위해 IGW 생성및 연결 From bde405a044e379c7bb73fb100a45cfbedd12b0a4 Mon Sep 17 00:00:00 2001 From: dev4unet Date: Wed, 17 Jul 2024 07:59:53 +0000 Subject: [PATCH 16/56] Fixed some Go syntax warnings --- .../cloud-driver/drivers/aws/resources/ClusterHandler.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go index c3c395584..f07a1b43a 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go @@ -91,12 +91,9 @@ func (ClusterHandler *AwsClusterHandler) CreateCluster(clusterReqInfo irs.Cluste switch len(arrVer) { case 2: // 그대로 적용 input.Version = aws.String(reqK8sVersion) - break case 3: // 앞의 2자리만 취함. (정상적인 입력 형태) input.Version = aws.String(arrVer[0] + "." + arrVer[1]) - break default: // 위 2가지 외에는 CSP의 기본값(최신버전)을 적용 함. - break } } @@ -977,6 +974,7 @@ func (NodeGroupHandler *AwsClusterHandler) convertNodeGroup(nodeGroupOutput *eks nodeGroupInfo.MaxNodeSize = int(*scalingConfig.MaxSize) if nodeGroupTagList == nil { + nodeGroupTagList = make(map[string]*string) // nil 체크 후 초기화 nodeGroupTagList[NODEGROUP_TAG] = nodeGroupName // 값이없으면 nodeGroupName이랑 같은값으로 set. } nodeGroupTag := "" From 32e7a24589265eb2524d0985d64fbfe05276ac24 Mon Sep 17 00:00:00 2001 From: dev4unet Date: Wed, 17 Jul 2024 09:13:43 +0000 Subject: [PATCH 17/56] Add hiscall to FindTag --- .../drivers/aws/resources/TagHandler.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go index a5abf02bc..64aab4508 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go @@ -336,10 +336,15 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] cblogger.Debug(keyInput) } + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, keyword, "FindTag(key):DescribeTags()") + start := call.Start() + keyResult, err := tagHandler.Client.DescribeTags(keyInput) if err != nil { + LoggingError(hiscallInfo, err) return nil, fmt.Errorf("failed to describe tags by key: %w", err) } + LoggingInfo(hiscallInfo, start) processTags(keyResult) valueInput := &ec2.DescribeTagsInput{ @@ -355,10 +360,15 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] cblogger.Debug(valueInput) } + hiscallInfo2 := GetCallLogScheme(tagHandler.Region, call.TAG, keyword, "FindTag(value):DescribeTags()") + start2 := call.Start() + valueResult, err := tagHandler.Client.DescribeTags(valueInput) if err != nil { + LoggingError(hiscallInfo2, err) return nil, fmt.Errorf("failed to describe tags by value: %w", err) } + LoggingInfo(hiscallInfo2, start2) processTags(valueResult) } else { // Search all tags if keyword is empty or "*" @@ -366,10 +376,15 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] Filters: filters, } + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, keyword, "FindTag(all):DescribeTags()") + start := call.Start() + result, err := tagHandler.Client.DescribeTags(input) if err != nil { + LoggingError(hiscallInfo, err) return nil, fmt.Errorf("failed to describe tags: %w", err) } + LoggingInfo(hiscallInfo, start) processTags(result) } From 097e2c6cfa63df25afd26d88d5eebfb49aee1d40 Mon Sep 17 00:00:00 2001 From: dev4unet Date: Thu, 18 Jul 2024 02:27:32 +0000 Subject: [PATCH 18/56] Fix some message warnings --- .../drivers/aws/resources/CommonHandler.go | 157 +++++++++++++++++- 1 file changed, 149 insertions(+), 8 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonHandler.go index a39a85ab7..15bd583cf 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonHandler.go @@ -2,6 +2,7 @@ package resources import ( "errors" + "fmt" "reflect" "strings" @@ -9,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" + idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" ) @@ -57,7 +59,7 @@ func DescribeInstanceById(svc *ec2.EC2, vmIID irs.IID) (*ec2.Instance, error) { var iid irs.IID if vmIID == iid { - return nil, errors.New("instanceID is empty.") + return nil, errors.New("instanceID is empty") } vmIIDs = append(vmIIDs, vmIID) @@ -69,7 +71,7 @@ func DescribeInstanceById(svc *ec2.EC2, vmIID irs.IID) (*ec2.Instance, error) { if len(result.Reservations) < 1 || len(result.Reservations[0].Instances) < 1 { - return nil, errors.New(vmIID.SystemId + " instance not found.") + return nil, errors.New(vmIID.SystemId + " instance not found") } instance := result.Reservations[0].Instances[0] @@ -492,7 +494,7 @@ func DescribeImageById(svc *ec2.EC2, imageIID *irs.IID, owners []*string) (*ec2. var iid irs.IID if *imageIID == iid { - return nil, errors.New("imageID is empty.") + return nil, errors.New("imageID is empty") } imageIIDs = append(imageIIDs, imageIID) @@ -526,11 +528,11 @@ func GetImageSizeFromEc2Image(ec2Image *ec2.Image) (int64, error) { return *isize, nil } else { cblogger.Error("Ebs information not found in BlockDeviceMappings.") - return -1, errors.New("Ebs information not found in BlockDeviceMappings.") + return -1, errors.New("Ebs information not found in BlockDeviceMappings") } } else { cblogger.Error("BlockDeviceMappings information not found.") - return -1, errors.New("BlockDeviceMappings information not found.") + return -1, errors.New("BlockDeviceMappings information not found") } } @@ -569,7 +571,7 @@ func GetSnapshotIdFromEc2Image(ec2Image *ec2.Image) ([]string, error) { } } else { cblogger.Error("BlockDeviceMappings information not found.") - return snapshotIds, errors.New("BlockDeviceMappings information not found.") + return snapshotIds, errors.New("BlockDeviceMappings information not found") } return snapshotIds, nil @@ -584,11 +586,11 @@ func GetDisksFromEc2Image(ec2Image *ec2.Image) ([]irs.IID, error) { return diskIIDs, nil } else { cblogger.Error("Ebs information not found in BlockDeviceMappings.") - return diskIIDs, errors.New("Ebs information not found in BlockDeviceMappings.") + return diskIIDs, errors.New("Ebs information not found in BlockDeviceMappings") } } else { cblogger.Error("BlockDeviceMappings information not found.") - return diskIIDs, errors.New("BlockDeviceMappings information not found.") + return diskIIDs, errors.New("BlockDeviceMappings information not found") } } @@ -765,3 +767,142 @@ func DescribeAvailabilityZones(client *ec2.EC2, AllRegionsBool bool) (*ec2.Descr } // ---------------- RegionZone area end ----------// + +// ---------------- Tag area start ----------// +// Find tags by tag key or value +// resType: ALL | VPC, SUBNET, etc.,. +// keyword: The keyword to search for in the tag key or value. +// if you want to find all tags, set keyword to "" or "*". +func FindTagOrValue(client *ec2.EC2, regionInfo idrv.RegionInfo, resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + cblogger.Debugf("resType : [%s] / keyword : [%s]", resType, keyword) + + var filters []*ec2.Filter + + // Add resource type filter if resType is not ALL + if resType != irs.ALL { + if awsResType, ok := rsTypeToAwsResourceTypeMap[resType]; ok { + filters = append(filters, &ec2.Filter{ + Name: aws.String("resource-type"), + Values: []*string{ + aws.String(awsResType), + }, + }) + } else { + return nil, fmt.Errorf("unsupported resource type: %s", resType) + } + } + + tagInfoMap := make(map[string]*irs.TagInfo) + + // Function to process tags and add them to tagInfoMap + processTags := func(result *ec2.DescribeTagsOutput) { + if cblogger.Level.String() == "debug" { + cblogger.Debug(result) + //cblogger.Debug("=================================") + //spew.Dump(result) + //cblogger.Debug("=================================") + } + + for _, tag := range result.Tags { + resID := aws.StringValue(tag.ResourceId) + + awsResType := aws.StringValue(tag.ResourceType) + rType, exists := awsResourceTypeToRSTypeMap[awsResType] + if !exists { + //@TODO - 변환 실패한 리소스의 경우 UNKNOWN을 만들거나 에러 로그만 찍거나 결정 필요할 듯 + cblogger.Errorf("No RSType matching [%s] found.", awsResType) + + rType = irs.RSType(awsResType) // Use the raw AWS resource type if not mapped + } + + if _, exists := tagInfoMap[resID]; !exists { + tagInfoMap[resID] = &irs.TagInfo{ + ResType: rType, + ResIId: irs.IID{ + SystemId: resID, + }, + } + } + tagInfoMap[resID].TagList = append(tagInfoMap[resID].TagList, irs.KeyValue{ + Key: aws.StringValue(tag.Key), + Value: aws.StringValue(tag.Value), + }) + } + } + + // Search by tag-key if keyword is not empty or "*" + if keyword != "" && keyword != "*" { + keyInput := &ec2.DescribeTagsInput{ + Filters: append(filters, &ec2.Filter{ + Name: aws.String("tag-key"), + Values: []*string{ + aws.String(keyword), + }, + }), + } + + if cblogger.Level.String() == "debug" { + cblogger.Debug(keyInput) + } + + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag(key):DescribeTags()") + start := call.Start() + + keyResult, err := client.DescribeTags(keyInput) + if err != nil { + LoggingError(hiscallInfo, err) + return nil, fmt.Errorf("failed to describe tags by key: %w", err) + } + LoggingInfo(hiscallInfo, start) + processTags(keyResult) + + valueInput := &ec2.DescribeTagsInput{ + Filters: append(filters, &ec2.Filter{ + Name: aws.String("tag-value"), + Values: []*string{ + aws.String(keyword), + }, + }), + } + + if cblogger.Level.String() == "debug" { + cblogger.Debug(valueInput) + } + + hiscallInfo2 := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag(value):DescribeTags()") + start2 := call.Start() + + valueResult, err := client.DescribeTags(valueInput) + if err != nil { + LoggingError(hiscallInfo2, err) + return nil, fmt.Errorf("failed to describe tags by value: %w", err) + } + LoggingInfo(hiscallInfo2, start2) + processTags(valueResult) + } else { + // Search all tags if keyword is empty or "*" + input := &ec2.DescribeTagsInput{ + Filters: filters, + } + + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag(all):DescribeTags()") + start := call.Start() + + result, err := client.DescribeTags(input) + if err != nil { + LoggingError(hiscallInfo, err) + return nil, fmt.Errorf("failed to describe tags: %w", err) + } + LoggingInfo(hiscallInfo, start) + processTags(result) + } + + var tagInfos []*irs.TagInfo + for _, tagInfo := range tagInfoMap { + tagInfos = append(tagInfos, tagInfo) + } + + return tagInfos, nil +} + +// ---------------- Tag area end ----------// From 6d531ef2ae6bd2dd6b11528ed87dd627eed9421b Mon Sep 17 00:00:00 2001 From: innodreamer Date: Thu, 18 Jul 2024 18:06:51 +0900 Subject: [PATCH 19/56] Apply NCP's Updated Parameter Return Values about VMImage/MyImage --- .../drivers/ncp/resources/ImageHandler.go | 32 +++++++++++++++---- .../drivers/ncp/resources/MyImageHandler.go | 17 ++++++---- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ncp/resources/ImageHandler.go b/cloud-control-manager/cloud-driver/drivers/ncp/resources/ImageHandler.go index bffd56e40..f533af4b5 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncp/resources/ImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncp/resources/ImageHandler.go @@ -12,6 +12,7 @@ import ( "fmt" "strconv" "strings" + // "github.com/davecgh/go-spew/spew" ncloud "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" server "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/server" @@ -117,7 +118,7 @@ func MappingImageInfo(serverImage server.Product) irs.ImageInfo { NameId: *serverImage.ProductCode, SystemId: *serverImage.ProductCode, }, - GuestOS: *serverImage.ProductDescription, + GuestOS: *serverImage.OsInformation, Status: "available", } @@ -126,9 +127,9 @@ func MappingImageInfo(serverImage server.Product) irs.ImageInfo { {Key: "PlatformType", Value: *serverImage.PlatformType.CodeName}, {Key: "InfraResourceType", Value: *serverImage.InfraResourceType.CodeName}, {Key: "BaseBlockStorageSize(GB)", Value: strconv.FormatFloat(float64(*serverImage.BaseBlockStorageSize)/(1024*1024*1024), 'f', 0, 64)}, - //{Key: "OsInformation", Value: *serverImage.OsInformation}, - //{Key: "DB Type", Value: *serverImage.DbKindCode}, - //{Key: "NCP GenerationCode", Value: *serverImage.GenerationCode}, + // {Key: "OsInformation", Value: *serverImage.OsInformation}, + // {Key: "DB Type", Value: *serverImage.DbKindCode}, + // {Key: "NCP GenerationCode", Value: *serverImage.GenerationCode}, } keyValueList = append(keyValueList, irs.KeyValue{Key: "Description", Value: *serverImage.ProductDescription}) imageInfo.KeyValueList = keyValueList @@ -192,11 +193,30 @@ func (imageHandler *NcpImageHandler) GetNcpImageInfo(imageIID irs.IID) (*server. return nil, createErr } - imageReq := server.GetServerImageProductListRequest{ProductCode: ncloud.String(imageIID.SystemId)} + // cblogger.Info("\n\n### imageIID : ") + // spew.Dump(imageIID) + // cblogger.Info("\n") + + vmHandler := NcpVMHandler{ + RegionInfo: imageHandler.RegionInfo, + VMClient: imageHandler.VMClient, + } + regionNo, err := vmHandler.GetRegionNo(imageHandler.RegionInfo.Region) + if err != nil { + newErr := fmt.Errorf("Failed to Get NCP Region No of the Region Code : [%v]", err) + cblogger.Error(newErr.Error()) + LoggingError(callLogInfo, newErr) + return nil, newErr + } + + imageReq := server.GetServerImageProductListRequest{ + ProductCode: ncloud.String(imageIID.SystemId), + RegionNo: regionNo, + } callLogStart := call.Start() result, err := imageHandler.VMClient.V2Api.GetServerImageProductList(&imageReq) if err != nil { - newErr := fmt.Errorf("Failed to Find Image list from NCP : [%v]", err) + newErr := fmt.Errorf("Failed to Get Image list from NCP : [%v]", err) cblogger.Error(newErr.Error()) LoggingError(callLogInfo, newErr) return nil, newErr diff --git a/cloud-control-manager/cloud-driver/drivers/ncp/resources/MyImageHandler.go b/cloud-control-manager/cloud-driver/drivers/ncp/resources/MyImageHandler.go index 80537b76a..c1f969f34 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncp/resources/MyImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncp/resources/MyImageHandler.go @@ -338,6 +338,7 @@ func (myImageHandler *NcpMyImageHandler) MappingMyImageInfo(myImage *server.Memb {Key: "Region", Value: myImageHandler.RegionInfo.Region}, {Key: "OriginalImageProductCode", Value: *myImage.OriginalServerImageProductCode}, {Key: "MyImagePlatformType", Value: *myImage.MemberServerImagePlatformType.CodeName}, + {Key: "OriginalOsInformation", Value: *myImage.OriginalOsInformation}, {Key: "CreateDate", Value: *myImage.CreateDate}, } myImageInfo.KeyValueList = keyValueList @@ -383,8 +384,8 @@ func (myImageHandler *NcpMyImageHandler) GetNcpMemberServerImageInfo(myImageIID LoggingInfo(callLogInfo, callLogStart) if len(result.MemberServerImageList) < 1 { - newErr := fmt.Errorf("Failed to Get the Member Server Image List from NCP. Member Server Image does Not Exist!!") - cblogger.Error(newErr.Error()) + newErr := fmt.Errorf("The Member Server Image does Not Exist!!") + cblogger.Debug(newErr.Error()) LoggingError(callLogInfo, newErr) return server.MemberServerImage{}, newErr } else { @@ -424,7 +425,9 @@ func (myImageHandler *NcpMyImageHandler) GetOriginImageOSPlatform(imageIID irs.I LoggingError(callLogInfo, newErr) return "", newErr } else { - imagePlatformType := strings.ToUpper(*ncpImageInfo.PlatformType.CodeName) + // cblogger.Infof("### ImageOsInformation : [%s]", *ncpImageInfo.OsInformation) + imagePlatformType := strings.ToUpper(*ncpImageInfo.OsInformation) + var originImagePlatform string if strings.Contains(imagePlatformType, "UBUNTU") { originImagePlatform = "UBUNTU" @@ -444,13 +447,13 @@ func (myImageHandler *NcpMyImageHandler) GetOriginImageOSPlatform(imageIID irs.I memberServerImageInfo, err := myImageHandler.GetNcpMemberServerImageInfo(imageIID) if err != nil { newErr := fmt.Errorf("Failed to Get NCP Member Server Image Info. [%v]", err.Error()) - cblogger.Error(newErr.Error()) + cblogger.Debug(newErr.Error()) LoggingError(callLogInfo, newErr) return "", newErr } - cblogger.Infof("### MyImagePlatformType : [%s]", *memberServerImageInfo.MemberServerImagePlatformType.CodeName) - - imagePlatformType := strings.ToUpper(*memberServerImageInfo.MemberServerImagePlatformType.CodeName) + // cblogger.Infof("### MyImageOriginalOsInformation : [%s]", *memberServerImageInfo.OriginalOsInformation) + imagePlatformType := strings.ToUpper(*memberServerImageInfo.OriginalOsInformation) + var originImagePlatform string if strings.Contains(imagePlatformType, "UBUNTU") { originImagePlatform = "UBUNTU" From 72add2ba124c2e0972f25d1c5438786daa499790 Mon Sep 17 00:00:00 2001 From: innodreamer Date: Thu, 18 Jul 2024 18:10:58 +0900 Subject: [PATCH 20/56] Change NCP Classic Default VMImage and VMSpec for Test --- api-runtime/rest-runtime/admin-web/AdminWeb-VM.go | 4 ++-- test/vm-cb-user-validation-cli/common/ncp/setup.env | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api-runtime/rest-runtime/admin-web/AdminWeb-VM.go b/api-runtime/rest-runtime/admin-web/AdminWeb-VM.go index 6bf214319..90f4e50bd 100644 --- a/api-runtime/rest-runtime/admin-web/AdminWeb-VM.go +++ b/api-runtime/rest-runtime/admin-web/AdminWeb-VM.go @@ -590,8 +590,8 @@ func VM(c echo.Context) error { imageName = "16681742-f408-444d-a430-dd21a4bef42c" specName = "ETRI-small-2" case "NCP": - imageName = "SPSW0LINUX000052" - specName = "SPSVRHICPUSSD002" + imageName = "SPSW0LINUX000139" + specName = "SPSVRSTAND000005" case "NCPVPC": imageName = "SW.VSVR.OS.LNX64.UBNTU.SVR2004.B050" specName = "SVR.VSVR.HICPU.C004.M008.NET.SSD.B050.G002" diff --git a/test/vm-cb-user-validation-cli/common/ncp/setup.env b/test/vm-cb-user-validation-cli/common/ncp/setup.env index 5e7fbebf2..b83ba8959 100644 --- a/test/vm-cb-user-validation-cli/common/ncp/setup.env +++ b/test/vm-cb-user-validation-cli/common/ncp/setup.env @@ -1,5 +1,5 @@ CONN_CONFIG=ncp-korea1-config -IMAGE_NAME=SPSW0LINUX000130 -SPEC_NAME=SPSVRHICPUSSD002 +IMAGE_NAME=SPSW0LINUX000139 +SPEC_NAME=SPSVRSTAND000005 SG_NAME=spider-sg01 From 9ed5ed77d61e5b9590e5be3a69a100daf7306a82 Mon Sep 17 00:00:00 2001 From: dev4unet Date: Thu, 18 Jul 2024 09:45:10 +0000 Subject: [PATCH 21/56] Added TagHandler implementation to each handler and common function GetResourceTag() --- .../drivers/aws/connect/AwsCloudConnection.go | 41 +++-- .../drivers/aws/main/Test_Resources.go | 6 +- .../drivers/aws/resources/ClusterHandler.go | 1 + .../drivers/aws/resources/CommonHandler.go | 151 ++---------------- .../drivers/aws/resources/DiskHandler.go | 5 +- .../drivers/aws/resources/KeyPairHandler.go | 1 + .../drivers/aws/resources/MyImageHandler.go | 5 +- .../drivers/aws/resources/NLBHandler.go | 5 +- .../drivers/aws/resources/SecurityHandler.go | 5 +- .../drivers/aws/resources/VMHandler.go | 7 +- .../drivers/aws/resources/VPCHandler.go | 8 +- 11 files changed, 72 insertions(+), 163 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go b/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go index bf39d9e64..4530a7323 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go @@ -80,41 +80,50 @@ func (cloudConn *AwsCloudConnection) Close() error { } func (cloudConn *AwsCloudConnection) CreateKeyPairHandler() (irs.KeyPairHandler, error) { - keyPairHandler := ars.AwsKeyPairHandler{cloudConn.CredentialInfo, cloudConn.Region, cloudConn.KeyPairClient} + tagHandler := cloudConn.CreateAwsTagHandler() + keyPairHandler := ars.AwsKeyPairHandler{CredentialInfo: cloudConn.CredentialInfo, Region: cloudConn.Region, Client: cloudConn.KeyPairClient, TagHandler: &tagHandler} //keyPairHandler := ars.AwsKeyPairHandler{cloudConn.Region, cloudConn.KeyPairClient} return &keyPairHandler, nil } func (cloudConn *AwsCloudConnection) CreateVMHandler() (irs.VMHandler, error) { - vmHandler := ars.AwsVMHandler{cloudConn.Region, cloudConn.VMClient} + tagHandler := cloudConn.CreateAwsTagHandler() + vmHandler := ars.AwsVMHandler{Region: cloudConn.Region, Client: cloudConn.VMClient, TagHandler: &tagHandler} return &vmHandler, nil } func (cloudConn *AwsCloudConnection) CreateVPCHandler() (irs.VPCHandler, error) { - handler := ars.AwsVPCHandler{cloudConn.Region, cloudConn.VNetworkClient} + tagHandler := cloudConn.CreateAwsTagHandler() + handler := ars.AwsVPCHandler{Region: cloudConn.Region, Client: cloudConn.VNetworkClient, TagHandler: &tagHandler} return &handler, nil } // func (cloudConn *AwsCloudConnection) CreateImageHandler() (irs2.ImageHandler, error) { func (cloudConn *AwsCloudConnection) CreateImageHandler() (irs.ImageHandler, error) { - handler := ars.AwsImageHandler{cloudConn.Region, cloudConn.ImageClient} + handler := ars.AwsImageHandler{Region: cloudConn.Region, Client: cloudConn.ImageClient} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateSecurityHandler() (irs.SecurityHandler, error) { - handler := ars.AwsSecurityHandler{cloudConn.Region, cloudConn.SecurityClient} + tagHandler := cloudConn.CreateAwsTagHandler() + handler := ars.AwsSecurityHandler{Region: cloudConn.Region, Client: cloudConn.SecurityClient, TagHandler: &tagHandler} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateTagHandler() (irs.TagHandler, error) { - handler := ars.AwsTagHandler{cloudConn.Region, cloudConn.VMClient} + handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient} return &handler, nil } +func (cloudConn *AwsCloudConnection) CreateAwsTagHandler() ars.AwsTagHandler { + handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient} + return handler +} + /* func (cloudConn *AwsCloudConnection) CreateVNicHandler() (irs.VNicHandler, error) { cblogger.Info("Start") @@ -132,26 +141,30 @@ func (cloudConn *AwsCloudConnection) CreatePublicIPHandler() (irs.PublicIPHandle */ func (cloudConn *AwsCloudConnection) CreateVMSpecHandler() (irs.VMSpecHandler, error) { - handler := ars.AwsVmSpecHandler{cloudConn.Region, cloudConn.VmSpecClient} + handler := ars.AwsVmSpecHandler{Region: cloudConn.Region, Client: cloudConn.VmSpecClient} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateNLBHandler() (irs.NLBHandler, error) { - handler := ars.AwsNLBHandler{cloudConn.Region, cloudConn.NLBClient, cloudConn.VMClient} + tagHandler := cloudConn.CreateAwsTagHandler() + handler := ars.AwsNLBHandler{Region: cloudConn.Region, Client: cloudConn.NLBClient, VMClient: cloudConn.VMClient, TagHandler: &tagHandler} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateDiskHandler() (irs.DiskHandler, error) { - handler := ars.AwsDiskHandler{cloudConn.Region, cloudConn.DiskClient} + tagHandler := cloudConn.CreateAwsTagHandler() + handler := ars.AwsDiskHandler{Region: cloudConn.Region, Client: cloudConn.DiskClient, TagHandler: &tagHandler} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateMyImageHandler() (irs.MyImageHandler, error) { - handler := ars.AwsMyImageHandler{cloudConn.Region, cloudConn.MyImageClient} + tagHandler := cloudConn.CreateAwsTagHandler() + handler := ars.AwsMyImageHandler{Region: cloudConn.Region, Client: cloudConn.MyImageClient, TagHandler: &tagHandler} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateClusterHandler() (irs.ClusterHandler, error) { + tagHandler := cloudConn.CreateAwsTagHandler() cblogger.Info("CreateClusterHandler through") if cloudConn.MyImageClient == nil { cblogger.Info("cloudConn.MyImageClient is nil") @@ -168,21 +181,21 @@ func (cloudConn *AwsCloudConnection) CreateClusterHandler() (irs.ClusterHandler, if cloudConn.AutoScalingClient == nil { cblogger.Info("cloudConn.AutoScalingClient is nil") } - handler := ars.AwsClusterHandler{cloudConn.Region, cloudConn.EKSClient, cloudConn.VNetworkClient, cloudConn.IamClient, cloudConn.AutoScalingClient} + handler := ars.AwsClusterHandler{Region: cloudConn.Region, Client: cloudConn.EKSClient, EC2Client: cloudConn.VNetworkClient, Iam: cloudConn.IamClient, AutoScaling: cloudConn.AutoScalingClient, TagHandler: &tagHandler} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateAnyCallHandler() (irs.AnyCallHandler, error) { - handler := ars.AwsAnyCallHandler{cloudConn.Region, cloudConn.CredentialInfo, cloudConn.AnyCallClient} + handler := ars.AwsAnyCallHandler{Region: cloudConn.Region, CredentialInfo: cloudConn.CredentialInfo, Client: cloudConn.AnyCallClient} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateRegionZoneHandler() (irs.RegionZoneHandler, error) { - handler := ars.AwsRegionZoneHandler{cloudConn.Region, cloudConn.RegionZoneClient} + handler := ars.AwsRegionZoneHandler{Region: cloudConn.Region, Client: cloudConn.RegionZoneClient} return &handler, nil } func (cloudConn *AwsCloudConnection) CreatePriceInfoHandler() (irs.PriceInfoHandler, error) { - handler := ars.AwsPriceInfoHandler{cloudConn.Region, cloudConn.PriceInfoClient} + handler := ars.AwsPriceInfoHandler{Region: cloudConn.Region, Client: cloudConn.PriceInfoClient} return &handler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go index 798186302..b2c0d5e4a 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go @@ -951,7 +951,7 @@ func handleVM() { //config := readConfigFile() //VmID := irs.IID{NameId: config.Aws.BaseName, SystemId: config.Aws.VmID} // VmID := irs.IID{SystemId: "i-0cea86282a9e2a569"} - VmID := irs.IID{SystemId: "i-0c70f696e5f8e690c"} + VmID := irs.IID{SystemId: "i-02ac1c4ff1d40815c"} for { fmt.Println("VM Management") @@ -2061,7 +2061,7 @@ func main() { // handleKeyPair() // handlePublicIP() // PublicIP 생성 후 conf // handleSecurity() - // handleVM() + handleVM() // handleImage() //AMI // handleVNic() //Lancard // handleVMSpec() @@ -2069,5 +2069,5 @@ func main() { // handleCluster() //handleRegionZone() //handlePriceInfo() - handleTag() + //handleTag() } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go index f07a1b43a..bd0e86d82 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go @@ -24,6 +24,7 @@ type AwsClusterHandler struct { EC2Client *ec2.EC2 Iam *iam.IAM AutoScaling *autoscaling.AutoScaling + TagHandler *AwsTagHandler // 2024-07-18 TagHandler add } const ( diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonHandler.go index 15bd583cf..fa0b01b29 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/CommonHandler.go @@ -10,7 +10,6 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" - idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" ) @@ -769,140 +768,24 @@ func DescribeAvailabilityZones(client *ec2.EC2, AllRegionsBool bool) (*ec2.Descr // ---------------- RegionZone area end ----------// // ---------------- Tag area start ----------// -// Find tags by tag key or value -// resType: ALL | VPC, SUBNET, etc.,. -// keyword: The keyword to search for in the tag key or value. -// if you want to find all tags, set keyword to "" or "*". -func FindTagOrValue(client *ec2.EC2, regionInfo idrv.RegionInfo, resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { - cblogger.Debugf("resType : [%s] / keyword : [%s]", resType, keyword) - - var filters []*ec2.Filter - - // Add resource type filter if resType is not ALL - if resType != irs.ALL { - if awsResType, ok := rsTypeToAwsResourceTypeMap[resType]; ok { - filters = append(filters, &ec2.Filter{ - Name: aws.String("resource-type"), - Values: []*string{ - aws.String(awsResType), - }, - }) - } else { - return nil, fmt.Errorf("unsupported resource type: %s", resType) - } - } - - tagInfoMap := make(map[string]*irs.TagInfo) - - // Function to process tags and add them to tagInfoMap - processTags := func(result *ec2.DescribeTagsOutput) { - if cblogger.Level.String() == "debug" { - cblogger.Debug(result) - //cblogger.Debug("=================================") - //spew.Dump(result) - //cblogger.Debug("=================================") - } - - for _, tag := range result.Tags { - resID := aws.StringValue(tag.ResourceId) - - awsResType := aws.StringValue(tag.ResourceType) - rType, exists := awsResourceTypeToRSTypeMap[awsResType] - if !exists { - //@TODO - 변환 실패한 리소스의 경우 UNKNOWN을 만들거나 에러 로그만 찍거나 결정 필요할 듯 - cblogger.Errorf("No RSType matching [%s] found.", awsResType) - - rType = irs.RSType(awsResType) // Use the raw AWS resource type if not mapped - } - - if _, exists := tagInfoMap[resID]; !exists { - tagInfoMap[resID] = &irs.TagInfo{ - ResType: rType, - ResIId: irs.IID{ - SystemId: resID, - }, - } - } - tagInfoMap[resID].TagList = append(tagInfoMap[resID].TagList, irs.KeyValue{ - Key: aws.StringValue(tag.Key), - Value: aws.StringValue(tag.Value), - }) - } - } - - // Search by tag-key if keyword is not empty or "*" - if keyword != "" && keyword != "*" { - keyInput := &ec2.DescribeTagsInput{ - Filters: append(filters, &ec2.Filter{ - Name: aws.String("tag-key"), - Values: []*string{ - aws.String(keyword), - }, - }), - } - - if cblogger.Level.String() == "debug" { - cblogger.Debug(keyInput) - } - - hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag(key):DescribeTags()") - start := call.Start() - - keyResult, err := client.DescribeTags(keyInput) - if err != nil { - LoggingError(hiscallInfo, err) - return nil, fmt.Errorf("failed to describe tags by key: %w", err) - } - LoggingInfo(hiscallInfo, start) - processTags(keyResult) - - valueInput := &ec2.DescribeTagsInput{ - Filters: append(filters, &ec2.Filter{ - Name: aws.String("tag-value"), - Values: []*string{ - aws.String(keyword), - }, - }), - } - - if cblogger.Level.String() == "debug" { - cblogger.Debug(valueInput) - } - - hiscallInfo2 := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag(value):DescribeTags()") - start2 := call.Start() - - valueResult, err := client.DescribeTags(valueInput) - if err != nil { - LoggingError(hiscallInfo2, err) - return nil, fmt.Errorf("failed to describe tags by value: %w", err) - } - LoggingInfo(hiscallInfo2, start2) - processTags(valueResult) - } else { - // Search all tags if keyword is empty or "*" - input := &ec2.DescribeTagsInput{ - Filters: filters, - } - - hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag(all):DescribeTags()") - start := call.Start() - - result, err := client.DescribeTags(input) - if err != nil { - LoggingError(hiscallInfo, err) - return nil, fmt.Errorf("failed to describe tags: %w", err) - } - LoggingInfo(hiscallInfo, start) - processTags(result) - } - - var tagInfos []*irs.TagInfo - for _, tagInfo := range tagInfoMap { - tagInfos = append(tagInfos, tagInfo) +// This is a deprecated function, created as a reference for how to call it in various handlers. +// +// (exam) How to call from VMHandler +// +// vmInfo.TagList, _ = GetResourceTag(vmHandler, vmInfo.IId) +func GetResourceTag(handler interface{}, resIID irs.IID) ([]irs.KeyValue, error) { + var resType irs.RSType + + switch h := handler.(type) { + case *AwsVMHandler: + resType = irs.VM + return h.TagHandler.ListTag(resType, resIID) + case *AwsVPCHandler: + resType = irs.VPC + return h.TagHandler.ListTag(resType, resIID) + default: + return nil, fmt.Errorf("unsupported handler type") } - - return tagInfos, nil } // ---------------- Tag area end ----------// diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go index 36f4057bb..37e6cece8 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go @@ -19,8 +19,9 @@ import ( ) type AwsDiskHandler struct { - Region idrv.RegionInfo - Client *ec2.EC2 + Region idrv.RegionInfo + Client *ec2.EC2 + TagHandler *AwsTagHandler // 2024-07-18 TagHandler add } var VOLUME_TYPE = []string{"standard", "io1", "io2", "gp2", "gp3", "sc1", "st1"} // array 는 const 불가하여 변수로 처리. diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go index 337198191..02f2b5b45 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go @@ -20,6 +20,7 @@ type AwsKeyPairHandler struct { CredentialInfo idrv.CredentialInfo Region idrv.RegionInfo Client *ec2.EC2 + TagHandler *AwsTagHandler // 2024-07-18 TagHandler add } /* diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go index 8bdca4c6b..e34373b7d 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go @@ -22,8 +22,9 @@ import ( // CB-Spider MyImage 관리 기능은 VM Snapshot 실행과 결과로 생성된 VM Image(MyImage)를 관리하는 기능을 제공한다 // CB-Spider VM Snapshot은 운영 중인 VM의 상태와 VM에 Attach된 Data-Disk의 상태도 저장된다. type AwsMyImageHandler struct { - Region idrv.RegionInfo - Client *ec2.EC2 + Region idrv.RegionInfo + Client *ec2.EC2 + TagHandler *AwsTagHandler // 2024-07-18 TagHandler add } const ( diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go index b4e57b06d..318fd8556 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go @@ -23,8 +23,9 @@ import ( type AwsNLBHandler struct { Region idrv.RegionInfo //Client *elb.ELB - Client *elbv2.ELBV2 //elbV2 - VMClient *ec2.EC2 + Client *elbv2.ELBV2 //elbV2 + VMClient *ec2.EC2 + TagHandler *AwsTagHandler // 2024-07-18 TagHandler add } type TargetGroupInfo struct { diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go index 10aba285a..b14f6b7c6 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go @@ -28,8 +28,9 @@ import ( ) type AwsSecurityHandler struct { - Region idrv.RegionInfo - Client *ec2.EC2 + Region idrv.RegionInfo + Client *ec2.EC2 + TagHandler *AwsTagHandler // 2024-07-18 TagHandler add } // 2019-11-16부로 CB-Driver 전체 로직이 NameId 기반으로 변경됨. (보안 그룹은 그룹명으로 처리 가능하기 때문에 Name 태깅시 에러는 무시함) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go index 6bc0bfe0b..152ec5752 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go @@ -27,8 +27,9 @@ import ( ) type AwsVMHandler struct { - Region idrv.RegionInfo - Client *ec2.EC2 + Region idrv.RegionInfo + Client *ec2.EC2 + TagHandler *AwsTagHandler // 2024-07-18 TagHandler add } func Connect(region string) *ec2.EC2 { @@ -1182,6 +1183,8 @@ func (vmHandler *AwsVMHandler) ExtractDescribeInstanceToVmInfo(instance *ec2.Ins } vmInfo.KeyValueList = keyValueList + vmInfo.TagList, _ = vmHandler.TagHandler.ListTag(irs.VM, vmInfo.IId) + //vmInfo.TagList, _ = GetResourceTag(vmHandler, vmInfo.IId) return vmInfo } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go index ce148c44e..fa1bfd2f3 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go @@ -25,8 +25,9 @@ import ( ) type AwsVPCHandler struct { - Region idrv.RegionInfo - Client *ec2.EC2 + Region idrv.RegionInfo + Client *ec2.EC2 + TagHandler *AwsTagHandler // 2024-07-18 TagHandler add } func (VPCHandler *AwsVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.VPCInfo, error) { @@ -175,6 +176,8 @@ func (VPCHandler *AwsVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.VPCIn resSubnetList = append(resSubnetList, resSubnet) } retVpcInfo.SubnetInfoList = resSubnetList + + retVpcInfo.TagList, _ = VPCHandler.TagHandler.ListTag(irs.VM, retVpcInfo.IId) return retVpcInfo, nil } @@ -523,6 +526,7 @@ func (VPCHandler *AwsVPCHandler) GetVPC(vpcIID irs.IID) (irs.VPCInfo, error) { return awsVpcInfo, errSubnet } + awsVpcInfo.TagList, _ = VPCHandler.TagHandler.ListTag(irs.VM, awsVpcInfo.IId) return awsVpcInfo, nil } From 31d4f1e5f9561dd450cf587715337fd518dcbdf1 Mon Sep 17 00:00:00 2001 From: hippo-an Date: Fri, 19 Jul 2024 11:30:31 +0900 Subject: [PATCH 22/56] test resource creation and get resources info with tag list --- .../drivers/gcp/main/Test_Resources.go | 58 +++++++++++++------ .../drivers/gcp/main/conf/Test_Config.go | 2 + 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go index fd9c0eefe..311719535 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/main/Test_Resources.go @@ -34,6 +34,24 @@ func init() { cblog.SetLevel("debug") } +const ( + diskId = "cb-disk-03" + vmId = "mcloud-barista-vm-test" + vpcId = "cb-vpc-load-test" + subnetId = "vpc-loadtest-sub1" + sgId = "sg10" + keypairId = "cb-keypairtest123123" +) + +var ( + tagList = []irs.KeyValue{ + { + Key: "test-key", + Value: "test-value", + }, + } +) + // Test SecurityHandler func handleSecurityOld() { cblogger.Debug("Start handler") @@ -132,11 +150,11 @@ func handleSecurity() { // securityName := "cb-securitytest1" // securityId := "cb-securitytest1" - securityName := "sg10" - securityId := "sg10" + securityName := sgId + securityId := sgId //securityId := "cb-secu-all" //vpcId := "cb-vpc" - vpcId := "cb-vpc-load-test" + vpcId := vpcId for { fmt.Println("Security Management") @@ -677,10 +695,10 @@ func handleVPC() { cblogger.Debug(reqSubnetId) vpcReqInfo := irs.VPCReqInfo{ - IId: irs.IID{NameId: "cb-vpc-load-test"}, + IId: irs.IID{NameId: vpcId}, SubnetInfoList: []irs.SubnetInfo{ { - IId: irs.IID{NameId: "vpc-loadtest-sub1"}, + IId: irs.IID{NameId: subnetId}, IPv4_CIDR: "10.0.3.0/24", }, { @@ -895,7 +913,7 @@ func handleKeyPair() { //config := readConfigFile() //VmID := config.Aws.VmID - keyPairName := "CB-KeyPairTest123123" + keyPairName := keypairId //keyPairName := config.Aws.KeyName for { @@ -1071,7 +1089,7 @@ func handleVM() { //config := readConfigFile() //VmID := irs.IID{NameId: config.Aws.BaseName, SystemId: config.Aws.VmID} - VmID := irs.IID{SystemId: "mcloud-barista-vm-test"} + VmID := irs.IID{SystemId: vmId} for { fmt.Println("VM Management") @@ -1100,7 +1118,7 @@ func handleVM() { case 1: vmReqInfo := irs.VMReqInfo{ - IId: irs.IID{NameId: "mcloud-barista-vm-test"}, + IId: irs.IID{NameId: vmId}, ImageIID: irs.IID{ NameId: "Test", //SystemId: "ubuntu-minimal-1804-bionic-v20200415", @@ -1117,21 +1135,22 @@ func handleVM() { }, //VpcIID: irs.IID{SystemId: "cb-vpc"}, //SubnetIID: irs.IID{SystemId: "cb-sub1"}, - VpcIID: irs.IID{SystemId: "cb-vpc-load-test"}, - SubnetIID: irs.IID{SystemId: "vpc-loadtest-sub1"}, - SecurityGroupIIDs: []irs.IID{{SystemId: "sg10"}}, + VpcIID: irs.IID{SystemId: vpcId}, + SubnetIID: irs.IID{SystemId: subnetId}, + SecurityGroupIIDs: []irs.IID{{SystemId: sgId}}, VMSpecName: "e2-small", - KeyPairIID: irs.IID{SystemId: "cb-keypairtest123123"}, + KeyPairIID: irs.IID{SystemId: keypairId}, VMUserId: "cb-user", //RootDiskType: "pd-ssd", //pd-standard/pd-balanced/pd-ssd/pd-extreme RootDiskType: "pd-balanced", //pd-standard/pd-balanced/pd-ssd/pd-extreme //RootDiskSize: "12", //최소 10GB 이상이어야 함. RootDiskSize: "default", //10GB - DataDiskIIDs: []irs.IID{{SystemId: "cb-disk-02"}}, + DataDiskIIDs: []irs.IID{{SystemId: diskId}}, VMUserPasswd: "1234qwer!@#$", //윈도우즈용 비밀번호 WindowsType: true, //윈도우즈 테스트 + TagList: tagList, } vmInfo, err := vmHandler.StartVM(vmReqInfo) @@ -1608,9 +1627,10 @@ func handleDisk() { //imageReqInfo := irs2.ImageReqInfo{ diskReqInfo := irs.DiskInfo{ - IId: irs.IID{NameId: "cb-disk-03", SystemId: "cb-disk-03"}, + IId: irs.IID{NameId: diskId, SystemId: diskId}, DiskType: "", DiskSize: "20", + TagList: tagList, } for { @@ -1692,7 +1712,7 @@ func handleDisk() { } case 6: cblogger.Infof("[%s] Disk Attach 테스트", diskReqInfo.IId.NameId) - result, err := handler.AttachDisk(diskReqInfo.IId, irs.IID{SystemId: "mcloud-barista-vm-test"}) + result, err := handler.AttachDisk(diskReqInfo.IId, irs.IID{SystemId: vmId}) if err != nil { cblogger.Infof("[%s] Disk Attach 실패 : ", diskReqInfo.IId.NameId, err) } else { @@ -1701,7 +1721,7 @@ func handleDisk() { } case 7: cblogger.Infof("[%s] Disk Detach 테스트", diskReqInfo.IId.NameId) - result, err := handler.DetachDisk(diskReqInfo.IId, irs.IID{SystemId: "mcloud-barista-vm-test"}) + result, err := handler.DetachDisk(diskReqInfo.IId, irs.IID{SystemId: vmId}) if err != nil { cblogger.Infof("[%s] Disk Detach 실패 : ", diskReqInfo.IId.NameId, err) } else { @@ -2022,7 +2042,7 @@ func handleTags() { sampleId = "2504669692882076487" sampleType = irs.VM } else if strings.EqualFold(strings.ToLower(key), "d") { - sampleId = "mcmp-demo" + sampleId = diskId sampleType = irs.DISK } else if strings.EqualFold(strings.ToLower(key), "a") { sampleType = irs.ALL @@ -2142,14 +2162,14 @@ func handleTags() { } func main() { cblogger.Info("GCP Resource Test") - //handleVPC() + // handleVPC() //handleVMSpec() //handleImage() //AMI //handleKeyPair() //handleSecurity() // handleVM() //handleLoadBalancer() - //handleDisk() + // handleDisk() //handleMyImage() //handleRegionZone() // handlePriceInfo() diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/main/conf/Test_Config.go b/cloud-control-manager/cloud-driver/drivers/gcp/main/conf/Test_Config.go index 60f172829..2e42d1c5a 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/main/conf/Test_Config.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/main/conf/Test_Config.go @@ -92,6 +92,8 @@ func GetResourceHandler(handlerType string) (interface{}, error) { // resourceHandler, err = cloudConnection.CreateVNetworkHandler() // case "VNic": // resourceHandler, err = cloudConnection.CreateVNicHandler() + case "Disk": + resourceHandler, err = cloudConnection.CreateDiskHandler() case "VM": resourceHandler, err = cloudConnection.CreateVMHandler() case "KeyPair": From cc6e296b3917cace37b0d98459a2ee02e4d0c103 Mon Sep 17 00:00:00 2001 From: dev4unet Date: Fri, 19 Jul 2024 10:27:58 +0000 Subject: [PATCH 23/56] Added Tag functionality to the creation and lookup of each handler. Testing in progress (VPC ok / Keypair ok) --- .../drivers/aws/main/Test_Resources.go | 29 ++++++++---- .../drivers/aws/resources/ClusterHandler.go | 4 ++ .../drivers/aws/resources/DiskHandler.go | 4 +- .../drivers/aws/resources/KeyPairHandler.go | 20 +++++++-- .../drivers/aws/resources/MyImageHandler.go | 2 + .../drivers/aws/resources/NLBHandler.go | 3 ++ .../drivers/aws/resources/SecurityHandler.go | 2 + .../drivers/aws/resources/TagHandler.go | 45 +++++++++++++++++++ .../drivers/aws/resources/VPCHandler.go | 32 +++++++++---- 9 files changed, 119 insertions(+), 22 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go index b2c0d5e4a..5c22326e9 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go @@ -467,6 +467,8 @@ func handleKeyPair() { //VmID := config.Aws.VmID keyPairName := "CB-KeyPairTest123123" + //keyPairName := "key-0a58c9a7b0a07a2d2" + //keyPairName := config.Aws.KeyName for { @@ -503,6 +505,8 @@ func handleKeyPair() { keyPairReqInfo := irs.KeyPairReqInfo{ IId: irs.IID{NameId: keyPairName}, //Name: keyPairName, + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: keyPairName+"123"}}, + TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}}, } result, err := KeyPairHandler.CreateKey(keyPairReqInfo) if err != nil { @@ -635,6 +639,8 @@ func handleVPC() { subnetReqInfo := irs.SubnetInfo{ IId: irs.IID{NameId: "AddTest-Subnet"}, IPv4_CIDR: "10.0.2.0/24", + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Subnet Name Value1"}, {Key: "Name2", Value: "Subnet Name Value2"}, {Key: "Name", Value: "AddTest-Subnet123"}}, + TagList: []irs.KeyValue{{Key: "Name1", Value: "Subnet Name Value1"}, {Key: "Name2", Value: "Subnet Name Value2"}}, } subnetReqVpcInfo := irs.IID{SystemId: "vpc-00e513fd64a7d9972"} @@ -649,6 +655,8 @@ func handleVPC() { { IId: irs.IID{NameId: "New-CB-Subnet"}, IPv4_CIDR: "10.0.1.0/24", + TagList: []irs.KeyValue{{Key: "Name1", Value: "Subnet Name Value1"}, {Key: "Name2", Value: "Subnet Name Value2"}, {Key: "Name", Value: "AddTest-Subnet123"}}, + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Subnet Name Value1"}, {Key: "Name2", Value: "Subnet Name Value2"}}, }, /* { @@ -661,6 +669,8 @@ func handleVPC() { //Name: "CB-VNet-Subnet", // 웹 도구 등 외부에서 전달 받지 않고 드라이버 내부적으로 자동 구현때문에 사용하지 않음. //CidrBlock: "10.0.0.0/16", //CidrBlock: "192.168.0.0/16", + TagList: []irs.KeyValue{{Key: "Name1", Value: "Subnet Name Value1"}, {Key: "Name2", Value: "Subnet Name Value2"}, {Key: "Name", Value: "New-CB-VPC123"}}, + //TagList: []irs.KeyValue{{Key: "Name1", Value: "VPC Name Value1"}, {Key: "Name2", Value: "VPC Name Value2"}}, } reqSubnetId := irs.IID{SystemId: "vpc-04f6de5c2af880978"} @@ -1787,12 +1797,14 @@ func handleTag() { } handler := ResourceHandler.(irs.TagHandler) - var reqType irs.RSType = irs.VM - reqIID := irs.IID{SystemId: "i-02ac1c4ff1d40815c"} + var reqType irs.RSType = irs.KEY + //reqIID := irs.IID{SystemId: "i-02ac1c4ff1d40815c"} + reqIID := irs.IID{SystemId: "CB-KeyPairTest123123"} + reqTag := irs.KeyValue{Key: "tag3", Value: "tag3 test"} reqKey := "tag3" reqKey = "" - reqType = irs.ALL + //reqType = irs.ALL for { fmt.Println("TagHandler Management") @@ -2057,17 +2069,18 @@ func readConfigFile() Config { } func main() { + handleKeyPair() + handleTag() //handleVPC() - // handleKeyPair() // handlePublicIP() // PublicIP 생성 후 conf - // handleSecurity() + handleSecurity() handleVM() // handleImage() //AMI // handleVNic() //Lancard // handleVMSpec() - // handleNLB() - // handleCluster() + handleNLB() + handleCluster() //handleRegionZone() //handlePriceInfo() - //handleTag() + handleTag() } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go index bd0e86d82..6b4f6c121 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/ClusterHandler.go @@ -178,6 +178,8 @@ func (ClusterHandler *AwsClusterHandler) CreateCluster(clusterReqInfo irs.Cluste return irs.ClusterInfo{}, errClusterInfo } clusterInfo.IId.NameId = clusterReqInfo.IId.NameId + clusterInfo.TagList, _ = ClusterHandler.TagHandler.ListTag(irs.CLUSTER, clusterInfo.IId) + return clusterInfo, nil } @@ -393,6 +395,8 @@ func (ClusterHandler *AwsClusterHandler) GetCluster(clusterIID irs.IID) (irs.Clu } clusterInfo.KeyValueList = keyValueList + clusterInfo.TagList, _ = ClusterHandler.TagHandler.ListTag(irs.CLUSTER, clusterInfo.IId) + //노드 그룹 처리 resNodeGroupList, errNodeGroup := ClusterHandler.ListNodeGroup(clusterInfo.IId) if errNodeGroup != nil { diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go index 37e6cece8..d479554b4 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/DiskHandler.go @@ -197,7 +197,7 @@ func (DiskHandler *AwsDiskHandler) GetDisk(diskIID irs.IID) (irs.DiskInfo, error } calllogger.Info(call.String(hiscallInfo)) - diskInfo, err := DiskHandler.convertVolumeInfoToDiskInfo(result.Volumes[0]) + diskInfo, _ := DiskHandler.convertVolumeInfoToDiskInfo(result.Volumes[0]) return diskInfo, nil } @@ -910,5 +910,7 @@ func (DiskHandler *AwsDiskHandler) convertVolumeInfoToDiskInfo(volumeInfo *ec2.V cblogger.Debug("keyvalue2") cblogger.Debug(diskInfo) + diskInfo.TagList, _ = DiskHandler.TagHandler.ListTag(irs.DISK, diskInfo.IId) + return diskInfo, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go index 02f2b5b45..e5639c227 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/KeyPairHandler.go @@ -13,6 +13,7 @@ import ( call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" + "github.com/davecgh/go-spew/spew" //_ "github.com/davecgh/go-spew/spew" ) @@ -108,7 +109,7 @@ func (keyPairHandler *AwsKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPairReq */ // Convert TagList to TagSpecifications - tagSpecifications, err := ConvertTagListToTagSpecifications("key-pair", keyPairReqInfo.TagList) + tagSpecifications, err := ConvertTagListToTagSpecifications("key-pair", keyPairReqInfo.TagList, keyPairReqInfo.IId.NameId) if err != nil { return irs.KeyPairInfo{}, fmt.Errorf("failed to convert tag list: %w", err) } @@ -125,12 +126,16 @@ func (keyPairHandler *AwsKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPairReq ErrorMSG: "", } callLogStart := call.Start() - // Creates a new key pair with the given name - result, err := keyPairHandler.Client.CreateKeyPair(&ec2.CreateKeyPairInput{ + + input := &ec2.CreateKeyPairInput{ //KeyName: aws.String(keyPairReqInfo.Name), KeyName: aws.String(keyPairReqInfo.IId.NameId), TagSpecifications: tagSpecifications, - }) + } + // Creates a new key pair with the given name + result, err := keyPairHandler.Client.CreateKeyPair(input) + spew.Dump(result) + callLogInfo.ElapsedTime = call.Elapsed(callLogStart) if err != nil { @@ -204,6 +209,9 @@ func (keyPairHandler *AwsKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPairReq return irs.KeyPairInfo{}, err } */ + + keyPairInfo.TagList, _ = keyPairHandler.TagHandler.ListTag(irs.KEY, keyPairInfo.IId) + return keyPairInfo, nil } @@ -245,6 +253,7 @@ func (keyPairHandler *AwsKeyPairHandler) GetKey(keyIID irs.IID) (irs.KeyPairInfo callLogStart := call.Start() result, err := keyPairHandler.Client.DescribeKeyPairs(input) + spew.Dump(result) callLogInfo.ElapsedTime = call.Elapsed(callLogStart) cblogger.Debug("result : ", result) cblogger.Debug("err : ", err) @@ -278,6 +287,9 @@ func (keyPairHandler *AwsKeyPairHandler) GetKey(keyIID irs.IID) (irs.KeyPairInfo return irs.KeyPairInfo{}, errKeyPair } + keyPairInfo.TagList, _ = keyPairHandler.TagHandler.ListTag(irs.KEY, keyPairInfo.IId) + spew.Dump(keyPairInfo.TagList) + cblogger.Debug(keyPairInfo) return keyPairInfo, nil } else { diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go index e34373b7d..80c709f88 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/MyImageHandler.go @@ -407,6 +407,8 @@ func (ImageHandler *AwsMyImageHandler) GetMyImage(myImageIID irs.IID) (irs.MyIma return irs.MyImageInfo{}, err } + returnMyImage.TagList, _ = ImageHandler.TagHandler.ListTag(irs.MYIMAGE, returnMyImage.IId) + return returnMyImage, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go index 318fd8556..44bac573a 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/NLBHandler.go @@ -813,6 +813,9 @@ func (NLBHandler *AwsNLBHandler) GetNLB(nlbIID irs.IID) (irs.NLBInfo, error) { if errInfo != nil { return irs.NLBInfo{}, errInfo } + + nlbInfo.TagList, _ = NLBHandler.TagHandler.ListTag(irs.NLB, nlbInfo.IId) + return nlbInfo, nil } else { return irs.NLBInfo{}, errors.New("InvalidNLBArn.NotFound: The NLB Arn '" + nlbIID.SystemId + "' does not exist") diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go index b14f6b7c6..40cd26dba 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/SecurityHandler.go @@ -411,6 +411,8 @@ func (securityHandler *AwsSecurityHandler) GetSecurity(securityIID irs.IID) (irs if len(result.SecurityGroups) > 0 { securityInfo := ExtractSecurityInfo(result.SecurityGroups[0]) + securityInfo.TagList, _ = securityHandler.TagHandler.ListTag(irs.SG, securityInfo.IId) + return securityInfo, nil } else { //return irs.SecurityInfo{}, errors.New("[" + securityNameId + "] 정보를 찾을 수 없습니다.") diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go index 64aab4508..23957e90a 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/aws/aws-sdk-go/aws" + "github.com/davecgh/go-spew/spew" //"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" @@ -72,6 +73,8 @@ func (tagHandler *AwsTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag return irs.KeyValue{}, errors.New(msg) } + resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.SystemId, "CreateTags()") start := call.Start() @@ -109,6 +112,7 @@ func (tagHandler *AwsTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([] return nil, errors.New(msg) } + resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error input := &ec2.DescribeTagsInput{ Filters: []*ec2.Filter{ { @@ -159,6 +163,7 @@ func (tagHandler *AwsTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key cblogger.Error(msg) return irs.KeyValue{}, errors.New(msg) } + resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error input := &ec2.DescribeTagsInput{ Filters: []*ec2.Filter{ @@ -217,6 +222,40 @@ func (tagHandler *AwsTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key return retTag, nil } +// Handles targets that have a Name-based to Id conversion task, like Keypair. +// Keypair should use id, not name. +func (tagHandler *AwsTagHandler) GetRealResourceId(resType irs.RSType, resIID irs.IID) irs.IID { + + cblogger.Debugf("resType : [%s] / resIID : [%s]", resType, resIID.SystemId) + + if resType != irs.KEY { + return resIID + } + + // + // Keypair should use id, not name, when using Tag-related APIs. + // + input := &ec2.DescribeKeyPairsInput{ + KeyNames: []*string{ + aws.String(resIID.SystemId), + }, + } + + result, err := tagHandler.Client.DescribeKeyPairs(input) + spew.Dump(result) + if err != nil { + cblogger.Error(err) + return resIID + } + + if len(result.KeyPairs) > 0 { + newIID := irs.IID{NameId: *result.KeyPairs[0].KeyName, SystemId: *result.KeyPairs[0].KeyPairId} + return newIID + } + + return resIID +} + func (tagHandler *AwsTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string) (bool, error) { cblogger.Debugf("Req resTyp:[%s] / resIID:[%s] / key:[%s]", resType, resIID, key) @@ -225,6 +264,7 @@ func (tagHandler *AwsTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, k cblogger.Error(msg) return false, errors.New(msg) } + resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error input := &ec2.DeleteTagsInput{ Resources: []*string{ @@ -269,6 +309,11 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] var filters []*ec2.Filter + if resType == irs.KEY { + resIID := tagHandler.GetRealResourceId(resType, irs.IID{SystemId: keyword}) // fix some resource id error + keyword = resIID.SystemId + } + // Add resource type filter if resType is not ALL if resType != irs.ALL { if awsResType, ok := rsTypeToAwsResourceTypeMap[resType]; ok { diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go index fa1bfd2f3..29e399086 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/VPCHandler.go @@ -298,18 +298,25 @@ func (VPCHandler *AwsVPCHandler) CreateSubnet(vpcId string, reqSubnetInfo irs.Su if reqSubnetInfo.IId.SystemId != "" { vpcInfo, errVpcInfo := VPCHandler.GetSubnet(reqSubnetInfo.IId.SystemId) if errVpcInfo == nil { - cblogger.Errorf("[%S] subnet already exists. returns an error without creating it.", reqSubnetInfo.IId.SystemId) + cblogger.Errorf("[%s] subnet already exists. returns an error without creating it", reqSubnetInfo.IId.SystemId) cblogger.Info(vpcInfo) return vpcInfo, errors.New("InvalidVNetwork.Duplicate: The Subnet '" + reqSubnetInfo.IId.SystemId + "' already exists.") } } - //서브넷 생성 + // Convert TagList to TagSpecifications + tagSpecifications, err := ConvertTagListToTagSpecifications("subnet", reqSubnetInfo.TagList, reqSubnetInfo.IId.NameId) + if err != nil { + return irs.SubnetInfo{}, fmt.Errorf("failed to convert tag list: %w", err) + } + + // Create subnet input with tag specifications input := &ec2.CreateSubnetInput{ CidrBlock: aws.String(reqSubnetInfo.IPv4_CIDR), VpcId: aws.String(vpcId), //AvailabilityZoneId: aws.String(zoneId), //use1-az1, use1-az2, use1-az3, use1-az4, use1-az5, use1-az6 - AvailabilityZone: aws.String(zoneId), + AvailabilityZone: aws.String(zoneId), + TagSpecifications: tagSpecifications, } // logger for HisCall @@ -351,13 +358,16 @@ func (VPCHandler *AwsVPCHandler) CreateSubnet(vpcId string, reqSubnetInfo irs.Su //vNetworkInfo := irs.VNetworkInfo{} vNetworkInfo := ExtractSubnetDescribeInfo(result.Subnet) + vNetworkInfo.TagList, _ = VPCHandler.TagHandler.ListTag(irs.SUBNET, vNetworkInfo.IId) - //Subnet Name 태깅 - if SetNameTag(VPCHandler.Client, *result.Subnet.SubnetId, reqSubnetInfo.IId.NameId) { - cblogger.Infof("set %s Name to subnet", reqSubnetInfo.IId.NameId) - } else { - cblogger.Errorf("set %s Name to subnet failed", reqSubnetInfo.IId.NameId) - } + /* + //Subnet Name 태깅 + if SetNameTag(VPCHandler.Client, *result.Subnet.SubnetId, reqSubnetInfo.IId.NameId) { + cblogger.Infof("set %s Name to subnet", reqSubnetInfo.IId.NameId) + } else { + cblogger.Errorf("set %s Name to subnet failed", reqSubnetInfo.IId.NameId) + } + */ vNetworkInfo.IId.NameId = reqSubnetInfo.IId.NameId @@ -985,6 +995,8 @@ func (VPCHandler *AwsVPCHandler) ListSubnet(vpcId string) ([]irs.SubnetInfo, err for _, curSubnet := range result.Subnets { cblogger.Infof("Retrieve [%s] Subnet info", *curSubnet.SubnetId) arrSubnetInfo := ExtractSubnetDescribeInfo(curSubnet) + arrSubnetInfo.TagList, _ = VPCHandler.TagHandler.ListTag(irs.SUBNET, arrSubnetInfo.IId) + //arrSubnetInfo, errSubnet := VPCHandler.GetSubnet(*curSubnet.SubnetId) /* if errSubnet != nil { @@ -1046,6 +1058,8 @@ func (VPCHandler *AwsVPCHandler) GetSubnet(reqSubnetId string) (irs.SubnetInfo, if !reflect.ValueOf(result.Subnets).IsNil() { retSubnetInfo := ExtractSubnetDescribeInfo(result.Subnets[0]) + retSubnetInfo.TagList, _ = VPCHandler.TagHandler.ListTag(irs.SUBNET, retSubnetInfo.IId) + return retSubnetInfo, nil } else { return irs.SubnetInfo{}, errors.New("InvalidSubnet.NotFound: The CBVnetwork '" + reqSubnetId + "' does not exist") From fc83e87f425087c5a3d21d1a66c4b1d6fbb764f1 Mon Sep 17 00:00:00 2001 From: raccoon-pi Date: Mon, 22 Jul 2024 05:21:13 +0000 Subject: [PATCH 24/56] Create Tencent Cloud Tag handler --- .../drivers/tencent/TencentDriver.go | 81 ++- .../tencent/connect/TencentCloudConnection.go | 39 +- .../drivers/tencent/main/Test_Resources.go | 124 ++++- .../drivers/tencent/main/conf/Test_Config.go | 14 +- .../drivers/tencent/resources/TagHandler.go | 490 ++++++++++++++++++ go.mod | 3 +- go.sum | 4 + 7 files changed, 711 insertions(+), 44 deletions(-) create mode 100644 cloud-control-manager/cloud-driver/drivers/tencent/resources/TagHandler.go diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/TencentDriver.go b/cloud-control-manager/cloud-driver/drivers/tencent/TencentDriver.go index 330821aa5..8f6d54a70 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/TencentDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/TencentDriver.go @@ -23,8 +23,12 @@ import ( "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" + tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312" + tkeprofile "github.com/tencentcloud/tencentcloud-sdk-go-intl-en/tencentcloud/common/profile" + tke "github.com/tencentcloud/tencentcloud-sdk-go-intl-en/tencentcloud/tke/v20180525" + cblog "github.com/cloud-barista/cb-log" "github.com/sirupsen/logrus" ) @@ -62,9 +66,9 @@ func init() { func getVmClient(connectionInfo idrv.ConnectionInfo) (*cvm.Client, error) { // setup Region - cblogger.Debug("TencentDriver : getVpcClient() - Region : [" + connectionInfo.RegionInfo.Region + "]") - cblogger.Debug("TencentDriver : getVpcClient() - Zone : [" + connectionInfo.RegionInfo.Zone + "]") - cblogger.Debug("TencentDriver : getVpcClient() - ClientId : [" + connectionInfo.CredentialInfo.ClientId + "]") + cblogger.Debug("TencentDriver : getVmClient() - Region : [" + connectionInfo.RegionInfo.Region + "]") + cblogger.Debug("TencentDriver : getVmClient() - Zone : [" + connectionInfo.RegionInfo.Zone + "]") + cblogger.Debug("TencentDriver : getVmClient() - ClientId : [" + connectionInfo.CredentialInfo.ClientId + "]") zoneId := connectionInfo.RegionInfo.Zone if len(zoneId) < 1 { @@ -124,9 +128,9 @@ func getVpcClient(connectionInfo idrv.ConnectionInfo) (*vpc.Client, error) { func getClbClient(connectionInfo idrv.ConnectionInfo) (*clb.Client, error) { // setup Region - cblogger.Debug("TencentDriver : getVpcClient() - Region : [" + connectionInfo.RegionInfo.Region + "]") - cblogger.Debug("TencentDriver : getVpcClient() - Zone : [" + connectionInfo.RegionInfo.Zone + "]") - cblogger.Debug("TencentDriver : getVpcClient() - ClientId : [" + connectionInfo.CredentialInfo.ClientId + "]") + cblogger.Debug("TencentDriver : getClbClient() - Region : [" + connectionInfo.RegionInfo.Region + "]") + cblogger.Debug("TencentDriver : getClbClient() - Zone : [" + connectionInfo.RegionInfo.Zone + "]") + cblogger.Debug("TencentDriver : getClbClient() - ClientId : [" + connectionInfo.CredentialInfo.ClientId + "]") zoneId := connectionInfo.RegionInfo.Zone if len(zoneId) < 1 { @@ -155,9 +159,9 @@ func getClbClient(connectionInfo idrv.ConnectionInfo) (*clb.Client, error) { func getCbsClient(connectionInfo idrv.ConnectionInfo) (*cbs.Client, error) { // setup Region - cblogger.Debug("TencentDriver : getVpcClient() - Region : [" + connectionInfo.RegionInfo.Region + "]") - cblogger.Debug("TencentDriver : getVpcClient() - Zone : [" + connectionInfo.RegionInfo.Zone + "]") - cblogger.Debug("TencentDriver : getVpcClient() - ClientId : [" + connectionInfo.CredentialInfo.ClientId + "]") + cblogger.Debug("TencentDriver : getCbsClient() - Region : [" + connectionInfo.RegionInfo.Region + "]") + cblogger.Debug("TencentDriver : getCbsClient() - Zone : [" + connectionInfo.RegionInfo.Zone + "]") + cblogger.Debug("TencentDriver : getCbsClient() - ClientId : [" + connectionInfo.CredentialInfo.ClientId + "]") zoneId := connectionInfo.RegionInfo.Zone if len(zoneId) < 1 { @@ -184,6 +188,48 @@ func getCbsClient(connectionInfo idrv.ConnectionInfo) (*cbs.Client, error) { return client, nil } +func getTagClient(connectionInfo idrv.ConnectionInfo) (*tag.Client, error) { + cblogger.Debug("TencentDriver : getTagClient() - ClientId : [" + connectionInfo.CredentialInfo.ClientId + "]") + + credential := common.NewCredential( + connectionInfo.CredentialInfo.ClientId, + connectionInfo.CredentialInfo.ClientSecret, + ) + + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "tag.tencentcloudapi.com" + cpf.Language = "en-US" //메시지를 영어로 설정 + client, err := tag.NewClient(credential, "", cpf) + if err != nil { + cblogger.Error("Could not create New Session") + cblogger.Error(err) + return nil, err + } + + return client, nil +} + +func getClusterClient(connectionInfo idrv.ConnectionInfo) (*tke.Client, error) { + cblogger.Debug("TencentDriver : getClusterClient() - Region : [" + connectionInfo.RegionInfo.Region + "]") + cblogger.Debug("TencentDriver : getClusterClient() - ClientId : [" + connectionInfo.CredentialInfo.ClientId + "]") + + credential := common.NewCredential( + connectionInfo.CredentialInfo.ClientId, + connectionInfo.CredentialInfo.ClientSecret, + ) + + cpf := tkeprofile.NewClientProfile() + cpf.HttpProfile.Endpoint = "tke.tencentcloudapi.com" + client, err := tke.NewClient(credential, connectionInfo.RegionInfo.Region, cpf) + if err != nil { + cblogger.Error("Could not create New Session") + cblogger.Error(err) + return nil, err + } + + return client, nil +} + func (driver *TencentDriver) ConnectCloud(connectionInfo idrv.ConnectionInfo) (icon.CloudConnection, error) { // 1. get info of credential and region for Test A Cloud from connectionInfo. // 2. create a client object(or service object) of Test A Cloud with credential info. @@ -219,6 +265,18 @@ func (driver *TencentDriver) ConnectCloud(connectionInfo idrv.ConnectionInfo) (i return nil, err } + tagClient, err := getTagClient(connectionInfo) + if err != nil { + cblogger.Error(err) + return nil, err + } + + clusterClient, err := getClusterClient(connectionInfo) + if err != nil { + cblogger.Error(err) + return nil, err + } + iConn := tcon.TencentCloudConnection{ CredentialInfo: connectionInfo.CredentialInfo, Region: connectionInfo.RegionInfo, @@ -232,9 +290,8 @@ func (driver *TencentDriver) ConnectCloud(connectionInfo idrv.ConnectionInfo) (i DiskClient: cbsClient, MyImageClient: vmClient, RegionZoneClient: vmClient, - - //VNicClient: vmClient, - //PublicIPClient: vmClient, + TagClient: tagClient, + ClusterClient: clusterClient, } return &iConn, nil // return type: (icon.CloudConnection, error) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/connect/TencentCloudConnection.go b/cloud-control-manager/cloud-driver/drivers/tencent/connect/TencentCloudConnection.go index a063271cc..d9e5b7cc2 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/connect/TencentCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/connect/TencentCloudConnection.go @@ -17,15 +17,14 @@ import ( irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" "github.com/sirupsen/logrus" - //"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - //"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" - //"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - cbs "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cbs/v20170312" clb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317" cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" + tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312" + tke "github.com/tencentcloud/tencentcloud-sdk-go-intl-en/tencentcloud/tke/v20180525" + "errors" ) @@ -42,8 +41,8 @@ type TencentCloudConnection struct { DiskClient *cbs.Client MyImageClient *cvm.Client RegionZoneClient *cvm.Client - //VNicClient *cvm.Client - //PublicIPClient *cvm.Client + TagClient *tag.Client + ClusterClient *tke.Client } var cblogger *logrus.Logger @@ -109,22 +108,6 @@ func (cloudConn *TencentCloudConnection) CreateVMSpecHandler() (irs.VMSpecHandle return &handler, nil } -/* -func (cloudConn *TencentCloudConnection) CreateVNicHandler() (irs.VNicHandler, error) { - cblogger.Info("Start") - handler := trs.TencentVNicHandler{cloudConn.Region, cloudConn.VNicClient} - - return &handler, nil -} - -func (cloudConn *TencentCloudConnection) CreatePublicIPHandler() (irs.PublicIPHandler, error) { - cblogger.Info("Start") - handler := trs.TencentPublicIPHandler{cloudConn.Region, cloudConn.PublicIPClient} - - return &handler, nil -} -*/ - func (cloudConn *TencentCloudConnection) CreateDiskHandler() (irs.DiskHandler, error) { cblogger.Info("Start") @@ -164,5 +147,15 @@ func (cloudConn *TencentCloudConnection) CreatePriceInfoHandler() (irs.PriceInfo } func (cloudConn *TencentCloudConnection) CreateTagHandler() (irs.TagHandler, error) { - return nil, errors.New("Tencent Driver: not implemented") + handler := trs.TencentTagHandler{ + Region: cloudConn.Region, + TagClient: cloudConn.TagClient, + // below client is for validate resources + VNetworkClient: cloudConn.VNetworkClient, + VMClient: cloudConn.VMClient, + NLBClient: cloudConn.NLBClient, + DiskClient: cloudConn.DiskClient, + ClusterClient: cloudConn.ClusterClient, + } + return &handler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/tencent/main/Test_Resources.go index 7078fd743..9fb6dd12a 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/main/Test_Resources.go @@ -1510,6 +1510,123 @@ func handleRegionZone() { } } +func handleTag() { + cblogger.Debug("Start handle Tag Test") + + ResourceHandler, err := testconf.GetResourceHandler("Tag") + if err != nil { + panic(err) + } + handler := ResourceHandler.(irs.TagHandler) + + var reqType irs.RSType = irs.VM + reqIID := irs.IID{SystemId: "ins-grur8hw9"} + reqTag := irs.KeyValue{Key: "testTagKey", Value: "testTagValue"} + reqKey := "testTagKey" + reqKey = "" + reqType = irs.ALL + + for { + fmt.Println("TagHandler Management") + fmt.Println("0. Quit") + fmt.Println("1. Tag List") + fmt.Println("2. Tag Add") + fmt.Println("3. Tag Get") + fmt.Println("4. Tag Delete") + fmt.Println("5. Tag Find") + + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + panic(err) + } + + if inputCnt == 1 { + switch commandNum { + case 0: + return + + case 1: + reqIID = irs.IID{SystemId: "ins-grur8hw9"} + reqType = irs.VM + cblogger.Infof("조회 요청 태그 타입 : [%s]", reqType) + if reqType == irs.VM { + cblogger.Debug("VM 요청됨") + } + result, err := handler.ListTag(reqType, reqIID) + if err != nil { + cblogger.Info(" Tag 목록 조회 실패 : ", err) + } else { + cblogger.Info("Tag 목록 조회 결과") + cblogger.Debug(result) + cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + //spew.Dump(result) + cblogger.Info("출력 결과 수 : ", len(result)) + + //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. + if result != nil { + //tagReqInfo.IId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 + } + } + + case 2: + reqIID = irs.IID{SystemId: "ins-grur8hw9"} + reqTag = irs.KeyValue{Key: "tagkey5", Value: "tagvalue5"} + reqType = irs.VM + cblogger.Infof("[%s] Tag 추가 테스트", reqIID.SystemId) + result, err := handler.AddTag(reqType, reqIID, reqTag) + if err != nil { + cblogger.Infof(reqIID.SystemId, " Tag 생성 실패 : ", err) + } else { + cblogger.Info("Tag 생성 결과 : ", result) + reqKey = result.Key + cblogger.Infof("요청 대상 Tag Key가 [%s]로 변경 됨", reqKey) + spew.Dump(result) + } + + case 3: + reqIID = irs.IID{SystemId: "ins-grur8hw9"} + reqKey = "tagkey5" + reqType = irs.VM + cblogger.Infof("[%s] Tag 조회 테스트 - Key[%s]", reqIID.SystemId, reqKey) + result, err := handler.GetTag(reqType, reqIID, reqKey) + if err != nil { + cblogger.Infof("[%s] Tag 조회 실패 : [%v]", reqKey, err) + } else { + cblogger.Infof("[%s] Tag 조회 결과 : [%s]", reqKey, result) + spew.Dump(result) + } + + case 4: + reqIID = irs.IID{SystemId: "ins-grur8hw9"} + reqKey = "tagkey5" + reqType = irs.VM + cblogger.Infof("[%s] Tag 삭제 테스트 - Key[%s]", reqIID.SystemId, reqKey) + result, err := handler.RemoveTag(reqType, reqIID, reqKey) + if err != nil { + cblogger.Infof("[%s] Tag 삭제 실패 : [%v]", reqKey, err) + } else { + cblogger.Infof("[%s] Tag 삭제 결과 : [%v]", reqKey, result) + } + + case 5: + reqKey = "tagkey5" + // reqKey = "testTagValue" + reqType = irs.ALL + cblogger.Infof("[%s] Tag 찾기 테스트 - Key[%s]", reqType, reqKey) + result, err := handler.FindTag(reqType, reqKey) + if err != nil { + cblogger.Infof("[%s] Tag 검색 실패 : [%s]", reqKey, err) + } else { + cblogger.Infof("[%s] Tag 검색 결과 : [%d]건", reqKey, len(result)) + spew.Dump(result) + cblogger.Infof("Tag 검색 결과 : [%d]건", len(result)) + } + } + } + } +} + func handlePriceInfo() { cblogger.Debug("Start PriceInfoHandler Resource Test") @@ -1574,8 +1691,8 @@ func handlePriceInfo() { func main() { cblogger.Info("Tencent Cloud Resource Test") - //handleVPC() //VPC - //handleNLB() + // handleVPC() //VPC + // //handleNLB() //handleVMSpec() //handleSecurity() //handleImage() //AMI @@ -1586,5 +1703,6 @@ func main() { //handlePublicIP() // PublicIP 생성 후 conf // handleRegionZone() //handleVNic() //Lancard - handlePriceInfo() + // handlePriceInfo() + handleTag() } diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/main/conf/Test_Config.go b/cloud-control-manager/cloud-driver/drivers/tencent/main/conf/Test_Config.go index 0f0840e93..5e43e3059 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/main/conf/Test_Config.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/main/conf/Test_Config.go @@ -12,6 +12,7 @@ package TencentTestConfig import ( "io/ioutil" + "os" tdrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/tencent" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" @@ -43,10 +44,12 @@ type Config struct { // 환경변수 CBSPIDER_PATH 설정 후 해당 폴더 하위에 /config/configTencent.yaml 파일 생성해야 함. func ReadConfigFile() Config { // Set Environment Value of Project Root Path - // /mnt/d/Workspace/mcloud-barista-config/config/config.yaml - //testFilePath := os.Getenv("CBSPIDER_PATH") + "/config/configTencent.yaml" //혹시 모를 키 노출 대비 시스템 외부에 존재(개발용용) - testFilePath := "./conf/testConfigTencent.yaml" - cblogger.Debugf("Test Data 설정파일 : [%]", testFilePath) + testFilePath := os.Getenv("CBSPIDER_TEST_CONF_PATH") + cblogger.Info("Set the full path to the config files you want to use for testing, including your AWS credentials, in the OS environment variable [CBSPIDER_TEST_CONF_PATH].") + cblogger.Infof("OS environment variable [CBSPIDER_TEST_CONF_PATH] : [%s]", testFilePath) + + // testFilePath := "./conf/testConfigTencent.yaml" + // cblogger.Debugf("Test Data 설정파일 : [%]", testFilePath) data, err := ioutil.ReadFile(testFilePath) if err != nil { @@ -122,7 +125,8 @@ func GetResourceHandler(handlerType string) (interface{}, error) { resourceHandler, err = cloudConnection.CreateRegionZoneHandler() case "PriceInfo": resourceHandler, err = cloudConnection.CreatePriceInfoHandler() - + case "Tag": + resourceHandler, err = cloudConnection.CreateTagHandler() } if err != nil { diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/TagHandler.go new file mode 100644 index 000000000..394a2d1ab --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/TagHandler.go @@ -0,0 +1,490 @@ +package resources + +import ( + "fmt" + "strings" + + call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" + idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" + irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" + taglib "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" + + cbs "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cbs/v20170312" + clb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317" + cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" + vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312" + + tke "github.com/tencentcloud/tencentcloud-sdk-go-intl-en/tencentcloud/tke/v20180525" +) + +type TencentTagHandler struct { + Region idrv.RegionInfo + TagClient *taglib.Client + + VNetworkClient *vpc.Client + VMClient *cvm.Client + NLBClient *clb.Client + DiskClient *cbs.Client + ClusterClient *tke.Client +} + +// Map of RSType to Tencent resource types +// tencent need to provide service Type arg, {ServiceType}:{ResourcePrefix} +var rsTypeToTencentTypeMap = map[irs.RSType]string{ + irs.VPC: "vpc:vpc", + irs.SUBNET: "vpc:subnet", + irs.SG: "cvm:sg", + irs.KEY: "cvm:keypair", + irs.VM: "cvm:instance", + irs.NLB: "clb:clb", + irs.DISK: "cvm:volume", + irs.MYIMAGE: "cvm:image", + irs.CLUSTER: "ccs:cluster", +} + +// Map of RSType to Tencent resource types reversed +// tencent need to provide service Type arg, {resourceIdStartStr}:irs.Type +var resourceIdStartStrToRsType = map[string]irs.RSType{ + "vpc": irs.VPC, + "subnet": irs.SUBNET, + "sg": irs.SG, + "skey": irs.KEY, + "ins": irs.VM, + "lb": irs.NLB, + "disk": irs.DISK, + "img": irs.MYIMAGE, + "cls": irs.CLUSTER, +} + +// tencent need to provide service Type arg. +// rsTypeToTencentTypeMap arg return split by ':' (ServiceType, ResourcePrefix, resourceIdStartStr) +// return ServiceType and ResourcePrefix +func rsTypeToTencentTypeMapParse(rstypeTencent string) (string, string) { + rstypeTencentArr := strings.SplitN(rstypeTencent, ":", 2) + return rstypeTencentArr[0], rstypeTencentArr[1] +} + +func uniqueStringSlice(strings []string) []string { + stringMap := make(map[string]struct{}) + uniqueStrings := []string{} + + for _, str := range strings { + if _, exists := stringMap[str]; !exists { + stringMap[str] = struct{}{} + uniqueStrings = append(uniqueStrings, str) + } + } + + return uniqueStrings +} + +func validateResource(t *TencentTagHandler, resType irs.RSType, resIID irs.IID) (bool, error) { + switch resType { + case irs.VPC: // region Require + request := vpc.NewDescribeVpcsRequest() + request.VpcIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.VNetworkClient.DescribeVpcs(request) + if err != nil { + err := fmt.Errorf("An VPC API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + case irs.SUBNET: // region Require + request := vpc.NewDescribeSubnetsRequest() + request.SubnetIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.VNetworkClient.DescribeSubnets(request) + if err != nil { + err := fmt.Errorf("An SUBNET API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + // VPC와 클라이언트 공유함. + case irs.SG: // region Require + request := vpc.NewDescribeSecurityGroupsRequest() + request.SecurityGroupIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.VNetworkClient.DescribeSecurityGroups(request) + if err != nil { + err := fmt.Errorf("An SG API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + // VM과 클라이언트 공유함. + case irs.KEY: // region Require + request := cvm.NewDescribeKeyPairsRequest() + request.KeyIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.VMClient.DescribeKeyPairs(request) + if err != nil { + err := fmt.Errorf("An KEY API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + case irs.VM: // region Require + request := cvm.NewDescribeInstancesRequest() + request.InstanceIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.VMClient.DescribeInstances(request) + if err != nil { + err := fmt.Errorf("An VM API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + case irs.NLB: // region Require + request := clb.NewDescribeLoadBalancersRequest() + request.LoadBalancerIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.NLBClient.DescribeLoadBalancers(request) + if err != nil { + err := fmt.Errorf("An NLB API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + case irs.DISK: // region Require + request := cbs.NewDescribeDisksRequest() + request.DiskIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.DiskClient.DescribeDisks(request) + if err != nil { + err := fmt.Errorf("An DISK API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + // VM과 클라이언트 공유함. + case irs.MYIMAGE: // region Require + request := cvm.NewDescribeImagesRequest() + request.ImageIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.VMClient.DescribeImages(request) + if err != nil { + err := fmt.Errorf("An MYIMAGE API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + case irs.CLUSTER: // region Require + request := tke.NewDescribeClustersRequest() + request.ClusterIds = common.StringPtrs([]string{resIID.SystemId}) + response, err := t.ClusterClient.DescribeClusters(request) + if err != nil { + err := fmt.Errorf("An CLUSTER API error has returned: %s", err.Error()) + return false, err + } + if *response.Response.TotalCount > 0 { + return true, nil + } + return false, nil + + default: + msg := "no resType to validate" + return false, fmt.Errorf("%s", msg) + } +} + +func (t *TencentTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + cblogger.Debugf("Req resTyp:[%s] / resIID:[%s] / Tag Key:[%s] / Tag Value:[%s]", resType, resIID, tag.Key, tag.Value) + + if resIID.SystemId == "" || tag.Key == "" { + msg := "tag will not be add because resIID.SystemId or tag.Key is not provided" + cblogger.Error(msg) + return irs.KeyValue{}, fmt.Errorf("%s", msg) + } + if resType == irs.ALL { + msg := "addTag is not allow to return for all rstype" + cblogger.Error(msg) + return irs.KeyValue{}, fmt.Errorf("%s", msg) + } + + hiscallInfo := GetCallLogScheme(t.Region, call.TAG, resIID.SystemId, "AddTag()") + start := call.Start() + + isValid, err := validateResource(t, resType, resIID) + if err != nil { + msg := "error while validate resource: " + err.Error() + cblogger.Error(msg) + return irs.KeyValue{}, err + } + if !isValid { + msg := fmt.Sprintf("resource is not exist, Region: %s, ResourceIID: %s\n", t.Region.Region, resIID.SystemId) + cblogger.Error(msg) + return irs.KeyValue{}, fmt.Errorf("%s", msg) + } + + // create Tag + createTagReq := taglib.NewCreateTagRequest() + createTagReq.TagKey = common.StringPtr(tag.Key) + createTagReq.TagValue = common.StringPtr(tag.Value) + + createTagRes, err := t.TagClient.CreateTag(createTagReq) + if err != nil && err.(*errors.TencentCloudSDKError).GetCode() != taglib.RESOURCEINUSE_TAGDUPLICATE { + msg := "createTag error has returned: " + err.Error() + cblogger.Error(msg) + return irs.KeyValue{}, err + } + + if cblogger.Level.String() == "debug" { + cblogger.Infof("createTagRes: %s\n", createTagRes.ToJsonString()) + } + + serviceTypeStr, resourcePrefixStr := rsTypeToTencentTypeMapParse(rsTypeToTencentTypeMap[resType]) + + // attach Tag + attachTagReq := taglib.NewAttachResourcesTagRequest() + attachTagReq.ServiceType = common.StringPtr(serviceTypeStr) + attachTagReq.ResourceRegion = common.StringPtr(t.Region.Region) + attachTagReq.ResourcePrefix = common.StringPtr(resourcePrefixStr) + attachTagReq.ResourceIds = common.StringPtrs([]string{resIID.SystemId}) + attachTagReq.TagKey = common.StringPtr(tag.Key) + attachTagReq.TagValue = common.StringPtr(tag.Value) + + attachTagRes, err := t.TagClient.AttachResourcesTag(attachTagReq) + if err != nil { + msg := "attachTag error has returned: " + err.Error() + cblogger.Error(msg) + return irs.KeyValue{}, err + } + + if cblogger.Level.String() == "debug" { + cblogger.Infof("attachTagRes: %s\n", attachTagRes.ToJsonString()) + } + + LoggingInfo(hiscallInfo, start) + + return tag, nil +} + +func (t *TencentTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]irs.KeyValue, error) { + cblogger.Debugf("Req resTyp:[%s] / resIID:[%s]", resType, resIID) + if resIID.SystemId == "" { + msg := "resIID.SystemId is not provided" + cblogger.Error(msg) + return nil, fmt.Errorf("%s", msg) + } + if resType == irs.ALL { + msg := "listTag is not allow to return for all rstype" + cblogger.Error(msg) + return nil, fmt.Errorf("%s", msg) + } + + serviceTypeStr, resourcePrefixStr := rsTypeToTencentTypeMapParse(rsTypeToTencentTypeMap[resType]) + + hiscallInfo := GetCallLogScheme(t.Region, call.TAG, resIID.SystemId, "ListTag()") + start := call.Start() + + req := taglib.NewDescribeResourceTagsRequest() + req.ResourceRegion = common.StringPtr(t.Region.Region) + req.ServiceType = common.StringPtr(serviceTypeStr) + req.ResourcePrefix = common.StringPtr(resourcePrefixStr) + req.ResourceId = common.StringPtr(resIID.SystemId) + + res, err := t.TagClient.DescribeResourceTags(req) + if err != nil { + return nil, err + } + + LoggingInfo(hiscallInfo, start) + + var tagList []irs.KeyValue + for _, tag := range res.Response.Rows { + tagList = append(tagList, irs.KeyValue{ + Key: *tag.TagKey, + Value: *tag.TagValue, + }) + } + + return tagList, nil +} + +func (t *TencentTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (irs.KeyValue, error) { + cblogger.Debugf("Req resTyp:[%s] / resIID:[%s] / key:[%s]", resType, resIID, key) + if resIID.SystemId == "" || key == "" { + msg := "resIID.SystemId or key is not provided" + cblogger.Error(msg) + return irs.KeyValue{}, fmt.Errorf("%s", msg) + } + if resType == irs.ALL { + msg := "getTag is not allow to return for all rstype" + cblogger.Error(msg) + return irs.KeyValue{}, fmt.Errorf("%s", msg) + } + + serviceTypeStr, resourcePrefixStr := rsTypeToTencentTypeMapParse(rsTypeToTencentTypeMap[resType]) + + hiscallInfo := GetCallLogScheme(t.Region, call.TAG, resIID.SystemId, "GetTag()") + start := call.Start() + + req := taglib.NewDescribeResourceTagsByTagKeysRequest() + req.ServiceType = common.StringPtr(serviceTypeStr) + req.ResourcePrefix = common.StringPtr(resourcePrefixStr) + req.ResourceRegion = common.StringPtr(t.Region.Region) + req.ResourceIds = common.StringPtrs([]string{resIID.SystemId}) + req.TagKeys = common.StringPtrs([]string{key}) + req.Limit = common.Uint64Ptr(1) + + res, err := t.TagClient.DescribeResourceTagsByTagKeys(req) + if err != nil { + return irs.KeyValue{}, err + } + + LoggingInfo(hiscallInfo, start) + + if len(res.Response.Rows) == 0 { + msg := "tag with key " + key + " not found" + cblogger.Error(msg) + return irs.KeyValue{}, fmt.Errorf("%s", msg) + } + + resTag := irs.KeyValue{Key: key, Value: *res.Response.Rows[0].TagKeyValues[0].TagValue} + + return resTag, nil +} + +func (t *TencentTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string) (bool, error) { + cblogger.Debugf("Req resTyp:[%s] / resIID:[%s] / key:[%s]", resType, resIID, key) + if resIID.SystemId == "" || key == "" { + msg := "resIID.SystemId or key is not provided" + cblogger.Error(msg) + return false, fmt.Errorf("%s", msg) + } + if resType == irs.ALL { + msg := "getTag is not allow to return for all rstype" + cblogger.Error(msg) + return false, fmt.Errorf("%s", msg) + } + + serviceTypeStr, resourcePrefixStr := rsTypeToTencentTypeMapParse(rsTypeToTencentTypeMap[resType]) + + hiscallInfo := GetCallLogScheme(t.Region, call.TAG, resIID.SystemId, "GetTag()") + start := call.Start() + + req := taglib.NewDetachResourcesTagRequest() + + req.ServiceType = common.StringPtr(serviceTypeStr) + req.ResourcePrefix = common.StringPtr(resourcePrefixStr) + req.ResourceRegion = common.StringPtr(t.Region.Region) + req.ResourceIds = common.StringPtrs([]string{resIID.SystemId}) + req.TagKey = common.StringPtr(key) + + res, err := t.TagClient.DetachResourcesTag(req) + if err != nil { + return false, err + } + + LoggingInfo(hiscallInfo, start) + + if cblogger.Level.String() == "debug" { + cblogger.Infof("%s", res.ToJsonString()) + } + + return true, nil +} + +func (t *TencentTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + cblogger.Debugf("Req resTyp:[%s] / keyword:[%s]", resType, keyword) + if resType == "" { + msg := "resType is not provided" + cblogger.Error(msg) + return nil, fmt.Errorf("%s", msg) + } + + req := taglib.NewDescribeResourceTagsRequest() + if resType != irs.ALL { + serviceTypeStr, resourcePrefixStr := rsTypeToTencentTypeMapParse(rsTypeToTencentTypeMap[resType]) + req.ServiceType = common.StringPtr(serviceTypeStr) + req.ResourcePrefix = common.StringPtr(resourcePrefixStr) + } + req.ResourceRegion = common.StringPtr(t.Region.Region) + req.Limit = common.Uint64Ptr(1) + + initRes, err := t.TagClient.DescribeResourceTags(req) + if err != nil { + return nil, err + } + + req.Limit = common.Uint64Ptr(*initRes.Response.TotalCount) + + res, err := t.TagClient.DescribeResourceTags(req) + if err != nil { + return nil, err + } + + tagInfoResIdArr := map[irs.RSType][]string{} + for _, tag := range res.Response.Rows { + if strings.Contains(*tag.TagKey, keyword) || strings.Contains(*tag.TagValue, keyword) || keyword == "" || keyword == "*" { + resourceIdSplitArr := strings.Split(*tag.ResourceId, "-") + tagInfoResIdArr[resourceIdStartStrToRsType[resourceIdSplitArr[0]]] = append(tagInfoResIdArr[resourceIdStartStrToRsType[resourceIdSplitArr[0]]], *tag.ResourceId) + } + } + + var resultTagInfos []*irs.TagInfo + for rsType, idArr := range tagInfoResIdArr { + uniqueIds := uniqueStringSlice(idArr) + if rsType == "" { + continue + } + + for _, id := range uniqueIds { + tagInfo := &irs.TagInfo{ + ResType: rsType, + ResIId: irs.IID{SystemId: id}, + } + resultTagInfos = append(resultTagInfos, tagInfo) + } + + request := taglib.NewDescribeResourceTagsByResourceIdsRequest() + + serviceTypeStr, resourcePrefixStr := rsTypeToTencentTypeMapParse(rsTypeToTencentTypeMap[rsType]) + request.ServiceType = common.StringPtr(serviceTypeStr) + request.ResourcePrefix = common.StringPtr(resourcePrefixStr) + request.ResourceIds = common.StringPtrs(uniqueIds) + request.ResourceRegion = common.StringPtr(t.Region.Region) + + response, err := t.TagClient.DescribeResourceTagsByResourceIds(request) + if err != nil { + return nil, err + } + + for _, tag := range response.Response.Tags { + for idx, tagInfos := range resultTagInfos { + if *tag.ResourceId == tagInfos.ResIId.SystemId { + kv := irs.KeyValue{ + Key: *tag.TagKey, + Value: *tag.TagValue, + } + resultTagInfos[idx].TagList = append(resultTagInfos[idx].TagList, kv) + break + } + } + } + + } + + return resultTagInfos, nil +} diff --git a/go.mod b/go.mod index fc7842f31..633b59e51 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/swaggo/echo-swagger v1.1.0 github.com/swaggo/swag v1.7.0 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.415 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.493 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.964 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm v1.0.493 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc v1.0.206 github.com/uber/jaeger-client-go v2.30.0+incompatible @@ -97,6 +97,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.964 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect diff --git a/go.sum b/go.sum index 589126304..d08ee0de5 100644 --- a/go.sum +++ b/go.sum @@ -711,8 +711,12 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.415/go.mod github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.492/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.493 h1:iROpofiufr+AiHdF5Vl9FlD1MNo11T5qe+s4q0pLTVo= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.493/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.964 h1:ET3EulYQvWrdD5FNwOP+196w5Vbniy/uRGucM5ILExQ= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.964/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm v1.0.493 h1:AF2MJgVPvMYNCQSQ/UsiGgKokMZ2JQreYyjz/VKrNEI= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm v1.0.493/go.mod h1:4c9rz1avM3qvpv6IrrWqe+j6LefpE0reUUwIUomzFg4= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.964 h1:YxlaK/2zEyC6cL+1Cz0ZmN6lKghx46QgbdzEFddh9qM= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.964/go.mod h1:2aiGewiAyHVnXdFT/PwYV0CNQt2Fq0sXcPiwhNtzbbE= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc v1.0.206 h1:O2leTYORlZlpVLHXUA6p1hqupEwaHtkHHvA7MGtDiUA= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc v1.0.206/go.mod h1:SKgeSsIfPEM6BeoIFiGHsWG9UsEXzkK0SkWx51H/OS8= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= From 001fd00926b7f5743ce20dea21afcabe3f330de5 Mon Sep 17 00:00:00 2001 From: raccoon-pi Date: Mon, 22 Jul 2024 05:31:35 +0000 Subject: [PATCH 25/56] https://github.com/cloud-barista/cb-spider/issues/1247 --- .../cloud-driver/drivers/tencent/resources/TagHandler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/TagHandler.go index 394a2d1ab..39b7c977e 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/TagHandler.go @@ -468,6 +468,7 @@ func (t *TencentTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs. response, err := t.TagClient.DescribeResourceTagsByResourceIds(request) if err != nil { + err := fmt.Errorf("An DescribeResourceTagsByResourceIds error has returned: %s", err.Error()) return nil, err } From 36941623f59b18a71b896958acc8ce1b4cc4d63a Mon Sep 17 00:00:00 2001 From: raccoon-pi Date: Mon, 22 Jul 2024 06:35:17 +0000 Subject: [PATCH 26/56] add Cluster, Disk, Keypair, Myimage, NLB, VM, VPC add tag while Create resource https://github.com/cloud-barista/cb-spider/issues/1247 --- .../tencent/resources/ClusterHandler.go | 13 ++++++++++ .../drivers/tencent/resources/DiskHandler.go | 10 ++++++++ .../tencent/resources/KeyPairHandler.go | 16 ++++++++++++ .../tencent/resources/MyImageHandler.go | 25 +++++++++++++------ .../drivers/tencent/resources/NLBHandler.go | 10 ++++++++ .../drivers/tencent/resources/VMHandler.go | 16 ++++++++++++ .../drivers/tencent/resources/VPCHandler.go | 11 ++++++++ 7 files changed, 94 insertions(+), 7 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/ClusterHandler.go index b59a36f65..f4bd0935c 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/ClusterHandler.go @@ -793,11 +793,24 @@ func getCreateClusterRequest(clusterHandler *TencentClusterHandler, clusterInfo desc_str := `#CB-SPIDER:PMKS:SECURITYGROUP:ID:%s #CB-SPIDER:PMKS:SUBNET:ID:%s` desc_str = fmt.Sprintf(desc_str, clusterInfo.Network.SecurityGroupIIDs[0].SystemId, clusterInfo.Network.SubnetIIDs[0].SystemId) + var tags []*tke.Tag + for _, inputTag := range clusterInfo.TagList { + tag := &tke.Tag{ + Key: &inputTag.Key, + Value: &inputTag.Value, + } + tags = append(tags, tag) + } + request.ClusterBasicSettings = &tke.ClusterBasicSettings{ ClusterName: common.StringPtr(clusterInfo.IId.NameId), VpcId: common.StringPtr(clusterInfo.Network.VpcIID.SystemId), ClusterVersion: common.StringPtr(clusterInfo.Version), // option, version: 1.22.5 ClusterDescription: common.StringPtr(desc_str), // option, #CB-SPIDER:PMKS:SECURITYGROUP:sg-c00t00ih + TagSpecification: []*tke.TagSpecification{{ + ResourceType: common.StringPtr("cluster"), + Tags: tags, + }}, } request.ClusterType = common.StringPtr("MANAGED_CLUSTER") //default value request.ClusterAdvancedSettings = &tke.ClusterAdvancedSettings{ diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/DiskHandler.go index 4e0c20b8f..56d51a466 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/DiskHandler.go @@ -52,6 +52,16 @@ func (DiskHandler *TencentDiskHandler) CreateDisk(diskReqInfo irs.DiskInfo) (irs request.Placement = &cbs.Placement{Zone: common.StringPtr(zone)} request.DiskChargeType = common.StringPtr("POSTPAID_BY_HOUR") + var tags []*cbs.Tag + for _, inputTag := range diskReqInfo.TagList { + tag := &cbs.Tag{ + Key: &inputTag.Key, + Value: &inputTag.Value, + } + tags = append(tags, tag) + } + request.Tags = tags + diskErr := validateDisk(&diskReqInfo) if diskErr != nil { cblogger.Error(diskErr) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/KeyPairHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/KeyPairHandler.go index e9a14c29c..94e8542b2 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/KeyPairHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/KeyPairHandler.go @@ -163,6 +163,22 @@ func (keyPairHandler *TencentKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPai request.KeyName = common.StringPtr(keyPairReqInfo.IId.NameId) request.ProjectId = common.Int64Ptr(0) + var tags []*cvm.Tag + for _, inputTag := range keyPairReqInfo.TagList { + tag := &cvm.Tag{ + Key: &inputTag.Key, + Value: &inputTag.Value, + } + tags = append(tags, tag) + } + + request.TagSpecification = []*cvm.TagSpecification{ + { + ResourceType: common.StringPtr("instance"), + Tags: tags, + }, + } + callLogStart := call.Start() response, err := keyPairHandler.Client.CreateKeyPair(request) callLogInfo.ElapsedTime = call.Elapsed(callLogStart) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/MyImageHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/MyImageHandler.go index a68710473..01665cf5a 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/MyImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/MyImageHandler.go @@ -89,16 +89,27 @@ func (myImageHandler *TencentMyImageHandler) SnapshotVM(snapshotReqInfo irs.MyIm request.InstanceId = common.StringPtr(snapshotReqInfo.SourceVM.SystemId) // Tag 추가 ResourceType : instance(for CVM), host(for CDH), image(for image), keypair(for key) + + var tags []*cvm.Tag + for _, inputTag := range snapshotReqInfo.TagList { + tag := &cvm.Tag{ + Key: &inputTag.Key, + Value: &inputTag.Value, + } + tags = append(tags, tag) + } + + imageTagSourceVm := &cvm.Tag{ + Key: common.StringPtr(IMAGE_TAG_SOURCE_VM), + Value: common.StringPtr(snapshotReqInfo.SourceVM.SystemId), + } + + tags = append(tags, imageTagSourceVm) + request.TagSpecification = []*cvm.TagSpecification{ { ResourceType: common.StringPtr(RESOURCE_TYPE_MYIMAGE), - Tags: []*cvm.Tag{ - { - - Key: common.StringPtr(IMAGE_TAG_SOURCE_VM), - Value: common.StringPtr(snapshotReqInfo.SourceVM.SystemId), - }, - }, + Tags: tags, }, } diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/NLBHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/NLBHandler.go index 4f1e89c44..53c70d87e 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/NLBHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/NLBHandler.go @@ -104,6 +104,16 @@ func (NLBHandler *TencentNLBHandler) CreateNLB(nlbReqInfo irs.NLBInfo) (irs.NLBI nlbRequest.VpcId = common.StringPtr(nlbReqInfo.VpcIID.SystemId) + var tags []*clb.TagInfo + for _, inputTag := range nlbReqInfo.TagList { + tag := &clb.TagInfo{ + TagKey: &inputTag.Key, + TagValue: &inputTag.Value, + } + tags = append(tags, tag) + } + nlbRequest.Tags = tags + nlbResponse, nlbErr := NLBHandler.Client.CreateLoadBalancer(nlbRequest) if nlbErr != nil { return irs.NLBInfo{}, nlbErr diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go index 410463402..3cb9e2aed 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go @@ -178,6 +178,22 @@ func (vmHandler *TencentVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, request.InstanceName = common.StringPtr(vmReqInfo.IId.NameId) + var tags []*cvm.Tag + for _, inputTag := range vmReqInfo.TagList { + tag := &cvm.Tag{ + Key: &inputTag.Key, + Value: &inputTag.Value, + } + tags = append(tags, tag) + } + + request.TagSpecification = []*cvm.TagSpecification{ + { + ResourceType: common.StringPtr("instance"), + Tags: tags, + }, + } + // windows의 경우 keyPair set 하면 오류. password setting 되어있는지 확인 if isWindow { //user := vmReqInfo.VMUserId // administrator diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go index c28147159..ce126ab52 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go @@ -67,6 +67,17 @@ func (VPCHandler *TencentVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.V request.VpcName = common.StringPtr(vpcReqInfo.IId.NameId) request.CidrBlock = common.StringPtr(vpcReqInfo.IPv4_CIDR) + var tags []*vpc.Tag + for _, inputTag := range vpcReqInfo.TagList { + tag := &vpc.Tag{ + Key: &inputTag.Key, + Value: &inputTag.Value, + } + tags = append(tags, tag) + } + + request.Tags = tags + callLogStart := call.Start() response, err := VPCHandler.Client.CreateVpc(request) callLogInfo.ElapsedTime = call.Elapsed(callLogStart) From db97bd0132b8de9a464fb66a1e5e51e9bb9618f0 Mon Sep 17 00:00:00 2001 From: raccoon-pi Date: Mon, 22 Jul 2024 06:50:28 +0000 Subject: [PATCH 27/56] SG add tag while Create Resouce --- .../tencent/connect/TencentCloudConnection.go | 2 +- .../tencent/resources/SecurityHandler.go | 41 ++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/connect/TencentCloudConnection.go b/cloud-control-manager/cloud-driver/drivers/tencent/connect/TencentCloudConnection.go index d9e5b7cc2..b76570a23 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/connect/TencentCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/connect/TencentCloudConnection.go @@ -97,7 +97,7 @@ func (cloudConn *TencentCloudConnection) CreateImageHandler() (irs.ImageHandler, func (cloudConn *TencentCloudConnection) CreateSecurityHandler() (irs.SecurityHandler, error) { cblogger.Info("Start") - handler := trs.TencentSecurityHandler{Region: cloudConn.Region, Client: cloudConn.SecurityClient} + handler := trs.TencentSecurityHandler{Region: cloudConn.Region, Client: cloudConn.SecurityClient, TagClient: cloudConn.TagClient} return &handler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/SecurityHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/SecurityHandler.go index 86ba5e4e4..441f05b89 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/SecurityHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/SecurityHandler.go @@ -15,16 +15,21 @@ import ( "errors" "strings" + taglib "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" + call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312" + + tencentError "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" ) type TencentSecurityHandler struct { - Region idrv.RegionInfo - Client *vpc.Client + Region idrv.RegionInfo + Client *vpc.Client + TagClient *taglib.Client } type RuleAction string @@ -150,6 +155,7 @@ func (securityHandler *TencentSecurityHandler) CreateSecurity(securityReqInfo ir } //cblogger.Debug(response) cblogger.Debug(response.ToJsonString()) + callogger.Info(call.String(callLogInfo)) securityInfo, errSecurity := securityHandler.GetSecurity(irs.IID{SystemId: *defaultEgressResponse.Response.SecurityGroup.SecurityGroupId}) @@ -159,6 +165,37 @@ func (securityHandler *TencentSecurityHandler) CreateSecurity(securityReqInfo ir } securityInfo.IId.NameId = securityReqInfo.IId.NameId + + callLogInfo.ElapsedTime = call.Elapsed(callLogStart) + + for _, tag := range securityReqInfo.TagList { + createTagReq := taglib.NewCreateTagRequest() + createTagReq.TagKey = common.StringPtr(tag.Key) + createTagReq.TagValue = common.StringPtr(tag.Value) + _, err := securityHandler.TagClient.CreateTag(createTagReq) + if err != nil && err.(*tencentError.TencentCloudSDKError).GetCode() != taglib.RESOURCEINUSE_TAGDUPLICATE { + msg := "createTag error has returned: " + err.Error() + " but, CreateSecurity is success.." + cblogger.Error(msg) + return securityInfo, err + } + + attachTagReq := taglib.NewAttachResourcesTagRequest() + attachTagReq.ServiceType = common.StringPtr("cvm") + attachTagReq.ResourcePrefix = common.StringPtr("sg") + attachTagReq.ResourceRegion = common.StringPtr(securityHandler.Region.Region) + attachTagReq.ResourceIds = common.StringPtrs([]string{*defaultEgressResponse.Response.SecurityGroup.SecurityGroupId}) + attachTagReq.TagKey = common.StringPtr(tag.Key) + attachTagReq.TagValue = common.StringPtr(tag.Value) + _, err = securityHandler.TagClient.AttachResourcesTag(attachTagReq) + if err != nil { + msg := "attachTag error has returned: " + err.Error() + " but, CreateSecurity is success.." + cblogger.Error(msg) + return securityInfo, err + } + } + + callLogInfo.ElapsedTime = call.Elapsed(callLogStart) + return securityInfo, nil } From 258178c4a5dd8bc053db26d1bccd8b573127264c Mon Sep 17 00:00:00 2001 From: raccoon-pi Date: Mon, 22 Jul 2024 07:37:27 +0000 Subject: [PATCH 28/56] tencent AddSubnet tags --- .../drivers/tencent/resources/VPCHandler.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go index ce126ab52..5041d2d76 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go @@ -452,6 +452,17 @@ func (VPCHandler *TencentVPCHandler) AddSubnet(vpcIID irs.IID, subnetInfo irs.Su request.CidrBlock = common.StringPtr(subnetInfo.IPv4_CIDR) request.Zone = common.StringPtr(zoneId) + var tags []*vpc.Tag + for _, inputTag := range subnetInfo.TagList { + tag := &vpc.Tag{ + Key: &inputTag.Key, + Value: &inputTag.Value, + } + tags = append(tags, tag) + } + + request.Tags = tags + callLogStart := call.Start() response, err := VPCHandler.Client.CreateSubnet(request) callLogInfo.ElapsedTime = call.Elapsed(callLogStart) From a905daa9813ca3769fd7ff0ed60ea02ed619b5bb Mon Sep 17 00:00:00 2001 From: raccoon-pi Date: Mon, 22 Jul 2024 09:03:28 +0000 Subject: [PATCH 29/56] tencentCloud ptr error fix and run instance Bug Fix --- .../tencent/resources/ClusterHandler.go | 9 ++++----- .../drivers/tencent/resources/DiskHandler.go | 9 ++++----- .../tencent/resources/KeyPairHandler.go | 9 ++++----- .../tencent/resources/MyImageHandler.go | 9 ++++----- .../drivers/tencent/resources/NLBHandler.go | 9 ++++----- .../drivers/tencent/resources/VMHandler.go | 13 ++++++------ .../drivers/tencent/resources/VPCHandler.go | 20 +++++++++---------- 7 files changed, 37 insertions(+), 41 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/ClusterHandler.go index f4bd0935c..5db6f9c09 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/ClusterHandler.go @@ -795,11 +795,10 @@ func getCreateClusterRequest(clusterHandler *TencentClusterHandler, clusterInfo var tags []*tke.Tag for _, inputTag := range clusterInfo.TagList { - tag := &tke.Tag{ - Key: &inputTag.Key, - Value: &inputTag.Value, - } - tags = append(tags, tag) + tags = append(tags, &tke.Tag{ + Key: common.StringPtr(inputTag.Key), + Value: common.StringPtr(inputTag.Value), + }) } request.ClusterBasicSettings = &tke.ClusterBasicSettings{ diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/DiskHandler.go index 56d51a466..d3279ab27 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/DiskHandler.go @@ -54,11 +54,10 @@ func (DiskHandler *TencentDiskHandler) CreateDisk(diskReqInfo irs.DiskInfo) (irs var tags []*cbs.Tag for _, inputTag := range diskReqInfo.TagList { - tag := &cbs.Tag{ - Key: &inputTag.Key, - Value: &inputTag.Value, - } - tags = append(tags, tag) + tags = append(tags, &cbs.Tag{ + Key: common.StringPtr(inputTag.Key), + Value: common.StringPtr(inputTag.Value), + }) } request.Tags = tags diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/KeyPairHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/KeyPairHandler.go index 94e8542b2..44afdddda 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/KeyPairHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/KeyPairHandler.go @@ -165,11 +165,10 @@ func (keyPairHandler *TencentKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPai var tags []*cvm.Tag for _, inputTag := range keyPairReqInfo.TagList { - tag := &cvm.Tag{ - Key: &inputTag.Key, - Value: &inputTag.Value, - } - tags = append(tags, tag) + tags = append(tags, &cvm.Tag{ + Key: common.StringPtr(inputTag.Key), + Value: common.StringPtr(inputTag.Value), + }) } request.TagSpecification = []*cvm.TagSpecification{ diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/MyImageHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/MyImageHandler.go index 01665cf5a..daa5134d8 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/MyImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/MyImageHandler.go @@ -92,11 +92,10 @@ func (myImageHandler *TencentMyImageHandler) SnapshotVM(snapshotReqInfo irs.MyIm var tags []*cvm.Tag for _, inputTag := range snapshotReqInfo.TagList { - tag := &cvm.Tag{ - Key: &inputTag.Key, - Value: &inputTag.Value, - } - tags = append(tags, tag) + tags = append(tags, &cvm.Tag{ + Key: common.StringPtr(inputTag.Key), + Value: common.StringPtr(inputTag.Value), + }) } imageTagSourceVm := &cvm.Tag{ diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/NLBHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/NLBHandler.go index 53c70d87e..268040c17 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/NLBHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/NLBHandler.go @@ -106,11 +106,10 @@ func (NLBHandler *TencentNLBHandler) CreateNLB(nlbReqInfo irs.NLBInfo) (irs.NLBI var tags []*clb.TagInfo for _, inputTag := range nlbReqInfo.TagList { - tag := &clb.TagInfo{ - TagKey: &inputTag.Key, - TagValue: &inputTag.Value, - } - tags = append(tags, tag) + tags = append(tags, &clb.TagInfo{ + TagKey: common.StringPtr(inputTag.Key), + TagValue: common.StringPtr(inputTag.Value), + }) } nlbRequest.Tags = tags diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go index 3cb9e2aed..57ab1765e 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go @@ -180,11 +180,10 @@ func (vmHandler *TencentVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, var tags []*cvm.Tag for _, inputTag := range vmReqInfo.TagList { - tag := &cvm.Tag{ - Key: &inputTag.Key, - Value: &inputTag.Value, - } - tags = append(tags, tag) + tags = append(tags, &cvm.Tag{ + Key: common.StringPtr(inputTag.Key), + Value: common.StringPtr(inputTag.Value), + }) } request.TagSpecification = []*cvm.TagSpecification{ @@ -354,7 +353,9 @@ func (vmHandler *TencentVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, cblogger.Info("The Image includes a DataDisk.") } } - request.DataDisks = dataDiskList + if len(dataDiskList) > 0 { + request.DataDisks = dataDiskList + } //============================= // UserData생성 처리(File기반) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go index 5041d2d76..18eafe197 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VPCHandler.go @@ -69,11 +69,10 @@ func (VPCHandler *TencentVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.V var tags []*vpc.Tag for _, inputTag := range vpcReqInfo.TagList { - tag := &vpc.Tag{ - Key: &inputTag.Key, - Value: &inputTag.Value, - } - tags = append(tags, tag) + tags = append(tags, &vpc.Tag{ + Key: common.StringPtr(inputTag.Key), + Value: common.StringPtr(inputTag.Value), + }) } request.Tags = tags @@ -118,6 +117,8 @@ func (VPCHandler *TencentVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.V requestSubnet.Subnets = append(requestSubnet.Subnets, reqSubnet) } + requestSubnet.Tags = tags + responseSubnet, errSubnet := VPCHandler.Client.CreateSubnets(requestSubnet) cblogger.Debug(responseSubnet.ToJsonString()) //cblogger.Debug(responseSubnet) @@ -454,11 +455,10 @@ func (VPCHandler *TencentVPCHandler) AddSubnet(vpcIID irs.IID, subnetInfo irs.Su var tags []*vpc.Tag for _, inputTag := range subnetInfo.TagList { - tag := &vpc.Tag{ - Key: &inputTag.Key, - Value: &inputTag.Value, - } - tags = append(tags, tag) + tags = append(tags, &vpc.Tag{ + Key: common.StringPtr(inputTag.Key), + Value: common.StringPtr(inputTag.Value), + }) } request.Tags = tags From d785886d090c547695498a3a2aaf635895d27121 Mon Sep 17 00:00:00 2001 From: dev4unet Date: Mon, 22 Jul 2024 10:38:36 +0000 Subject: [PATCH 30/56] Complete NLB Tag Test & Add NLB Search to FindTag --- .../drivers/aws/connect/AwsCloudConnection.go | 4 +- .../drivers/aws/main/Test_Resources.go | 50 ++--- .../drivers/aws/resources/TagHandler.go | 173 ++++++++++++++++-- 3 files changed, 191 insertions(+), 36 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go b/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go index 4530a7323..424000f90 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go @@ -115,12 +115,12 @@ func (cloudConn *AwsCloudConnection) CreateSecurityHandler() (irs.SecurityHandle } func (cloudConn *AwsCloudConnection) CreateTagHandler() (irs.TagHandler, error) { - handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient} + handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient, NLBClient: cloudConn.NLBClient} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateAwsTagHandler() ars.AwsTagHandler { - handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient} + handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient, NLBClient: cloudConn.NLBClient} return handler } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go index 5c22326e9..0e0f8a51a 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go @@ -50,9 +50,9 @@ func handleSecurity() { //config := readConfigFile() //VmID := config.Aws.VmID - securityName := "CB-SecurityAddTest1" + securityName := "CB-SecurityTagTest" securityId := "sg-0d6a2bb960481ce68" - vpcId := "vpc-c0479cab" + vpcId := "vpc-0a115f43d4fcbab36" //New-CB-VPC for { fmt.Println("Security Management") @@ -78,7 +78,7 @@ func handleSecurity() { case 1: result, err := handler.ListSecurity() if err != nil { - cblogger.Infof(" Security List Lookup Failed : ", err) + cblogger.Info(" Security List Lookup Failed : ", err) } else { cblogger.Info("Security List Lookup Result") cblogger.Info(result) @@ -94,6 +94,8 @@ func handleSecurity() { securityReqInfo := irs.SecurityReqInfo{ IId: irs.IID{NameId: securityName}, VpcIID: irs.IID{SystemId: vpcId}, + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: securityName+"123"}}, + TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}}, SecurityRules: &[]irs.SecurityRuleInfo{ //보안 정책 설정 //CIDR 테스트 { @@ -193,7 +195,7 @@ func handleSecurity() { if err != nil { cblogger.Infof(securityId, " Security Delete Failed : ", err) } else { - cblogger.Infof("[%s] Security Delete Result : [%s]", securityId, result) + cblogger.Infof("[%s] Security Delete Result : [%t]", securityId, result) } case 5: @@ -466,9 +468,7 @@ func handleKeyPair() { //config := readConfigFile() //VmID := config.Aws.VmID - keyPairName := "CB-KeyPairTest123123" - //keyPairName := "key-0a58c9a7b0a07a2d2" - + keyPairName := "CB-KeyPairTagTest" //keyPairName := config.Aws.KeyName for { @@ -505,7 +505,7 @@ func handleKeyPair() { keyPairReqInfo := irs.KeyPairReqInfo{ IId: irs.IID{NameId: keyPairName}, //Name: keyPairName, - //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: keyPairName+"123"}}, + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: keyPairName + "123"}}, TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}}, } result, err := KeyPairHandler.CreateKey(keyPairReqInfo) @@ -990,17 +990,19 @@ func handleVM() { case 1: vmReqInfo := irs.VMReqInfo{ - IId: irs.IID{NameId: "mcloud-barista-windows-test"}, + IId: irs.IID{NameId: "mcloud-barista-tag-test"}, + TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: "mcloud-barista-tag-test123"}}, + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}}, //ImageIID: irs.IID{SystemId: "ami-001b6f8703b50e077"}, //centos-stable-7.2003.13-ebs-202005201235 - //ImageIID: irs.IID{SystemId: "ami-059b6d3840b03d6dd"}, //Ubuntu Server 20.04 LTS (HVM) + ImageIID: irs.IID{SystemId: "ami-056a29f2eddc40520"}, //Ubuntu Server 22.04 LTS (HVM), SSD Volume Type //ImageIID: irs.IID{SystemId: "ami-09e67e426f25ce0d7"}, //Ubuntu Server 20.04 LTS (HVM) - 버지니아 북부 리전 //ImageIID: irs.IID{SystemId: "ami-059b6d3840b03d6dd"}, //Ubuntu Server 20.04 LTS (HVM) //ImageIID: irs.IID{SystemId: "ami-0fe22bffdec36361c"}, //Ubuntu Server 18.04 LTS (HVM) - Japan 리전 - ImageIID: irs.IID{SystemId: "ami-093f427eb324bb754"}, //Microsoft Windows Server 2012 R2 RTM 64-bit Locale English AMI provided by Amazon - Japan 리전 - SubnetIID: irs.IID{SystemId: "subnet-0a6ca346752be1ca4"}, - SecurityGroupIIDs: []irs.IID{{SystemId: "sg-0f4532a525ad09de1"}}, //3389 RDP 포트 Open + //ImageIID: irs.IID{SystemId: "ami-093f427eb324bb754"}, //Microsoft Windows Server 2012 R2 RTM 64-bit Locale English AMI provided by Amazon - Japan 리전 + SubnetIID: irs.IID{SystemId: "subnet-02127b9d8c84f7440"}, + SecurityGroupIIDs: []irs.IID{{SystemId: "sg-0209d9dc23ebd4cdd"}}, //3389 RDP 포트 Open VMSpecName: "t2.micro", - KeyPairIID: irs.IID{SystemId: "japan-test"}, + KeyPairIID: irs.IID{SystemId: "CB-KeyPairTagTest"}, VMUserPasswd: "1234qwer!@#$", //윈도우즈용 비밀번호 RootDiskType: "standard", //gp2/standard/io1/io2/sc1/st1/gp3 @@ -1236,10 +1238,13 @@ func handleNLB() { nlbReqInfo := irs.NLBInfo{ IId: irs.IID{NameId: "cb-nlb-test01"}, - VpcIID: irs.IID{SystemId: "vpc-0c4d36a3ac3924419"}, + VpcIID: irs.IID{SystemId: "vpc-0a115f43d4fcbab36"}, Type: "PUBLIC", Scope: "REGION", + TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: "cb-nlb-test01123"}}, + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}}, + Listener: irs.ListenerInfo{ Protocol: "TCP", // AWS NLB : TCP, TLS, UDP, or TCP_UDP //IP: "", @@ -1249,7 +1254,7 @@ func handleNLB() { VMGroup: irs.VMGroupInfo{ Protocol: "TCP", //TCP|UDP|HTTP|HTTPS Port: "22", //1-65535 - VMs: &[]irs.IID{irs.IID{SystemId: "i-0dcbcbeadbb14212f"}, irs.IID{SystemId: "i-0cba8efe123ab0b42"}, irs.IID{SystemId: "i-010c858cbe5b6fe93"}}, + VMs: &[]irs.IID{irs.IID{SystemId: "i-0c65033e158e0fd99"}}, }, HealthChecker: irs.HealthCheckerInfo{ @@ -1803,8 +1808,10 @@ func handleTag() { reqTag := irs.KeyValue{Key: "tag3", Value: "tag3 test"} reqKey := "tag3" + reqKey = "CB-KeyPairTagTest" + reqKey = "cb-nlb-test01123" reqKey = "" - //reqType = irs.ALL + reqType = irs.ALL for { fmt.Println("TagHandler Management") @@ -2069,12 +2076,13 @@ func readConfigFile() Config { } func main() { - handleKeyPair() handleTag() - //handleVPC() // handlePublicIP() // PublicIP 생성 후 conf - handleSecurity() - handleVM() + + //handleKeyPair() + //handleVPC() + //handleSecurity() + //handleVM() // handleImage() //AMI // handleVNic() //Lancard // handleVMSpec() diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go index 23957e90a..76cdc47c2 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go @@ -24,14 +24,17 @@ import ( //"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/elbv2" + call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" ) type AwsTagHandler struct { - Region idrv.RegionInfo - Client *ec2.EC2 + Region idrv.RegionInfo + Client *ec2.EC2 + NLBClient *elbv2.ELBV2 } // Map of RSType to AWS resource types @@ -103,6 +106,88 @@ func (tagHandler *AwsTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag return tag, nil } +func (tagHandler *AwsTagHandler) GetAllNLBTags() ([]*irs.TagInfo, error) { + // Step 1: List all load balancers and store their ARNs and Names + lbArnToName := make(map[string]string) + + err := tagHandler.NLBClient.DescribeLoadBalancersPages(&elbv2.DescribeLoadBalancersInput{}, func(page *elbv2.DescribeLoadBalancersOutput, lastPage bool) bool { + for _, lb := range page.LoadBalancers { + lbArnToName[*lb.LoadBalancerArn] = *lb.LoadBalancerName + } + return !lastPage + }) + + if err != nil { + return nil, fmt.Errorf("failed to describe load balancers: %w", err) + } + + if len(lbArnToName) == 0 { + return nil, fmt.Errorf("no load balancers found") + } + + // Step 2: Describe tags for each load balancer using the GetNLBTags function + var allTagInfos []*irs.TagInfo + + for arn, name := range lbArnToName { + resIID := irs.IID{ + NameId: name, + SystemId: arn, + } + + tagInfos, err := tagHandler.GetNLBTags(resIID) + if err != nil { + return nil, fmt.Errorf("failed to get tags for load balancer %s: %w", arn, err) + } + + // Process only if Tag exists + if len(tagInfos) == 0 { + continue + } + + // Convert tags into TagInfo with the correct ResType + tagInfo := &irs.TagInfo{ + ResType: irs.NLB, + ResIId: resIID, + TagList: tagInfos, + } + + allTagInfos = append(allTagInfos, tagInfo) + } + + return allTagInfos, nil +} + +func (tagHandler *AwsTagHandler) GetNLBTags(resIID irs.IID) ([]irs.KeyValue, error) { + input := &elbv2.DescribeTagsInput{ + ResourceArns: []*string{ + aws.String(resIID.SystemId), + }, + } + + result, err := tagHandler.NLBClient.DescribeTags(input) + if err != nil { + return nil, fmt.Errorf("failed to describe load balancer tags: %w", err) + } + + if len(result.TagDescriptions) == 0 { + return nil, fmt.Errorf("no tags found for load balancer: %s", resIID.SystemId) + } + + if cblogger.Level.String() == "debug" { + cblogger.Debug(result) + } + + var retTagList []irs.KeyValue + for _, tag := range result.TagDescriptions[0].Tags { + retTagList = append(retTagList, irs.KeyValue{ + Key: aws.StringValue(tag.Key), + Value: aws.StringValue(tag.Value), + }) + } + + return retTagList, nil +} + func (tagHandler *AwsTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]irs.KeyValue, error) { cblogger.Debugf("Req resTyp:[%s] / resIID:[%s]", resType, resIID) @@ -112,6 +197,10 @@ func (tagHandler *AwsTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([] return nil, errors.New(msg) } + if resType == irs.NLB { + return tagHandler.GetNLBTags(resIID) + } + resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error input := &ec2.DescribeTagsInput{ Filters: []*ec2.Filter{ @@ -140,7 +229,7 @@ func (tagHandler *AwsTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([] LoggingInfo(hiscallInfo, start) if cblogger.Level.String() == "debug" { - cblogger.Info(result) + cblogger.Debug(result) } var retTagList []irs.KeyValue @@ -300,6 +389,48 @@ func (tagHandler *AwsTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, k return true, nil } +// Extracts a list of Key or Value tags corresponding to keyword from the tagInfos array. +func (tagHandler *AwsTagHandler) ExtractTagKeyValue(tagInfos []*irs.TagInfo, keyword string) []*irs.TagInfo { + var matchingTagInfos []*irs.TagInfo + cblogger.Debugf("tagInfos count : [%d] / keyword : [%s]", len(tagInfos), keyword) + if cblogger.Level.String() == "debug" { + spew.Dump(tagInfos) + } + + /* + for _, tagInfo := range tagInfos { + for _, kv := range tagInfo.TagList { + if kv.Key == keyword || kv.Value == keyword { + matchingTagInfos = append(matchingTagInfos, tagInfo) + break // If any match, add that tagInfo and move on to the next tagInfo + } + } + } + */ + + // The DescribeTags() API used by FindTag() only includes matching Keys, so we modified it with the same logic. + for _, tagInfo := range tagInfos { + var filteredTagList []irs.KeyValue + + for _, kv := range tagInfo.TagList { + if kv.Key == keyword || kv.Value == keyword { + filteredTagList = append(filteredTagList, kv) + } + } + + if len(filteredTagList) > 0 { + matchingTagInfo := &irs.TagInfo{ + ResType: tagInfo.ResType, + ResIId: tagInfo.ResIId, + TagList: filteredTagList, + KeyValueList: tagInfo.KeyValueList, + } + matchingTagInfos = append(matchingTagInfos, matchingTagInfo) + } + } + return matchingTagInfos +} + // Find tags by tag key or value // resType: ALL | VPC, SUBNET, etc.,. // keyword: The keyword to search for in the tag key or value. @@ -307,15 +438,22 @@ func (tagHandler *AwsTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, k func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { cblogger.Debugf("resType : [%s] / keyword : [%s]", resType, keyword) + var tagInfos []*irs.TagInfo var filters []*ec2.Filter - if resType == irs.KEY { - resIID := tagHandler.GetRealResourceId(resType, irs.IID{SystemId: keyword}) // fix some resource id error - keyword = resIID.SystemId - } - // Add resource type filter if resType is not ALL if resType != irs.ALL { + if resType == irs.NLB { + // Add a list of NLB Tags if this is a all search + if keyword == "" || keyword == "*" { + return tagHandler.GetAllNLBTags() + } else { + nlbTaginfos, _ := tagHandler.GetAllNLBTags() + //spew.Dump(nlbTaginfos) + return tagHandler.ExtractTagKeyValue(nlbTaginfos, keyword), nil + } + } + if awsResType, ok := rsTypeToAwsResourceTypeMap[resType]; ok { filters = append(filters, &ec2.Filter{ Name: aws.String("resource-type"), @@ -333,10 +471,10 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] // Function to process tags and add them to tagInfoMap processTags := func(result *ec2.DescribeTagsOutput) { if cblogger.Level.String() == "debug" { - cblogger.Debug(result) - //cblogger.Debug("=================================") - //spew.Dump(result) - //cblogger.Debug("=================================") + //cblogger.Debug(result) + cblogger.Debug("=================================") + spew.Dump(result) + cblogger.Debug("=================================") } for _, tag := range result.Tags { @@ -433,10 +571,19 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] processTags(result) } - var tagInfos []*irs.TagInfo + //var tagInfos []*irs.TagInfo for _, tagInfo := range tagInfoMap { tagInfos = append(tagInfos, tagInfo) } + // Add a list of NLB Tags if this is a all search + if resType == irs.ALL { + nlbTaginfos, _ := tagHandler.GetAllNLBTags() + if keyword != "" && keyword != "*" { + nlbTaginfos = tagHandler.ExtractTagKeyValue(nlbTaginfos, keyword) + } + tagInfos = append(tagInfos, nlbTaginfos...) + } + return tagInfos, nil } From cfa035b6e5cd42a38103bb17ea8065dff96f4c0c Mon Sep 17 00:00:00 2001 From: raccoon-pi Date: Tue, 23 Jul 2024 00:52:46 +0000 Subject: [PATCH 31/56] testResource language kor to en - tag --- .../drivers/tencent/main/Test_Resources.go | 253 +++++++++++++----- 1 file changed, 192 insertions(+), 61 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/tencent/main/Test_Resources.go index 9fb6dd12a..99d39c519 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/main/Test_Resources.go @@ -23,6 +23,17 @@ import ( var cblogger *logrus.Logger +var testReourceTags = []irs.KeyValue{ + { + Key: "allTestKey", + Value: "allTestValue", + }, + { + Key: "allTestKey2", + Value: "allTestValue2", + }, +} + func init() { // cblog is a global variable. cblogger = cblog.GetLogger("TencentCloud Resource Test") @@ -136,9 +147,10 @@ func handleSecurity() { } handler := ResourceHandler.(irs.SecurityHandler) - securityName := "sg20" + securityName := "New-CB-SG" + securityId := "sg-5m5pezaj" - vpcId := "vpc-f3teez1l" + // vpcId := "vpc-f3teez1l" for { fmt.Println("Security Management") @@ -177,8 +189,9 @@ func handleSecurity() { case 2: cblogger.Infof("[%s] Security 생성 테스트", securityName) securityReqInfo := irs.SecurityReqInfo{ - IId: irs.IID{NameId: securityName}, - VpcIID: irs.IID{SystemId: vpcId}, + IId: irs.IID{NameId: securityName}, + // VpcIID: irs.IID{SystemId: vpcId}, + VpcIID: irs.IID{SystemId: "vpc-nqjv5krw"}, SecurityRules: &[]irs.SecurityRuleInfo{ //보안 정책 설정 //CIDR 테스트 { @@ -260,6 +273,7 @@ func handleSecurity() { }, */ }, + TagList: testReourceTags, } result, err := handler.CreateSecurity(securityReqInfo) @@ -651,7 +665,8 @@ func handleKeyPair() { case 2: cblogger.Infof("[%s] 키 페어 생성 테스트", keyPairName) keyPairReqInfo := irs.KeyPairReqInfo{ - IId: irs.IID{NameId: keyPairName}, + IId: irs.IID{NameId: keyPairName}, + TagList: testReourceTags, } result, err := handler.CreateKey(keyPairReqInfo) if err != nil { @@ -697,10 +712,12 @@ func handleVPC() { subnetReqInfo := irs.SubnetInfo{ IId: irs.IID{NameId: "AddTest-Subnet"}, IPv4_CIDR: "10.0.3.0/24", + TagList: testReourceTags, } subnetReqVpcInfo := irs.IID{SystemId: "vpc-6wex2mrx1fovfecsl44mx"} reqSubnetId := irs.IID{SystemId: "vsw-6we4h4n4wp9xdtakrno15"} + cblogger.Debug(subnetReqInfo) cblogger.Debug(subnetReqVpcInfo) cblogger.Debug(reqSubnetId) @@ -712,13 +729,15 @@ func handleVPC() { { IId: irs.IID{NameId: "New-CB-Subnet"}, IPv4_CIDR: "10.0.1.0/24", + TagList: testReourceTags, }, - { IId: irs.IID{NameId: "New-CB-Subnet2"}, IPv4_CIDR: "10.0.2.0/24", + TagList: testReourceTags, }, }, + TagList: testReourceTags, //Id: "subnet-044a2b57145e5afc5", //Name: "CB-VNet-Subnet", // 웹 도구 등 외부에서 전달 받지 않고 드라이버 내부적으로 자동 구현때문에 사용하지 않음. //CidrBlock: "10.0.0.0/16", @@ -946,23 +965,24 @@ func handleVM() { case 1: vmReqInfo := irs.VMReqInfo{ - IId: irs.IID{NameId: "mcloud-barista-vm-test"}, + IId: irs.IID{NameId: "New-CB-VM"}, //IId: irs.IID{NameId: "bill-test"}, //ImageIID: irs.IID{SystemId: "img-22trbn9x"}, //Ubuntu Server 20.04 LTS 64 - ImageIID: irs.IID{SystemId: "img-9x5o844i"}, //Ubuntu Server 18.04.1 LTS 64 - VpcIID: irs.IID{SystemId: "vpc-g3imdykc"}, - SubnetIID: irs.IID{SystemId: "subnet-rlr71m6n"}, //Zone2 - SecurityGroupIIDs: []irs.IID{{SystemId: "sg-j43bvarj"}}, + ImageIID: irs.IID{SystemId: "img-j5e5hadz"}, //Ubuntu Server 18.04.1 LTS 64 + VpcIID: irs.IID{SystemId: "vpc-nqjv5krw"}, + SubnetIID: irs.IID{SystemId: "subnet-q6etxdzn"}, //subnet-q6etxdzn + SecurityGroupIIDs: []irs.IID{{SystemId: "sg-mezlp15n"}}, VMSpecName: "SA2.MEDIUM2", - KeyPairIID: irs.IID{SystemId: "skey-cp2013rp"}, //cb_user_test + KeyPairIID: irs.IID{SystemId: "skey-6zud5qb7"}, //cb_user_test //VMUserId: "root", //root만 가능 //VMUserPasswd: "Cbuser!@#", //대문자 소문자 모두 사용되어야 함. 그리고 숫자나 특수 기호 중 하나가 포함되어야 함. //RootDiskType: "CLOUD_PREMIUM", //LOCAL_BASIC/LOCAL_SSD/CLOUD_BASIC/CLOUD_SSD/CLOUD_PREMIUM - RootDiskType: "CLOUD_PREMIUM", //LOCAL_BASIC/LOCAL_SSD/CLOUD_BASIC/CLOUD_SSD/CLOUD_PREMIUM - RootDiskSize: "60", //Image Size 보다 작으면 에러 남 + RootDiskType: "CLOUD_SSD", //LOCAL_BASIC/LOCAL_SSD/CLOUD_BASIC/CLOUD_SSD/CLOUD_PREMIUM + RootDiskSize: "60", //Image Size 보다 작으면 에러 남 //RootDiskSize: "Default", //Image Size 보다 작으면 에러 남 - //DataDiskIIDs: []irs.IID{{SystemId: "disk-obk07o6e"}}, + // DataDiskIIDs: []irs.IID{{SystemId: "disk-d0wn492r"}}, + TagList: testReourceTags, } vmInfo, err := vmHandler.StartVM(vmReqInfo) @@ -1084,15 +1104,16 @@ func handleNLB() { nlbReqInfo := irs.NLBInfo{ IId: irs.IID{NameId: "New-CB-NLB03"}, - VpcIID: irs.IID{SystemId: "vpc-i614yona"}, + VpcIID: irs.IID{SystemId: "vpc-nqjv5krw"}, Type: "PUBLIC", Listener: irs.ListenerInfo{Protocol: "TCP", Port: "80"}, HealthChecker: irs.HealthCheckerInfo{Port: "1234"}, VMGroup: irs.VMGroupInfo{ Protocol: "TCP", Port: "80", - VMs: &[]irs.IID{{SystemId: "ins-5tf50w2x"}, {SystemId: "ins-lqds5b1h"}}, + VMs: &[]irs.IID{{SystemId: "ins-grur8hw9"}, {SystemId: "ins-lqds5b1h"}}, }, + TagList: testReourceTags, } reqNLBId := irs.IID{SystemId: "lb-qfipv1il"} @@ -1245,6 +1266,7 @@ func handleDisk() { IId: irs.IID{NameId: "cb-disk-01"}, DiskType: "CLOUD_PREMIUM", DiskSize: "20", + TagList: testReourceTags, } for { @@ -1521,10 +1543,24 @@ func handleTag() { var reqType irs.RSType = irs.VM reqIID := irs.IID{SystemId: "ins-grur8hw9"} - reqTag := irs.KeyValue{Key: "testTagKey", Value: "testTagValue"} + // reqTag := irs.KeyValue{Key: "testTagKey", Value: "testTagValue"} + addreqTag := irs.KeyValue{Key: "tagkey5", Value: "tagvalue5"} reqKey := "testTagKey" - reqKey = "" - reqType = irs.ALL + reqKey = "tagkey5" + // reqType = irs.ALL + + // tags := []irs.KeyValue{ + // { + // Key: "allTestKey", + // Value: "allTestValue", + // }, + // { + // Key: "allTestKey2", + // Value: "allTestValue2", + // }, + // } + + // vpcSystemId := "" for { fmt.Println("TagHandler Management") @@ -1534,6 +1570,9 @@ func handleTag() { fmt.Println("3. Tag Get") fmt.Println("4. Tag Delete") fmt.Println("5. Tag Find") + // fmt.Println("6. ALL Create And Tagging") + // fmt.Println("7. ALL Created Resource Delete") + // fmt.Println("8. print ALL Created Resource SystemId") var commandNum int inputCnt, err := fmt.Scan(&commandNum) @@ -1547,21 +1586,19 @@ func handleTag() { return case 1: - reqIID = irs.IID{SystemId: "ins-grur8hw9"} - reqType = irs.VM - cblogger.Infof("조회 요청 태그 타입 : [%s]", reqType) + cblogger.Infof("Inquiry Request Tag Type : [%s]", reqType) if reqType == irs.VM { - cblogger.Debug("VM 요청됨") + cblogger.Debug("VM Inquiry") } result, err := handler.ListTag(reqType, reqIID) if err != nil { cblogger.Info(" Tag 목록 조회 실패 : ", err) } else { - cblogger.Info("Tag 목록 조회 결과") + cblogger.Info("Tag list lookup results") cblogger.Debug(result) - cblogger.Infof("로그 레벨 : [%s]", cblog.GetLevel()) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of output results : ", len(result)) //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. if result != nil { @@ -1570,58 +1607,152 @@ func handleTag() { } case 2: - reqIID = irs.IID{SystemId: "ins-grur8hw9"} - reqTag = irs.KeyValue{Key: "tagkey5", Value: "tagvalue5"} - reqType = irs.VM - cblogger.Infof("[%s] Tag 추가 테스트", reqIID.SystemId) - result, err := handler.AddTag(reqType, reqIID, reqTag) + cblogger.Infof("[%s][%+v] Tag Add Test", reqIID.SystemId, addreqTag) + result, err := handler.AddTag(reqType, reqIID, addreqTag) if err != nil { - cblogger.Infof(reqIID.SystemId, " Tag 생성 실패 : ", err) + cblogger.Infof(reqIID.SystemId, " Tag Add failed : ", err) } else { - cblogger.Info("Tag 생성 결과 : ", result) + cblogger.Info("Tag Add results : ", result) reqKey = result.Key - cblogger.Infof("요청 대상 Tag Key가 [%s]로 변경 됨", reqKey) + cblogger.Infof("target Tag Key is now [%s]", reqKey) spew.Dump(result) } case 3: - reqIID = irs.IID{SystemId: "ins-grur8hw9"} - reqKey = "tagkey5" - reqType = irs.VM - cblogger.Infof("[%s] Tag 조회 테스트 - Key[%s]", reqIID.SystemId, reqKey) + cblogger.Infof("[%s] Tag Inquiry Test - Key[%s]", reqIID.SystemId, reqKey) result, err := handler.GetTag(reqType, reqIID, reqKey) if err != nil { - cblogger.Infof("[%s] Tag 조회 실패 : [%v]", reqKey, err) + cblogger.Infof("[%s] Tag inquiry failed : [%v]", reqKey, err) } else { - cblogger.Infof("[%s] Tag 조회 결과 : [%s]", reqKey, result) + cblogger.Infof("[%s] Tag Inquiry Results : [%s]", reqKey, result) spew.Dump(result) } case 4: - reqIID = irs.IID{SystemId: "ins-grur8hw9"} - reqKey = "tagkey5" - reqType = irs.VM - cblogger.Infof("[%s] Tag 삭제 테스트 - Key[%s]", reqIID.SystemId, reqKey) + cblogger.Infof("[%s] Tag Delete Test - Key[%s]", reqIID.SystemId, addreqTag) result, err := handler.RemoveTag(reqType, reqIID, reqKey) if err != nil { - cblogger.Infof("[%s] Tag 삭제 실패 : [%v]", reqKey, err) + cblogger.Infof("[%s] Tag deletion failed : [%v]", reqKey, err) } else { - cblogger.Infof("[%s] Tag 삭제 결과 : [%v]", reqKey, result) + cblogger.Infof("[%s] Tag Delete Results : [%v]", reqKey, result) } case 5: - reqKey = "tagkey5" - // reqKey = "testTagValue" reqType = irs.ALL - cblogger.Infof("[%s] Tag 찾기 테스트 - Key[%s]", reqType, reqKey) + cblogger.Infof("[%s] Tag Find Test - Key[%s]", reqType, reqKey) result, err := handler.FindTag(reqType, reqKey) if err != nil { - cblogger.Infof("[%s] Tag 검색 실패 : [%s]", reqKey, err) + cblogger.Infof("[%s] Tag search failed : [%s]", reqKey, err) } else { - cblogger.Infof("[%s] Tag 검색 결과 : [%d]건", reqKey, len(result)) + cblogger.Infof("[%s]Tag search Results : [%d]건", reqKey, len(result)) spew.Dump(result) - cblogger.Infof("Tag 검색 결과 : [%d]건", len(result)) - } + cblogger.Infof("Tag search Results : [%d]건", len(result)) + } + + // case 6: + // MyImageResourceHandler, err := testconf.GetResourceHandler("MyImage") + // if err != nil { + // panic(err) + // } + // MyImageHandler := MyImageResourceHandler.(irs.MyImageHandler) + + // SecurityResourceHandler, err := testconf.GetResourceHandler("Security") + // if err != nil { + // panic(err) + // } + // SecurityHandler := SecurityResourceHandler.(irs.SecurityHandler) + + // KeyPairResourceHandler, err := testconf.GetResourceHandler("KeyPair") + // if err != nil { + // panic(err) + // } + // KeyPairHandler := KeyPairResourceHandler.(irs.KeyPairHandler) + + // VPCResourceHandler, err := testconf.GetResourceHandler("VPC") + // if err != nil { + // panic(err) + // } + // VPCHandler := VPCResourceHandler.(irs.VPCHandler) + + // NLBResourceHandler, err := testconf.GetResourceHandler("NLB") + // if err != nil { + // panic(err) + // } + // NLBHandler := NLBResourceHandler.(irs.NLBHandler) + + // DiskResourceHandler, err := testconf.GetResourceHandler("Disk") + // if err != nil { + // panic(err) + // } + // DiskHandler := DiskResourceHandler.(irs.DiskHandler) + + // var errs []error + // fmt.Println("@@ START VPCHandler") + // fmt.Println("@ CreateVPC") + // vpcReqInfo := irs.VPCReqInfo{ + // IId: irs.IID{NameId: "TAG-VPC"}, + // IPv4_CIDR: "10.0.0.0/16", + // SubnetInfoList: []irs.SubnetInfo{ + // { + // IId: irs.IID{NameId: "TAG-Subnet-1"}, + // IPv4_CIDR: "10.0.1.0/24", + // TagList: tags, + // }, + // // { + // // IId: irs.IID{NameId: "New-TAG-Subnet-2"}, + // // IPv4_CIDR: "10.0.2.0/24", + // // }, + // }, + // TagList: tags, + // } + // createVPCRes, err := VPCHandler.CreateVPC(vpcReqInfo) + // if err != nil { + // errs = append(errs, err) + // fmt.Println("@@@@@@@@@@@@ " + err.Error()) + // } else { + // fmt.Printf("Res : %+v \n", createVPCRes) + // vpcSystemId = createVPCRes.IId.SystemId + // } + // fmt.Println("@ AddSubnet") + // subnetReqVpcInfo := irs.IID{SystemId: vpcSystemId} + // subnetReqInfo := irs.SubnetInfo{ + // IId: irs.IID{NameId: "TAG-Subnet-2"}, + // IPv4_CIDR: "10.0.3.0/24", + // TagList: tags, + // } + // addSubnetRes, err := VPCHandler.AddSubnet(subnetReqVpcInfo, subnetReqInfo) + // if err != nil { + // errs = append(errs, err) + // fmt.Println("@@@@@@@@@@@@ " + err.Error()) + // } else { + // fmt.Printf("Res : %+v \n", addSubnetRes) + // } + + // fmt.Println("@@ START MyImageHandler") + // myImageReqInfo := irs.MyImageInfo{ + // SourceVM: irs.IID{SystemId: instanceId}, + // TagList: tags, + // } + // myIamgeRes, err := MyImageHandler.SnapshotVM(myImageReqInfo) + // if err != nil { + // errs = append(errs, err) + // } + // fmt.Printf("Res : %+v \n", myIamgeRes) + + // fmt.Println("@@ START MyImageHandler") + // myImageReqInfo := irs.SecurityReqInfo{ + // SourceVM: irs.IID{SystemId: instanceId}, + // TagList: tags, + // } + // myIamgeRes, err := SecurityHandler.CreateSecurity() + // if err != nil { + // errs = append(errs, err) + // } + // fmt.Printf("Res : %+v \n", myIamgeRes) + + // case 8: + // fmt.Println("### vpcSystemId:", vpcSystemId) + } } } @@ -1691,18 +1822,18 @@ func handlePriceInfo() { func main() { cblogger.Info("Tencent Cloud Resource Test") - // handleVPC() //VPC - // //handleNLB() + // handleVPC() //VPC - + // handleNLB() //handleVMSpec() - //handleSecurity() + // handleSecurity() - //handleImage() //AMI - //handleKeyPair() - //handleVM() - //handleDisk() + // handleKeyPair() - + // handleVM() - + // handleDisk() - // handleMyImage() //handlePublicIP() // PublicIP 생성 후 conf // handleRegionZone() //handleVNic() //Lancard // handlePriceInfo() - handleTag() + // handleTag() } From e5ad56fcf0c001f5514bffa02be2c0716c32cd59 Mon Sep 17 00:00:00 2001 From: innodreamer Date: Tue, 23 Jul 2024 21:24:26 +0900 Subject: [PATCH 32/56] Unify NCP VM Creating Status (Booting / Setting up -> Creating) --- .../drivers/ncp/resources/VMHandler.go | 107 +++++------------- 1 file changed, 28 insertions(+), 79 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go index 5cf94d903..24d716035 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go @@ -185,7 +185,7 @@ func (vmHandler *NcpVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, err } } } - cblogger.Info("### Succeeded in Creating Init UserData!!") + // cblogger.Info("### Succeeded in Creating Init UserData!!") // cblogger.Infof("Init UserData : [%s]", *initUserData) // Note) NCP에서는 UserData용 string에 Base64 인코딩 불필요 // cmdStringBase64 := base64.StdEncoding.EncodeToString([]byte(cmdString)) @@ -239,7 +239,7 @@ func (vmHandler *NcpVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, err cblogger.Info("# createTagResult : ", createTagResult) // Wait while being created to get VM information. - curStatus, statusErr := vmHandler.WaitToGetInfo(newVMIID) + curStatus, statusErr := vmHandler.WaitToGetVMInfo(newVMIID) if statusErr != nil { rtnErr := logAndReturnError(callLogInfo, "Failed to Wait to Get the VM info. : ", statusErr) return irs.VMInfo{}, rtnErr @@ -472,10 +472,9 @@ func (vmHandler *NcpVMHandler) MappingServerInfo(NcpInstance *server.ServerInsta cblogger.Debug(error.Error()) cblogger.Debug("Failed to Get VPC Name from Tag of the VM instance!!") // return irs.VMInfo{}, error // Caution!! - } else { - cblogger.Infof("# vpcName : [%s]", vpcName) - cblogger.Infof("# subnetName : [%s]", subnetName) } + // cblogger.Infof("# vpcName : [%s]", vpcName) + // cblogger.Infof("# subnetName : [%s]", subnetName) if len(vpcName) < 1 { cblogger.Debug("Failed to Get VPC Name from Tag!!") @@ -521,55 +520,20 @@ func (vmHandler *NcpVMHandler) GetVM(vmIID irs.IID) (irs.VMInfo, error) { InitLog() callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Zone, call.VM, vmIID.NameId, "GetVM()") - instanceNumList := []*string{ncloud.String(vmIID.SystemId)} - // spew.Dump(instanceNumList) - - curStatus, errStatus := vmHandler.GetVMStatus(vmIID) - if errStatus != nil { - rtnErr := logAndReturnError(callLogInfo, "Failed to Get the VM Status : ", errStatus) - return irs.VMInfo{}, rtnErr - } - cblogger.Info("===> VM Status : ", curStatus) - - // Since it's impossible to get VM info. during Creation, ... - switch string(curStatus) { - case "Creating": - cblogger.Infof("Wait for the VM creation before inquiring VM info. The VM status : [%s]", string(curStatus)) - return irs.VMInfo{}, errors.New("The VM status is 'Creating', wait for the VM creation before inquiring VM info. : " + vmIID.SystemId) - default: - cblogger.Infof("===> The VM status not 'Creating', you can get the VM info.") + if strings.EqualFold(vmIID.SystemId, "") { + newErr := fmt.Errorf("Invalid VM System ID!!") + cblogger.Error(newErr.Error()) + return irs.VMInfo{}, newErr } - regionNo, err := vmHandler.GetRegionNo(vmHandler.RegionInfo.Region) - if err != nil { - rtnErr := logAndReturnError(callLogInfo, "Failed to Get NCP Region No :", err) - return irs.VMInfo{}, rtnErr - } - zoneNo, err := vmHandler.GetZoneNo(vmHandler.RegionInfo.Region, vmHandler.RegionInfo.Zone) - if err != nil { - rtnErr := logAndReturnError(callLogInfo, "Failed to Get NCP Zone No of the Zone Code :", err) - return irs.VMInfo{}, rtnErr - } - instanceReq := server.GetServerInstanceListRequest{ - ServerInstanceNoList: instanceNumList, - RegionNo: regionNo, - ZoneNo: zoneNo, - } - callLogStart := call.Start() - result, err := vmHandler.VMClient.V2Api.GetServerInstanceList(&instanceReq) + ncpVMInfo, err := vmHandler.GetNcpVMInfo(vmIID.SystemId) if err != nil { - rtnErr := logAndReturnError(callLogInfo, "Failed to Get VM list from NCP :", err) - return irs.VMInfo{}, rtnErr - } - LoggingInfo(callLogInfo, callLogStart) - - if len(result.ServerInstanceList) < 1 { - newErr := fmt.Errorf("Failed to Find Any VM info with the SystemId : [%s]", vmIID.SystemId) + newErr := fmt.Errorf("Failed to Get the VM Info : [%v]", err) cblogger.Error(newErr.Error()) return irs.VMInfo{}, newErr } - vmInfo, err := vmHandler.MappingServerInfo(result.ServerInstanceList[0]) + vmInfo, err := vmHandler.MappingServerInfo(ncpVMInfo) if err != nil { LoggingError(callLogInfo, err) return irs.VMInfo{}, err @@ -967,9 +931,9 @@ func ConvertVMStatusString(vmStatus string) (irs.VMStatus, error) { resultStatus = "Creating" } else if strings.EqualFold(vmStatus, "booting") { //Caution!! - resultStatus = "Booting" + resultStatus = "Creating" } else if strings.EqualFold(vmStatus, "setting up") { - resultStatus = "Setting_up" + resultStatus = "Creating" } else if strings.EqualFold(vmStatus, "running") { resultStatus = "Running" } else if strings.EqualFold(vmStatus, "shutting down") { @@ -1033,7 +997,7 @@ func (vmHandler *NcpVMHandler) GetVMStatus(vmIID irs.IID) (irs.VMStatus, error) cblogger.Debug(newErr.Error()) // For after Termination!! return irs.VMStatus(""), newErr // Caution!!) Do not fill in "Failed." } - cblogger.Info("Succeeded in Getting ServerInstanceList!!") + // cblogger.Info("Succeeded in Getting ServerInstanceList!!") vmStatus, errStatus := ConvertVMStatusString(*result.ServerInstanceList[0].ServerInstanceStatusName) cblogger.Info("# Converted VM Status : " + vmStatus) @@ -1165,7 +1129,7 @@ func (vmHandler *NcpVMHandler) ListVM() ([]*irs.VMInfo, error) { } // Waiting for up to 300 seconds until VM info. can be get -func (vmHandler *NcpVMHandler) WaitToGetInfo(vmIID irs.IID) (irs.VMStatus, error) { +func (vmHandler *NcpVMHandler) WaitToGetVMInfo(vmIID irs.IID) (irs.VMStatus, error) { cblogger.Info("======> As VM info. cannot be retrieved immediately after VM creation, it waits until running.") curRetryCnt := 0 @@ -1178,14 +1142,13 @@ func (vmHandler *NcpVMHandler) WaitToGetInfo(vmIID irs.IID) (irs.VMStatus, error cblogger.Error(newErr.Error()) return irs.VMStatus("Failed. "), newErr } else { - cblogger.Infof("Succeeded in Getting the VM Status of [%s] : [%s]", vmIID.SystemId, curStatus) + cblogger.Infof("===> VM Status : [%s]", curStatus) } - cblogger.Infof("===> VM Status : [%s]", curStatus) switch string(curStatus) { - case "Creating", "Booting", "Setting_up": + case "Creating": curRetryCnt++ - cblogger.Infof("The VM is still 'Creating' and 'Booting', so wait for a second more before inquiring the VM info.") + cblogger.Infof("The VM Status is still 'Creating', so wait for a second more before inquiring the VM info.") time.Sleep(time.Second * 5) if curRetryCnt > maxRetryCnt { cblogger.Errorf("Despite waiting for a long time(%d sec), the VM status is %s, so it is forcibly finishied.", maxRetryCnt, curStatus) @@ -1193,7 +1156,7 @@ func (vmHandler *NcpVMHandler) WaitToGetInfo(vmIID irs.IID) (irs.VMStatus, error } default: - cblogger.Infof("===> ### The VM Creation is finished, stopping the waiting.") + cblogger.Infof("===> ### The VM Creation has ended, stopping the waiting.") return irs.VMStatus(curStatus), nil //break } @@ -1221,11 +1184,11 @@ func (vmHandler *NcpVMHandler) WaitToDelPublicIp(vmIID irs.IID) (irs.VMStatus, e switch string(curStatus) { case "Suspended", "Terminating": curRetryCnt++ - cblogger.Infof("The VM is still 'Terminating', so wait for a second more before inquiring the VM info.") + cblogger.Infof("The VM Status is still 'Terminating', so wait for a second more before inquiring the VM info.") time.Sleep(time.Second * 5) if curRetryCnt > maxRetryCnt { cblogger.Errorf("Despite waiting for a long time(%d sec), the VM status is '%s', so it is forcibly finished.", maxRetryCnt, curStatus) - return irs.VMStatus("Failed"), errors.New("Despite waiting for a long time, the VM status is 'Creating', so it is forcibly finishied.") + return irs.VMStatus("Failed"), errors.New("Despite waiting for a long time, the VM status is 'Terminating', so it is forcibly finishied.") } default: @@ -1416,28 +1379,14 @@ func (vmHandler *NcpVMHandler) DeleteVMTags(vmID *string) (bool, error) { return true, nil } -func (vmHandler *NcpVMHandler) GetNcpVMInfo(instanceId string) (*server.ServerInstance, error) { +func (vmHandler *NcpVMHandler) GetNcpVMInfo(vmId string) (*server.ServerInstance, error) { cblogger.Info("NCP Classic Cloud driver: called GetNcpVMInfo()") - - vmIID := irs.IID{SystemId: instanceId} - instanceNumList := []*string{ncloud.String(instanceId)} - - curStatus, errStatus := vmHandler.GetVMStatus(vmIID) - if errStatus != nil { - newErr := fmt.Errorf("Failed to Get the Status of the VM : [%v]", errStatus) + + if strings.EqualFold(vmId, "") { + newErr := fmt.Errorf("Invalid VM ID!!") cblogger.Error(newErr.Error()) return nil, newErr } - cblogger.Info("===> VM Status : ", curStatus) - - // Since it's impossible to get VM info. during Creation, ... - switch string(curStatus) { - case "Creating", "Booting": - cblogger.Infof("Wait for the VM creation before inquiring VM info. The VM status : [%s]", string(curStatus)) - return nil, errors.New("The VM status is 'Creating' or 'Booting', wait for the VM creation before inquiring VM info. : " + vmIID.SystemId) - default: - cblogger.Infof("===> The VM status not 'Creating' or 'Booting', you can get the VM info.") - } regionNo, err := vmHandler.GetRegionNo(vmHandler.RegionInfo.Region) if err != nil { @@ -1450,18 +1399,18 @@ func (vmHandler *NcpVMHandler) GetNcpVMInfo(instanceId string) (*server.ServerIn return nil, err } instanceReq := server.GetServerInstanceListRequest{ - ServerInstanceNoList: instanceNumList, + ServerInstanceNoList: []*string{ncloud.String(vmId)}, RegionNo: regionNo, ZoneNo: zoneNo, } result, err := vmHandler.VMClient.V2Api.GetServerInstanceList(&instanceReq) if err != nil { - newErr := fmt.Errorf("Failed to Find VM list with the SystemId from NCP : [%s], [%v]", vmIID.SystemId, err) + newErr := fmt.Errorf("Failed to Find VM list with the SystemId from NCP : [%s], [%v]", vmId, err) cblogger.Error(newErr.Error()) return nil, newErr } if len(result.ServerInstanceList) < 1 { - newErr := fmt.Errorf("Failed to Find Any VM info with the SystemId : [%s]", vmIID.SystemId) + newErr := fmt.Errorf("Failed to Find Any VM info with the SystemId : [%s]", vmId) cblogger.Error(newErr.Error()) return nil, newErr } From dfcdda9ed892a312bb9d3db96a30159ac61df250 Mon Sep 17 00:00:00 2001 From: powerkimhub Date: Tue, 23 Jul 2024 22:43:48 +0900 Subject: [PATCH 33/56] Add initial Tag Mock Driver and Server Implementation --- api-runtime/common-runtime/TagManager.go | 146 +++ api-runtime/rest-runtime/TagRest.go | 163 +++ .../cloud-driver/drivers/mock/MockDriver.go | 1 + .../mock/connect/MockCloudConnection.go | 20 +- .../drivers/mock/resources/ClusterHandler.go | 364 ++++++ .../drivers/mock/resources/DiskHandler.go | 1 + .../drivers/mock/resources/KeyPairHandler.go | 17 +- .../drivers/mock/resources/MyImageHandler.go | 85 +- .../drivers/mock/resources/NLBHandler.go | 450 ++++---- .../drivers/mock/resources/SecurityHandler.go | 23 +- .../drivers/mock/resources/TagHandler.go | 1010 +++++++++++++++++ .../drivers/mock/resources/VMHandler.go | 2 + .../drivers/mock/resources/VPCHandler.go | 176 +-- .../drivers/mock/test/priceinfo_test.go | 12 +- .../drivers/mock/test/security_test.go | 216 ++-- .../drivers/mock/test/tag_test.go | 268 +++++ .../mock/test/test-priceinfo_test.go.sh | 0 .../drivers/mock/test/test-tag_test.go.sh | 3 + .../drivers/mock/test/test-v-tag_test.go.sh | 3 + .../drivers/mock/test/test-vm_test.go.sh | 3 + .../cloud-driver/drivers/mock/test/vm_test.go | 7 +- .../drivers/mock/test/vpc_test.go | 29 +- 22 files changed, 2477 insertions(+), 522 deletions(-) create mode 100644 api-runtime/common-runtime/TagManager.go create mode 100644 api-runtime/rest-runtime/TagRest.go create mode 100644 cloud-control-manager/cloud-driver/drivers/mock/resources/ClusterHandler.go create mode 100644 cloud-control-manager/cloud-driver/drivers/mock/resources/TagHandler.go create mode 100644 cloud-control-manager/cloud-driver/drivers/mock/test/tag_test.go mode change 100644 => 100755 cloud-control-manager/cloud-driver/drivers/mock/test/test-priceinfo_test.go.sh create mode 100755 cloud-control-manager/cloud-driver/drivers/mock/test/test-tag_test.go.sh create mode 100755 cloud-control-manager/cloud-driver/drivers/mock/test/test-v-tag_test.go.sh create mode 100755 cloud-control-manager/cloud-driver/drivers/mock/test/test-vm_test.go.sh diff --git a/api-runtime/common-runtime/TagManager.go b/api-runtime/common-runtime/TagManager.go new file mode 100644 index 000000000..ea1c3a423 --- /dev/null +++ b/api-runtime/common-runtime/TagManager.go @@ -0,0 +1,146 @@ +// Cloud Control Manager's Rest Runtime of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// by CB-Spider Team, 2024.07. + +package commonruntime + +import ( + ccm "github.com/cloud-barista/cb-spider/cloud-control-manager" + cres "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" +) + +//================ Tag Handler + +// AddTag adds a tag to a resource. +func AddTag(connectionName string, resType cres.RSType, resIID cres.IID, tag cres.KeyValue) (cres.KeyValue, error) { + cblog.Info("call AddTag()") + + // check empty and trim user inputs + connectionName, err := EmptyCheckAndTrim("connectionName", connectionName) + if err != nil { + cblog.Error(err) + return cres.KeyValue{}, err + } + + cldConn, err := ccm.GetCloudConnection(connectionName) + if err != nil { + cblog.Error(err) + return cres.KeyValue{}, err + } + + handler, err := cldConn.CreateTagHandler() + if err != nil { + cblog.Error(err) + return cres.KeyValue{}, err + } + + return handler.AddTag(resType, resIID, tag) +} + +// ListTag lists all tags of a resource. +func ListTag(connectionName string, resType cres.RSType, resIID cres.IID) ([]cres.KeyValue, error) { + cblog.Info("call ListTag()") + + // check empty and trim user inputs + connectionName, err := EmptyCheckAndTrim("connectionName", connectionName) + if err != nil { + cblog.Error(err) + return nil, err + } + + cldConn, err := ccm.GetCloudConnection(connectionName) + if err != nil { + cblog.Error(err) + return nil, err + } + + handler, err := cldConn.CreateTagHandler() + if err != nil { + cblog.Error(err) + return nil, err + } + + return handler.ListTag(resType, resIID) +} + +// GetTag gets a specific tag of a resource. +func GetTag(connectionName string, resType cres.RSType, resIID cres.IID, key string) (cres.KeyValue, error) { + cblog.Info("call GetTag()") + + // check empty and trim user inputs + connectionName, err := EmptyCheckAndTrim("connectionName", connectionName) + if err != nil { + cblog.Error(err) + return cres.KeyValue{}, err + } + + cldConn, err := ccm.GetCloudConnection(connectionName) + if err != nil { + cblog.Error(err) + return cres.KeyValue{}, err + } + + handler, err := cldConn.CreateTagHandler() + if err != nil { + cblog.Error(err) + return cres.KeyValue{}, err + } + + return handler.GetTag(resType, resIID, key) +} + +// RemoveTag removes a specific tag from a resource. +func RemoveTag(connectionName string, resType cres.RSType, resIID cres.IID, key string) (bool, error) { + cblog.Info("call RemoveTag()") + + // check empty and trim user inputs + connectionName, err := EmptyCheckAndTrim("connectionName", connectionName) + if err != nil { + cblog.Error(err) + return false, err + } + + cldConn, err := ccm.GetCloudConnection(connectionName) + if err != nil { + cblog.Error(err) + return false, err + } + + handler, err := cldConn.CreateTagHandler() + if err != nil { + cblog.Error(err) + return false, err + } + + return handler.RemoveTag(resType, resIID, key) +} + +// FindTag finds tags by key or value. +func FindTag(connectionName string, resType cres.RSType, keyword string) ([]*cres.TagInfo, error) { + cblog.Info("call FindTag()") + + // check empty and trim user inputs + connectionName, err := EmptyCheckAndTrim("connectionName", connectionName) + if err != nil { + cblog.Error(err) + return nil, err + } + + cldConn, err := ccm.GetCloudConnection(connectionName) + if err != nil { + cblog.Error(err) + return nil, err + } + + handler, err := cldConn.CreateTagHandler() + if err != nil { + cblog.Error(err) + return nil, err + } + + return handler.FindTag(resType, keyword) +} diff --git a/api-runtime/rest-runtime/TagRest.go b/api-runtime/rest-runtime/TagRest.go new file mode 100644 index 000000000..c488b6f66 --- /dev/null +++ b/api-runtime/rest-runtime/TagRest.go @@ -0,0 +1,163 @@ +// Cloud Control Manager's Rest Runtime of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// by CB-Spider Team, 2024. + +package restruntime + +import ( + cmrt "github.com/cloud-barista/cb-spider/api-runtime/common-runtime" + cres "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" + + // REST API (echo) + "net/http" + + "strconv" + + "github.com/labstack/echo/v4" +) + +//================ Tag Handler + +type tagAddReq struct { + ConnectionName string + ResType cres.RSType + ResIID cres.IID + Tag cres.KeyValue +} + +func AddTag(c echo.Context) error { + cblog.Info("call AddTag()") + + req := tagAddReq{} + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // Call common-runtime API + result, err := cmrt.AddTag(req.ConnectionName, req.ResType, req.ResIID, req.Tag) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.JSON(http.StatusOK, result) +} + +type tagListReq struct { + ConnectionName string + ResType cres.RSType + ResIID cres.IID +} + +func ListTag(c echo.Context) error { + cblog.Info("call ListTag()") + + req := tagListReq{} + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // To support for Get-Query Param Type API + if req.ConnectionName == "" { + req.ConnectionName = c.QueryParam("ConnectionName") + } + + // Call common-runtime API + result, err := cmrt.ListTag(req.ConnectionName, req.ResType, req.ResIID) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + var jsonResult struct { + Result []cres.KeyValue `json:"tag"` + } + jsonResult.Result = result + return c.JSON(http.StatusOK, &jsonResult) +} + +type tagGetReq struct { + ConnectionName string + ResType cres.RSType + ResIID cres.IID + Key string +} + +func GetTag(c echo.Context) error { + cblog.Info("call GetTag()") + + req := tagGetReq{} + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // To support for Get-Query Param Type API + if req.ConnectionName == "" { + req.ConnectionName = c.QueryParam("ConnectionName") + } + + // Call common-runtime API + result, err := cmrt.GetTag(req.ConnectionName, req.ResType, req.ResIID, req.Key) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.JSON(http.StatusOK, result) +} + +type tagRemoveReq struct { + ConnectionName string + ResType cres.RSType + ResIID cres.IID + Key string +} + +func RemoveTag(c echo.Context) error { + cblog.Info("call RemoveTag()") + + req := tagRemoveReq{} + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // Call common-runtime API + result, err := cmrt.RemoveTag(req.ConnectionName, req.ResType, req.ResIID, req.Key) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + resultInfo := BooleanInfo{ + Result: strconv.FormatBool(result), + } + + return c.JSON(http.StatusOK, &resultInfo) +} + +type tagFindReq struct { + ConnectionName string + ResType cres.RSType + Keyword string +} + +func FindTag(c echo.Context) error { + cblog.Info("call FindTag()") + + req := tagFindReq{} + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // Call common-runtime API + result, err := cmrt.FindTag(req.ConnectionName, req.ResType, req.Keyword) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + var jsonResult struct { + Result []*cres.TagInfo `json:"tag"` + } + jsonResult.Result = result + return c.JSON(http.StatusOK, &jsonResult) +} diff --git a/cloud-control-manager/cloud-driver/drivers/mock/MockDriver.go b/cloud-control-manager/cloud-driver/drivers/mock/MockDriver.go index 26a8fc747..03706cba6 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/MockDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/MockDriver.go @@ -44,6 +44,7 @@ func (MockDriver) GetDriverCapability() idrv.DriverCapabilityInfo { drvCapabilityInfo.PublicIPHandler = false drvCapabilityInfo.VMHandler = true drvCapabilityInfo.VMSpecHandler = true + drvCapabilityInfo.TagHandler = true // Add this line to indicate that TagHandler is supported return drvCapabilityInfo } diff --git a/cloud-control-manager/cloud-driver/drivers/mock/connect/MockCloudConnection.go b/cloud-control-manager/cloud-driver/drivers/mock/connect/MockCloudConnection.go index fc7fc0480..84d9d92a9 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/connect/MockCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/connect/MockCloudConnection.go @@ -1,18 +1,6 @@ -// Cloud Driver Interface of CB-Spider. -// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. -// The CB-Spider Mission is to connect all the clouds with a single interface. -// -// * Cloud-Barista: https://github.com/cloud-barista -// -// This is Mock Driver. -// -// by CB-Spider Team, 2020.05. - package connect import ( - "errors" - cblog "github.com/cloud-barista/cb-log" mkrs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/mock/resources" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" @@ -95,7 +83,9 @@ func (cloudConn *MockConnection) CreateDiskHandler() (irs.DiskHandler, error) { } func (cloudConn *MockConnection) CreateClusterHandler() (irs.ClusterHandler, error) { - return nil, errors.New("Mock Driver: not implemented") + cblogger.Info("Mock Driver: called CreateClusterHandler()!") + handler := mkrs.MockClusterHandler{cloudConn.MockName} + return &handler, nil } func (cloudConn *MockConnection) CreateMyImageHandler() (irs.MyImageHandler, error) { @@ -124,5 +114,7 @@ func (cloudConn *MockConnection) CreatePriceInfoHandler() (irs.PriceInfoHandler, } func (cloudConn *MockConnection) CreateTagHandler() (irs.TagHandler, error) { - return nil, errors.New("Mock Driver: not implemented") + cblogger.Info("Mock Driver: called CreateTagHandler()!") + handler := mkrs.MockTagHandler{MockName: cloudConn.MockName} + return &handler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/ClusterHandler.go new file mode 100644 index 000000000..4465688b1 --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/ClusterHandler.go @@ -0,0 +1,364 @@ +// Cloud Driver Interface of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// This is Mock Driver. +// +// by CB-Spider Team, 2024.07. + +package resources + +import ( + "fmt" + "sync" + "time" + + cblog "github.com/cloud-barista/cb-log" + irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" +) + +var clusterInfoMap map[string][]*irs.ClusterInfo + +type MockClusterHandler struct { + MockName string +} + +func init() { + // cblog is a global variable. + clusterInfoMap = make(map[string][]*irs.ClusterInfo) +} + +var clusterMapLock = new(sync.RWMutex) + +// (1) create clusterInfo object +// (2) insert clusterInfo into global Map +func (clusterHandler *MockClusterHandler) CreateCluster(clusterReqInfo irs.ClusterInfo) (irs.ClusterInfo, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called CreateCluster()!") + + mockName := clusterHandler.MockName + clusterReqInfo.IId.SystemId = clusterReqInfo.IId.NameId + + // Set SystemID for VPC, Subnets, and SecurityGroups + clusterReqInfo.Network.VpcIID.SystemId = clusterReqInfo.Network.VpcIID.NameId + for i, subnet := range clusterReqInfo.Network.SubnetIIDs { + subnet.SystemId = subnet.NameId + clusterReqInfo.Network.SubnetIIDs[i] = subnet + } + for i, sg := range clusterReqInfo.Network.SecurityGroupIIDs { + sg.SystemId = sg.NameId + clusterReqInfo.Network.SecurityGroupIIDs[i] = sg + } + + // Set SystemID for NodeGroups + for i, nodeGroup := range clusterReqInfo.NodeGroupList { + nodeGroup.IId.SystemId = nodeGroup.IId.NameId + nodeGroup.ImageIID.SystemId = nodeGroup.ImageIID.NameId + nodeGroup.KeyPairIID.SystemId = nodeGroup.KeyPairIID.NameId + for j, node := range nodeGroup.Nodes { + node.SystemId = node.NameId + nodeGroup.Nodes[j] = node + } + clusterReqInfo.NodeGroupList[i] = nodeGroup + } + + // Set initial status and created time + clusterReqInfo.Status = irs.ClusterCreating + clusterReqInfo.CreatedTime = time.Now() + + // (2) insert ClusterInfo into global Map + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + infoList, _ := clusterInfoMap[mockName] + infoList = append(infoList, &clusterReqInfo) + clusterInfoMap[mockName] = infoList + + clusterReqInfo.Status = irs.ClusterActive + return CloneClusterInfo(clusterReqInfo), nil +} + +func CloneClusterInfoList(srcInfoList []*irs.ClusterInfo) []*irs.ClusterInfo { + clonedInfoList := []*irs.ClusterInfo{} + for _, srcInfo := range srcInfoList { + clonedInfo := CloneClusterInfo(*srcInfo) + clonedInfoList = append(clonedInfoList, &clonedInfo) + } + return clonedInfoList +} + +func CloneClusterInfo(srcInfo irs.ClusterInfo) irs.ClusterInfo { + /* + type ClusterInfo struct { + IId IID // {NameId, SystemId} + Version string // Kubernetes Version, ex) 1.23.3 + Network NetworkInfo + NodeGroupList []NodeGroupInfo + AccessInfo AccessInfo + Addons AddonsInfo + Status ClusterStatus + CreatedTime time.Time + KeyValueList []KeyValue + } + */ + + // clone ClusterInfo + clonedInfo := irs.ClusterInfo{ + IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, + Version: srcInfo.Version, + Network: srcInfo.Network, + NodeGroupList: CloneNodeGroupInfoList(srcInfo.NodeGroupList), + AccessInfo: srcInfo.AccessInfo, + Addons: srcInfo.Addons, + Status: srcInfo.Status, + CreatedTime: srcInfo.CreatedTime, + TagList: srcInfo.TagList, + KeyValueList: srcInfo.KeyValueList, + } + + return clonedInfo +} + +func CloneNodeGroupInfoList(srcInfoList []irs.NodeGroupInfo) []irs.NodeGroupInfo { + clonedInfoList := []irs.NodeGroupInfo{} + for _, srcInfo := range srcInfoList { + clonedInfo := CloneNodeGroupInfo(srcInfo) + clonedInfoList = append(clonedInfoList, clonedInfo) + } + return clonedInfoList +} + +func CloneNodeGroupInfo(srcInfo irs.NodeGroupInfo) irs.NodeGroupInfo { + /* + type NodeGroupInfo struct { + IId IID // {NameId, SystemId} + ImageIID IID + VMSpecName string + RootDiskType string // "SSD(gp2)", "Premium SSD", ... + RootDiskSize string // "", "default", "50", "1000" (GB) + KeyPairIID IID + OnAutoScaling bool + DesiredNodeSize int + MinNodeSize int + MaxNodeSize int + Status NodeGroupStatus + Nodes []IID + KeyValueList []KeyValue + } + */ + + // clone NodeGroupInfo + clonedInfo := irs.NodeGroupInfo{ + IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, + ImageIID: irs.IID{srcInfo.ImageIID.NameId, srcInfo.ImageIID.SystemId}, + VMSpecName: srcInfo.VMSpecName, + RootDiskType: srcInfo.RootDiskType, + RootDiskSize: srcInfo.RootDiskSize, + KeyPairIID: irs.IID{srcInfo.KeyPairIID.NameId, srcInfo.KeyPairIID.SystemId}, + OnAutoScaling: srcInfo.OnAutoScaling, + DesiredNodeSize: srcInfo.DesiredNodeSize, + MinNodeSize: srcInfo.MinNodeSize, + MaxNodeSize: srcInfo.MaxNodeSize, + Status: srcInfo.Status, + Nodes: cloneIIDArray(srcInfo.Nodes), + KeyValueList: srcInfo.KeyValueList, + } + + return clonedInfo +} + +func (clusterHandler *MockClusterHandler) ListCluster() ([]*irs.ClusterInfo, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called ListCluster()!") + + mockName := clusterHandler.MockName + clusterMapLock.RLock() + defer clusterMapLock.RUnlock() + infoList, ok := clusterInfoMap[mockName] + if !ok { + return []*irs.ClusterInfo{}, nil + } + + // cloning list of Cluster + return CloneClusterInfoList(infoList), nil +} + +func (clusterHandler *MockClusterHandler) GetCluster(iid irs.IID) (irs.ClusterInfo, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called GetCluster()!") + + clusterMapLock.RLock() + defer clusterMapLock.RUnlock() + + mockName := clusterHandler.MockName + infoList, ok := clusterInfoMap[mockName] + if !ok { + return irs.ClusterInfo{}, fmt.Errorf("%s Cluster does not exist!!", iid.NameId) + } + + for _, info := range infoList { + if info.IId.NameId == iid.NameId { + return CloneClusterInfo(*info), nil + } + } + + return irs.ClusterInfo{}, fmt.Errorf("%s Cluster does not exist!!", iid.NameId) +} + +func (clusterHandler *MockClusterHandler) DeleteCluster(iid irs.IID) (bool, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called DeleteCluster()!") + + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + + mockName := clusterHandler.MockName + infoList, ok := clusterInfoMap[mockName] + if !ok { + return false, fmt.Errorf("%s Cluster does not exist!!", iid.NameId) + } + + for idx, info := range infoList { + if info.IId.SystemId == iid.SystemId { + infoList = append(infoList[:idx], infoList[idx+1:]...) + clusterInfoMap[mockName] = infoList + return true, nil + } + } + return false, nil +} + +func (clusterHandler *MockClusterHandler) AddNodeGroup(clusterIID irs.IID, nodeGroupReqInfo irs.NodeGroupInfo) (irs.NodeGroupInfo, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called AddNodeGroup()!") + + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + + mockName := clusterHandler.MockName + infoList, ok := clusterInfoMap[mockName] + if !ok { + return irs.NodeGroupInfo{}, fmt.Errorf("%s Cluster does not exist!!", clusterIID.NameId) + } + + nodeGroupReqInfo.IId.SystemId = nodeGroupReqInfo.IId.NameId + + for _, info := range infoList { + if info.IId.NameId == clusterIID.NameId { + info.NodeGroupList = append(info.NodeGroupList, nodeGroupReqInfo) + return CloneNodeGroupInfo(nodeGroupReqInfo), nil + } + } + + return irs.NodeGroupInfo{}, fmt.Errorf("%s Cluster does not exist!!", clusterIID.NameId) +} + +func (clusterHandler *MockClusterHandler) SetNodeGroupAutoScaling(clusterIID irs.IID, nodeGroupIID irs.IID, on bool) (bool, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called SetNodeGroupAutoScaling()!") + + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + + mockName := clusterHandler.MockName + infoList, ok := clusterInfoMap[mockName] + if !ok { + return false, fmt.Errorf("%s Cluster does not exist!!", clusterIID.NameId) + } + + for _, info := range infoList { + if info.IId.NameId == clusterIID.NameId { + for idx, ng := range info.NodeGroupList { + if ng.IId.NameId == nodeGroupIID.NameId { + info.NodeGroupList[idx].OnAutoScaling = on + return true, nil + } + } + } + } + + return false, fmt.Errorf("%s NodeGroup does not exist!!", nodeGroupIID.NameId) +} + +func (clusterHandler *MockClusterHandler) ChangeNodeGroupScaling(clusterIID irs.IID, nodeGroupIID irs.IID, DesiredNodeSize int, MinNodeSize int, MaxNodeSize int) (irs.NodeGroupInfo, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called ChangeNodeGroupScaling()!") + + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + + mockName := clusterHandler.MockName + infoList, ok := clusterInfoMap[mockName] + if !ok { + return irs.NodeGroupInfo{}, fmt.Errorf("%s Cluster does not exist!!", clusterIID.NameId) + } + + for _, info := range infoList { + if info.IId.NameId == clusterIID.NameId { + for idx, ng := range info.NodeGroupList { + if ng.IId.NameId == nodeGroupIID.NameId { + info.NodeGroupList[idx].DesiredNodeSize = DesiredNodeSize + info.NodeGroupList[idx].MinNodeSize = MinNodeSize + info.NodeGroupList[idx].MaxNodeSize = MaxNodeSize + return CloneNodeGroupInfo(info.NodeGroupList[idx]), nil + } + } + } + } + + return irs.NodeGroupInfo{}, fmt.Errorf("%s NodeGroup does not exist!!", nodeGroupIID.NameId) +} + +func (clusterHandler *MockClusterHandler) RemoveNodeGroup(clusterIID irs.IID, nodeGroupIID irs.IID) (bool, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called RemoveNodeGroup()!") + + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + + mockName := clusterHandler.MockName + infoList, ok := clusterInfoMap[mockName] + if !ok { + return false, fmt.Errorf("%s Cluster does not exist!!", clusterIID.NameId) + } + + for _, info := range infoList { + if info.IId.NameId == clusterIID.NameId { + for idx, ng := range info.NodeGroupList { + if ng.IId.NameId == nodeGroupIID.NameId { + info.NodeGroupList = append(info.NodeGroupList[:idx], info.NodeGroupList[idx+1:]...) + return true, nil + } + } + } + } + + return false, fmt.Errorf("%s NodeGroup does not exist!!", nodeGroupIID.NameId) +} + +func (clusterHandler *MockClusterHandler) UpgradeCluster(clusterIID irs.IID, newVersion string) (irs.ClusterInfo, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called UpgradeCluster()!") + + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + + mockName := clusterHandler.MockName + infoList, ok := clusterInfoMap[mockName] + if !ok { + return irs.ClusterInfo{}, fmt.Errorf("%s Cluster does not exist!!", clusterIID.NameId) + } + + for _, info := range infoList { + if info.IId.NameId == clusterIID.NameId { + info.Status = irs.ClusterUpdating + time.Sleep(2 * time.Second) // Simulate upgrade time + info.Version = newVersion + info.Status = irs.ClusterActive + return CloneClusterInfo(*info), nil + } + } + + return irs.ClusterInfo{}, fmt.Errorf("%s Cluster does not exist!!", clusterIID.NameId) +} diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/DiskHandler.go index e40ea35df..3340b2a81 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/DiskHandler.go @@ -94,6 +94,7 @@ func CloneDiskInfo(srcInfo irs.DiskInfo) irs.DiskInfo { Status: srcInfo.Status, OwnerVM: irs.IID{srcInfo.OwnerVM.NameId, srcInfo.OwnerVM.SystemId}, CreatedTime: srcInfo.CreatedTime, + TagList: srcInfo.TagList, // clone TagList KeyValueList: srcInfo.KeyValueList, // now, do not need cloning } diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/KeyPairHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/KeyPairHandler.go index 8685cdc99..b3701c3ea 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/resources/KeyPairHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/KeyPairHandler.go @@ -42,8 +42,15 @@ func (keyPairHandler *MockKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPairRe keyPairReqInfo.IId.SystemId = keyPairReqInfo.IId.NameId // (1) create keyPairInfo object - keyPairInfo := irs.KeyPairInfo{keyPairReqInfo.IId, - "XXXXFingerprint", "XXXXPublicKey", "XXXXPrivateKey", "cb-user", nil, nil} + keyPairInfo := irs.KeyPairInfo{ + IId: keyPairReqInfo.IId, + Fingerprint: "XXXXFingerprint", + PublicKey: "XXXXPublicKey", + PrivateKey: "XXXXPrivateKey", + VMUserID: "cb-user", + TagList: keyPairReqInfo.TagList, + KeyValueList: nil, + } // (2) insert KeyPairInfo into global Map keyMapLock.Lock() @@ -73,6 +80,7 @@ func CloneKeyPairInfo(srcInfo irs.KeyPairInfo) irs.KeyPairInfo { PrivateKey string VMUserID string + TagList []KeyValue KeyValueList []KeyValue } */ @@ -84,7 +92,8 @@ func CloneKeyPairInfo(srcInfo irs.KeyPairInfo) irs.KeyPairInfo { PublicKey: srcInfo.PublicKey, PrivateKey: srcInfo.PrivateKey, VMUserID: srcInfo.VMUserID, - KeyValueList: srcInfo.KeyValueList, // now, do not need cloning + TagList: srcInfo.TagList, // clone TagList + KeyValueList: srcInfo.KeyValueList, } return clonedInfo @@ -118,7 +127,7 @@ func (keyPairHandler *MockKeyPairHandler) GetKey(iid irs.IID) (irs.KeyPairInfo, } for _, info := range infoList { - if (*info).IId.NameId == iid.NameId { + if info.IId.NameId == iid.NameId { return CloneKeyPairInfo(*info), nil } } diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/MyImageHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/MyImageHandler.go index b16231976..733419007 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/resources/MyImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/MyImageHandler.go @@ -45,8 +45,8 @@ func (myImageHandler *MockMyImageHandler) SnapshotVM(myImageReqInfo irs.MyImageI myImageReqInfo.CreatedTime = time.Now() // (2) insert MyImageInfo into global Map -myImageMapLock.Lock() -defer myImageMapLock.Unlock() + myImageMapLock.Lock() + defer myImageMapLock.Unlock() infoList, _ := myImageInfoMap[mockName] infoList = append(infoList, &myImageReqInfo) myImageInfoMap[mockName] = infoList @@ -55,39 +55,37 @@ defer myImageMapLock.Unlock() } func CloneMyImageInfoList(srcInfoList []*irs.MyImageInfo) []*irs.MyImageInfo { - clonedInfoList := []*irs.MyImageInfo{} - for _, srcInfo := range srcInfoList { - clonedInfo := CloneMyImageInfo(*srcInfo) - clonedInfoList = append(clonedInfoList, &clonedInfo) - } - return clonedInfoList + clonedInfoList := []*irs.MyImageInfo{} + for _, srcInfo := range srcInfoList { + clonedInfo := CloneMyImageInfo(*srcInfo) + clonedInfoList = append(clonedInfoList, &clonedInfo) + } + return clonedInfoList } func CloneMyImageInfo(srcInfo irs.MyImageInfo) irs.MyImageInfo { - /* + /* type MyImageInfo struct { - IId IID // {NameId, SystemId} - - SourceVM IID - - Status MyImageStatus // Available | Deleting - - CreatedTime time.Time - KeyValueList []KeyValue + IId IID // {NameId, SystemId} + SourceVM IID + Status MyImageStatus // Available | Unavailable + CreatedTime time.Time + TagList []KeyValue + KeyValueList []KeyValue } - */ - - // clone MyImageInfo - clonedInfo := irs.MyImageInfo{ - IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, - SourceVM: irs.IID{srcInfo.SourceVM.NameId, srcInfo.SourceVM.SystemId}, - Status: srcInfo.Status, - - CreatedTime: srcInfo.CreatedTime, - KeyValueList: srcInfo.KeyValueList, // now, do not need cloning - } + */ + + // clone MyImageInfo + clonedInfo := irs.MyImageInfo{ + IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, + SourceVM: irs.IID{srcInfo.SourceVM.NameId, srcInfo.SourceVM.SystemId}, + Status: srcInfo.Status, + CreatedTime: srcInfo.CreatedTime, + TagList: srcInfo.TagList, + KeyValueList: srcInfo.KeyValueList, // now, do not need cloning + } - return clonedInfo + return clonedInfo } func (myImageHandler *MockMyImageHandler) ListMyImage() ([]*irs.MyImageInfo, error) { @@ -95,8 +93,8 @@ func (myImageHandler *MockMyImageHandler) ListMyImage() ([]*irs.MyImageInfo, err cblogger.Info("Mock Driver: called ListMyImage()!") mockName := myImageHandler.MockName -myImageMapLock.RLock() -defer myImageMapLock.RUnlock() + myImageMapLock.RLock() + defer myImageMapLock.RUnlock() infoList, ok := myImageInfoMap[mockName] if !ok { return []*irs.MyImageInfo{}, nil @@ -110,12 +108,12 @@ func (myImageHandler *MockMyImageHandler) GetMyImage(iid irs.IID) (irs.MyImageIn cblogger.Info("Mock Driver: called GetMyImage()!") mockName := myImageHandler.MockName -myImageMapLock.RLock() -defer myImageMapLock.RUnlock() - infoList, ok := myImageInfoMap[mockName] - if !ok { + myImageMapLock.RLock() + defer myImageMapLock.RUnlock() + infoList, ok := myImageInfoMap[mockName] + if !ok { return irs.MyImageInfo{}, fmt.Errorf("%s MyImage does not exist!!", iid.NameId) - } + } for _, info := range infoList { if (*info).IId.NameId == iid.NameId { @@ -130,15 +128,15 @@ func (myImageHandler *MockMyImageHandler) DeleteMyImage(iid irs.IID) (bool, erro cblogger := cblog.GetLogger("CB-SPIDER") cblogger.Info("Mock Driver: called DeleteMyImage()!") - mockName := myImageHandler.MockName + mockName := myImageHandler.MockName -myImageMapLock.Lock() -defer myImageMapLock.Unlock() + myImageMapLock.Lock() + defer myImageMapLock.Unlock() - infoList, ok := myImageInfoMap[mockName] - if !ok { - return false, fmt.Errorf("%s MyImage does not exist!!", iid.NameId) - } + infoList, ok := myImageInfoMap[mockName] + if !ok { + return false, fmt.Errorf("%s MyImage does not exist!!", iid.NameId) + } for idx, info := range infoList { if info.IId.SystemId == iid.SystemId { @@ -153,4 +151,3 @@ defer myImageMapLock.Unlock() func (myImageHandler *MockMyImageHandler) CheckWindowsImage(iid irs.IID) (bool, error) { return false, fmt.Errorf("Does not support CheckWindowsImage() yet!!") } - diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/NLBHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/NLBHandler.go index 4f5cb6b81..146ebcb15 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/resources/NLBHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/NLBHandler.go @@ -14,6 +14,7 @@ import ( "fmt" "sync" "time" + "github.com/rs/xid" cblog "github.com/cloud-barista/cb-log" @@ -41,16 +42,16 @@ func (nlbHandler *MockNLBHandler) CreateNLB(nlbInfo irs.NLBInfo) (irs.NLBInfo, e nlbInfo.IId.SystemId = nlbInfo.IId.NameId nlbInfo.VpcIID.SystemId = nlbInfo.VpcIID.NameId - // insert.NLBInfo into global Map -nlbMapLock.Lock() -defer nlbMapLock.Unlock() + // insert NLBInfo into global Map + nlbMapLock.Lock() + defer nlbMapLock.Unlock() infoList, _ := nlbInfoMap[mockName] - nlbInfo.CreatedTime = time.Now() - nlbInfo.Listener.IP = "1.2.3.4" - nlbInfo.Listener.DNSName = "" - nlbInfo.Listener.CspID = nlbInfo.IId.NameId + "-Listener-" + xid.New().String() - nlbInfo.VMGroup.CspID = nlbInfo.IId.NameId + "-VMGroup-" + xid.New().String() - nlbInfo.HealthChecker.CspID = nlbInfo.IId.NameId + "-HealthChecker-" + xid.New().String() + nlbInfo.CreatedTime = time.Now() + nlbInfo.Listener.IP = "1.2.3.4" + nlbInfo.Listener.DNSName = "" + nlbInfo.Listener.CspID = nlbInfo.IId.NameId + "-Listener-" + xid.New().String() + nlbInfo.VMGroup.CspID = nlbInfo.IId.NameId + "-VMGroup-" + xid.New().String() + nlbInfo.HealthChecker.CspID = nlbInfo.IId.NameId + "-HealthChecker-" + xid.New().String() clonedInfo := CloneNLBInfo(nlbInfo) infoList = append(infoList, &clonedInfo) nlbInfoMap[mockName] = infoList @@ -88,20 +89,17 @@ func CloneNLBInfo(srcInfo irs.NLBInfo) irs.NLBInfo { } */ - // clone.NLBInfo + // clone NLBInfo clonedInfo := irs.NLBInfo{ - IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, - VpcIID: irs.IID{srcInfo.VpcIID.NameId, srcInfo.VpcIID.SystemId}, - - Type: srcInfo.Type, - Scope: srcInfo.Scope, - - // Need not clone - Listener: srcInfo.Listener, - VMGroup: srcInfo.VMGroup, + IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, + VpcIID: irs.IID{srcInfo.VpcIID.NameId, srcInfo.VpcIID.SystemId}, + Type: srcInfo.Type, + Scope: srcInfo.Scope, + Listener: srcInfo.Listener, + VMGroup: srcInfo.VMGroup, HealthChecker: srcInfo.HealthChecker, - - CreatedTime: srcInfo.CreatedTime, + CreatedTime: srcInfo.CreatedTime, + TagList: srcInfo.TagList, // clone TagList KeyValueList: srcInfo.KeyValueList, } @@ -113,8 +111,8 @@ func (nlbHandler *MockNLBHandler) ListNLB() ([]*irs.NLBInfo, error) { cblogger.Info("Mock Driver: called ListNLB()!") mockName := nlbHandler.MockName -nlbMapLock.RLock() -defer nlbMapLock.RUnlock() + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() infoList, ok := nlbInfoMap[mockName] if !ok { return []*irs.NLBInfo{}, nil @@ -127,14 +125,14 @@ func (nlbHandler *MockNLBHandler) GetNLB(iid irs.IID) (irs.NLBInfo, error) { cblogger := cblog.GetLogger("CB-SPIDER") cblogger.Info("Mock Driver: called GetNLB()!") -nlbMapLock.RLock() -defer nlbMapLock.RUnlock() + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() mockName := nlbHandler.MockName - infoList, ok := nlbInfoMap[mockName] - if !ok { + infoList, ok := nlbInfoMap[mockName] + if !ok { return irs.NLBInfo{}, fmt.Errorf("%s NLB does not exist!!", iid.NameId) - } + } for _, info := range infoList { if info.IId.NameId == iid.NameId { @@ -149,14 +147,14 @@ func (nlbHandler *MockNLBHandler) DeleteNLB(iid irs.IID) (bool, error) { cblogger := cblog.GetLogger("CB-SPIDER") cblogger.Info("Mock Driver: called DeleteNLB()!") -nlbMapLock.Lock() -defer nlbMapLock.Unlock() + nlbMapLock.Lock() + defer nlbMapLock.Unlock() mockName := nlbHandler.MockName - infoList, ok := nlbInfoMap[mockName] - if !ok { - return false, fmt.Errorf("%s NLB does not exist!!", iid.NameId) - } + infoList, ok := nlbInfoMap[mockName] + if !ok { + return false, fmt.Errorf("%s NLB does not exist!!", iid.NameId) + } for idx, info := range infoList { if info.IId.SystemId == iid.SystemId { @@ -168,129 +166,125 @@ defer nlbMapLock.Unlock() return false, nil } - func (nlbHandler *MockNLBHandler) AddVMs(nlbIID irs.IID, vmIIDs *[]irs.IID) (irs.VMGroupInfo, error) { - cblogger := cblog.GetLogger("CB-SPIDER") - cblogger.Info("Mock Driver: called AddVMs()!") - -nlbMapLock.Lock() -defer nlbMapLock.Unlock() - - mockName := nlbHandler.MockName - infoList, ok := nlbInfoMap[mockName] - if !ok { - return irs.VMGroupInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) - } - - // check if all input rules exist - for _, info := range infoList { - if info.IId.NameId == nlbIID.NameId { - for _, vmIID := range *vmIIDs { - for _, vm := range *info.VMGroup.VMs { - if vm.NameId == vmIID.NameId { - errMSG := fmt.Sprintf("%s NLB already has this VM: %v!!", nlbIID.NameId, vmIID) - errMSG += fmt.Sprintf(" #### %s NLB has %v!!", nlbIID.NameId, *info.VMGroup.VMs) - return irs.VMGroupInfo{}, fmt.Errorf(errMSG) - } - } - } - } - } + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called AddVMs()!") + + nlbMapLock.Lock() + defer nlbMapLock.Unlock() + + mockName := nlbHandler.MockName + infoList, ok := nlbInfoMap[mockName] + if !ok { + return irs.VMGroupInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + } + + // check if all input VMs exist + for _, info := range infoList { + if info.IId.NameId == nlbIID.NameId { + for _, vmIID := range *vmIIDs { + for _, vm := range *info.VMGroup.VMs { + if vm.NameId == vmIID.NameId { + errMSG := fmt.Sprintf("%s NLB already has this VM: %v!!", nlbIID.NameId, vmIID) + errMSG += fmt.Sprintf(" #### %s NLB has %v!!", nlbIID.NameId, *info.VMGroup.VMs) + return irs.VMGroupInfo{}, fmt.Errorf(errMSG) + } + } + } + } + } // Add all VMs - for _, info := range infoList { - if info.IId.NameId == nlbIID.NameId { + for _, info := range infoList { + if info.IId.NameId == nlbIID.NameId { *info.VMGroup.VMs = append(*info.VMGroup.VMs, *vmIIDs...) - return CloneNLBInfo(*info).VMGroup, nil - } - } + return CloneNLBInfo(*info).VMGroup, nil + } + } return irs.VMGroupInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) } func (nlbHandler *MockNLBHandler) RemoveVMs(nlbIID irs.IID, vmIIDs *[]irs.IID) (bool, error) { - cblogger := cblog.GetLogger("CB-SPIDER") - cblogger.Info("Mock Driver: called RemoveVMs()!") - -nlbMapLock.Lock() -defer nlbMapLock.Unlock() - - mockName := nlbHandler.MockName - infoList, ok := nlbInfoMap[mockName] - if !ok { - return false, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) - } - - // check if all input rules do not exist - for _, info := range infoList { - if info.IId.NameId == nlbIID.NameId { - for _, vmIID := range *vmIIDs { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called RemoveVMs()!") + + nlbMapLock.Lock() + defer nlbMapLock.Unlock() + + mockName := nlbHandler.MockName + infoList, ok := nlbInfoMap[mockName] + if !ok { + return false, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + } + + // check if all input VMs do not exist + for _, info := range infoList { + if info.IId.NameId == nlbIID.NameId { + for _, vmIID := range *vmIIDs { existFlag := false - for _, vm := range *info.VMGroup.VMs { - if vm.NameId == vmIID.NameId { + for _, vm := range *info.VMGroup.VMs { + if vm.NameId == vmIID.NameId { existFlag = true - } - } + } + } if !existFlag { - errMSG := fmt.Sprintf("%s NLB does not have this VM: %v!!", nlbIID.NameId, vmIID) - errMSG += fmt.Sprintf(" #### %s NLB has %v!!", nlbIID.NameId, *info.VMGroup.VMs) - return false, fmt.Errorf(errMSG) + errMSG := fmt.Sprintf("%s NLB does not have this VM: %v!!", nlbIID.NameId, vmIID) + errMSG += fmt.Sprintf(" #### %s NLB has %v!!", nlbIID.NameId, *info.VMGroup.VMs) + return false, fmt.Errorf(errMSG) } - } - } - } + } + } + } - for _, info := range infoList { - if (*info).IId.NameId == nlbIID.NameId { + for _, info := range infoList { + if (*info).IId.NameId == nlbIID.NameId { for _, vmIID := range *vmIIDs { for idx, vm := range *info.VMGroup.VMs { if vm.NameId == vmIID.NameId { *info.VMGroup.VMs = removeVM(info.VMGroup.VMs, idx) - break; + break } } } - break; - } - } - + break + } + } - return true, nil + return true, nil } func removeVM(list *[]irs.IID, idx int) []irs.IID { return append((*list)[:idx], (*list)[idx+1:]...) } - -//------ Frontend Control +// ------ Frontend Control func (nlbHandler *MockNLBHandler) ChangeListener(nlbIID irs.IID, listener irs.ListenerInfo) (irs.ListenerInfo, error) { - cblogger := cblog.GetLogger("CB-SPIDER") - cblogger.Info("Mock Driver: called ChangeListener()!") + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called ChangeListener()!") -nlbMapLock.RLock() -defer nlbMapLock.RUnlock() + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() - mockName := nlbHandler.MockName - infoList, ok := nlbInfoMap[mockName] - if !ok { - return irs.ListenerInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) - } + mockName := nlbHandler.MockName + infoList, ok := nlbInfoMap[mockName] + if !ok { + return irs.ListenerInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + } - for _, info := range infoList { - if info.IId.NameId == nlbIID.NameId { + for _, info := range infoList { + if info.IId.NameId == nlbIID.NameId { info.Listener.Protocol = listener.Protocol info.Listener.Port = listener.Port - return CloneListenerInfo(info.Listener), nil - } - } + return CloneListenerInfo(info.Listener), nil + } + } - return irs.ListenerInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + return irs.ListenerInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) } - func CloneListenerInfo(srcInfo irs.ListenerInfo) irs.ListenerInfo { - /* + /* type ListenerInfo struct { Protocol string // TCP|UDP IP string // Auto Generated and attached @@ -300,76 +294,74 @@ func CloneListenerInfo(srcInfo irs.ListenerInfo) irs.ListenerInfo { CspID string // Optional, May be Used by Driver. KeyValueList []KeyValue } - */ - - clonedInfo := irs.ListenerInfo{ - Protocol: srcInfo.Protocol, - IP: srcInfo.IP, - Port: srcInfo.Port, - DNSName: srcInfo.DNSName, + */ - CspID: srcInfo.CspID, - KeyValueList: srcInfo.KeyValueList, - } + clonedInfo := irs.ListenerInfo{ + Protocol: srcInfo.Protocol, + IP: srcInfo.IP, + Port: srcInfo.Port, + DNSName: srcInfo.DNSName, + CspID: srcInfo.CspID, + KeyValueList: srcInfo.KeyValueList, + } - return clonedInfo + return clonedInfo } -//------ Backend Control +// ------ Backend Control func (nlbHandler *MockNLBHandler) ChangeVMGroupInfo(nlbIID irs.IID, vmGroup irs.VMGroupInfo) (irs.VMGroupInfo, error) { - cblogger := cblog.GetLogger("CB-SPIDER") - cblogger.Info("Mock Driver: called ChangeVMGroupInfo()!") - -nlbMapLock.RLock() -defer nlbMapLock.RUnlock() - - mockName := nlbHandler.MockName - infoList, ok := nlbInfoMap[mockName] - if !ok { - return irs.VMGroupInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) - } - - for _, info := range infoList { - if info.IId.NameId == nlbIID.NameId { - info.VMGroup.Protocol = vmGroup.Protocol - info.VMGroup.Port = vmGroup.Port - return CloneVMGroupInfo(info.VMGroup), nil - } - } - - return irs.VMGroupInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called ChangeVMGroupInfo()!") + + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() + + mockName := nlbHandler.MockName + infoList, ok := nlbInfoMap[mockName] + if !ok { + return irs.VMGroupInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + } + + for _, info := range infoList { + if info.IId.NameId == nlbIID.NameId { + info.VMGroup.Protocol = vmGroup.Protocol + info.VMGroup.Port = vmGroup.Port + return CloneVMGroupInfo(info.VMGroup), nil + } + } + + return irs.VMGroupInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) } func CloneVMGroupInfo(srcInfo irs.VMGroupInfo) irs.VMGroupInfo { - /* + /* type VMGroupInfo struct { - Protocol string // TCP|UDP|HTTP|HTTPS + Protocol string // TCP|UDP Port string // 1-65535 VMs *[]IID CspID string // Optional, May be Used by Driver. KeyValueList []KeyValue } - */ - - clonedInfo := irs.VMGroupInfo{ - Protocol: srcInfo.Protocol, - Port: srcInfo.Port, - VMs: CloneVMs(srcInfo.VMs), + */ - CspID: srcInfo.CspID, - KeyValueList: srcInfo.KeyValueList, - } + clonedInfo := irs.VMGroupInfo{ + Protocol: srcInfo.Protocol, + Port: srcInfo.Port, + VMs: CloneVMs(srcInfo.VMs), + CspID: srcInfo.CspID, + KeyValueList: srcInfo.KeyValueList, + } - return clonedInfo + return clonedInfo } func CloneVMs(srcInfo *[]irs.IID) *[]irs.IID { clonedList := []irs.IID{} for _, one := range *srcInfo { clonedInfo := irs.IID{ - NameId: one.NameId, - SystemId: one.SystemId, + NameId: one.NameId, + SystemId: one.SystemId, } clonedList = append(clonedList, clonedInfo) } @@ -377,88 +369,86 @@ func CloneVMs(srcInfo *[]irs.IID) *[]irs.IID { } func (nlbHandler *MockNLBHandler) ChangeHealthCheckerInfo(nlbIID irs.IID, healthChecker irs.HealthCheckerInfo) (irs.HealthCheckerInfo, error) { - cblogger := cblog.GetLogger("CB-SPIDER") - cblogger.Info("Mock Driver: called ChangeHealthCheckerInfo()!") - -nlbMapLock.RLock() -defer nlbMapLock.RUnlock() - - mockName := nlbHandler.MockName - infoList, ok := nlbInfoMap[mockName] - if !ok { - return irs.HealthCheckerInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) - } - - for _, info := range infoList { - if info.IId.NameId == nlbIID.NameId { - info.HealthChecker.Protocol = healthChecker.Protocol - info.HealthChecker.Port = healthChecker.Port - info.HealthChecker.Interval = healthChecker.Interval - info.HealthChecker.Timeout = healthChecker.Timeout - info.HealthChecker.Threshold = healthChecker.Threshold - return CloneHealthCheckerInfo(info.HealthChecker), nil - } - } - - return irs.HealthCheckerInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called ChangeHealthCheckerInfo()!") + + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() + + mockName := nlbHandler.MockName + infoList, ok := nlbInfoMap[mockName] + if !ok { + return irs.HealthCheckerInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + } + + for _, info := range infoList { + if info.IId.NameId == nlbIID.NameId { + info.HealthChecker.Protocol = healthChecker.Protocol + info.HealthChecker.Port = healthChecker.Port + info.HealthChecker.Interval = healthChecker.Interval + info.HealthChecker.Timeout = healthChecker.Timeout + info.HealthChecker.Threshold = healthChecker.Threshold + return CloneHealthCheckerInfo(info.HealthChecker), nil + } + } + + return irs.HealthCheckerInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) } func CloneHealthCheckerInfo(srcInfo irs.HealthCheckerInfo) irs.HealthCheckerInfo { - /* + /* type HealthCheckerInfo struct { - Protocol string // TCP|HTTP|HTTPS + Protocol string // TCP|HTTP Port string // Listener Port or 1-65535 Interval int // secs, Interval time between health checks. Timeout int // secs, Waiting time to decide an unhealthy VM when no response. Threshold int // num, The number of continuous health checks to change the VM status. CspID string // Optional, May be Used by Driver. - KeyValueList []KeyValue + KeyValueList []KeyValue } - */ - - clonedInfo := irs.HealthCheckerInfo{ - Protocol: srcInfo.Protocol, - Port: srcInfo.Port, - Interval: srcInfo.Interval, - Timeout: srcInfo.Timeout, - Threshold: srcInfo.Threshold, + */ - CspID: srcInfo.CspID, - KeyValueList: srcInfo.KeyValueList, - } + clonedInfo := irs.HealthCheckerInfo{ + Protocol: srcInfo.Protocol, + Port: srcInfo.Port, + Interval: srcInfo.Interval, + Timeout: srcInfo.Timeout, + Threshold: srcInfo.Threshold, + CspID: srcInfo.CspID, + KeyValueList: srcInfo.KeyValueList, + } - return clonedInfo + return clonedInfo } func (nlbHandler *MockNLBHandler) GetVMGroupHealthInfo(nlbIID irs.IID) (irs.HealthInfo, error) { - cblogger := cblog.GetLogger("CB-SPIDER") - cblogger.Info("Mock Driver: called GetVMGroupHealthInfo()!") - -nlbMapLock.RLock() -defer nlbMapLock.RUnlock() - - mockName := nlbHandler.MockName - infoList, ok := nlbInfoMap[mockName] - if !ok { - return irs.HealthInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) - } - - healthInfo := irs.HealthInfo{ &[]irs.IID{}, &[]irs.IID{}, &[]irs.IID{}} - for _, info := range infoList { - if info.IId.NameId == nlbIID.NameId { - for idx, vm := range *info.VMGroup.VMs { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called GetVMGroupHealthInfo()!") + + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() + + mockName := nlbHandler.MockName + infoList, ok := nlbInfoMap[mockName] + if !ok { + return irs.HealthInfo{}, fmt.Errorf("%s NLB does not exist!!", nlbIID.NameId) + } + + healthInfo := irs.HealthInfo{&[]irs.IID{}, &[]irs.IID{}, &[]irs.IID{}} + for _, info := range infoList { + if info.IId.NameId == nlbIID.NameId { + for idx, vm := range *info.VMGroup.VMs { *healthInfo.AllVMs = append(*healthInfo.AllVMs, vm) - if (idx+1) == len(*info.VMGroup.VMs) { - *healthInfo.UnHealthyVMs = append (*healthInfo.UnHealthyVMs, vm) - }else { - *healthInfo.HealthyVMs = append (*healthInfo.HealthyVMs, vm) + if (idx + 1) == len(*info.VMGroup.VMs) { + *healthInfo.UnHealthyVMs = append(*healthInfo.UnHealthyVMs, vm) + } else { + *healthInfo.HealthyVMs = append(*healthInfo.HealthyVMs, vm) } } - return healthInfo, nil - } - } + return healthInfo, nil + } + } - return irs.HealthInfo{}, fmt.Errorf("%s NLB VMGroup does not have VMs!!", nlbIID.NameId) + return irs.HealthInfo{}, fmt.Errorf("%s NLB VMGroup does not have VMs!!", nlbIID.NameId) } - diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/SecurityHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/SecurityHandler.go index 524607b42..dd0c2a1d4 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/resources/SecurityHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/SecurityHandler.go @@ -41,11 +41,13 @@ func (securityHandler *MockSecurityHandler) CreateSecurity(securityReqInfo irs.S securityReqInfo.IId.SystemId = securityReqInfo.IId.NameId securityReqInfo.VpcIID.SystemId = securityReqInfo.VpcIID.NameId // (1) create securityInfo object - securityInfo := irs.SecurityInfo{securityReqInfo.IId, - securityReqInfo.VpcIID, - // deprecated; securityReqInfo.Direction, - securityReqInfo.SecurityRules, - nil, nil} + securityInfo := irs.SecurityInfo{ + IId: securityReqInfo.IId, + VpcIID: securityReqInfo.VpcIID, + SecurityRules: securityReqInfo.SecurityRules, + TagList: securityReqInfo.TagList, + KeyValueList: nil, + } // (2) insert SecurityInfo into global Map sgMapLock.Lock() @@ -79,12 +81,10 @@ func CloneSecurityInfo(srcInfo irs.SecurityInfo) irs.SecurityInfo { // clone SecurityInfo clonedInfo := irs.SecurityInfo{ - IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, - VpcIID: irs.IID{srcInfo.VpcIID.NameId, srcInfo.VpcIID.SystemId}, - // deprecated; Direction: srcInfo.Direction, - - // Need not clone + IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, + VpcIID: irs.IID{srcInfo.VpcIID.NameId, srcInfo.VpcIID.SystemId}, SecurityRules: srcInfo.SecurityRules, + TagList: srcInfo.TagList, // clone TagList KeyValueList: srcInfo.KeyValueList, } @@ -224,13 +224,12 @@ func (securityHandler *MockSecurityHandler) RemoveRules(sgIID irs.IID, securityR } for _, info := range infoList { - if (*info).IId.NameId == sgIID.NameId { + if info.IId.NameId == sgIID.NameId { for idx := len(*info.SecurityRules) - 1; idx >= 0; idx-- { ruleInfo := (*info.SecurityRules)[idx] for _, reqRuleInfo := range *securityRules { if isEqualRule(&ruleInfo, &reqRuleInfo) { *info.SecurityRules = removeRule(info.SecurityRules, idx) - //return true, nil } } } diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/TagHandler.go new file mode 100644 index 000000000..a39d571a2 --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/TagHandler.go @@ -0,0 +1,1010 @@ +// Cloud Driver Interface of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// This is Mock Driver. +// +// by CB-Spider Team, 2020.09. + +package resources + +import ( + "fmt" + + cblog "github.com/cloud-barista/cb-log" + irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" +) + +type MockTagHandler struct { + MockName string +} + +func (tagHandler *MockTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called AddTag()!") + + mockName := tagHandler.MockName + + switch resType { + case irs.VPC: + return addTagToVPC(mockName, resIID, tag) + case irs.SUBNET: + return addTagToSubnet(mockName, resIID, tag) + case irs.SG: + return addTagToSG(mockName, resIID, tag) + case irs.KEY: + return addTagToKeyPair(mockName, resIID, tag) + case irs.VM: + return addTagToVM(mockName, resIID, tag) + case irs.NLB: + return addTagToNLB(mockName, resIID, tag) + case irs.DISK: + return addTagToDisk(mockName, resIID, tag) + case irs.MYIMAGE: + return addTagToMyImage(mockName, resIID, tag) + case irs.CLUSTER: + return addTagToCluster(mockName, resIID, tag) + default: + return irs.KeyValue{}, fmt.Errorf("unsupported resource type %s", resType) + } +} + +func addTagToVPC(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + vpcMapLock.Lock() + defer vpcMapLock.Unlock() + infoList, ok := vpcInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + info.TagList = append(info.TagList, tag) + return tag, nil + } + } + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func addTagToSubnet(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + vpcMapLock.Lock() + defer vpcMapLock.Unlock() + vpcInfoList, ok := vpcInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("VPC not found for %s", resIID.NameId) + } + for _, vpcInfo := range vpcInfoList { + for _, subnetInfo := range vpcInfo.SubnetInfoList { + if subnetInfo.IId.NameId == resIID.NameId { + subnetInfo.TagList = append(subnetInfo.TagList, tag) + return tag, nil + } + } + } + return irs.KeyValue{}, fmt.Errorf("Subnet not found for %s", resIID.NameId) +} + +func addTagToSG(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + sgMapLock.Lock() + defer sgMapLock.Unlock() + infoList, ok := securityInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + info.TagList = append(info.TagList, tag) + return tag, nil + } + } + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func addTagToKeyPair(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + keyMapLock.Lock() + defer keyMapLock.Unlock() + infoList, ok := keyPairInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + info.TagList = append(info.TagList, tag) + return tag, nil + } + } + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func addTagToVM(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + vmMapLock.Lock() + defer vmMapLock.Unlock() + infoList, ok := vmInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + info.TagList = append(info.TagList, tag) + return tag, nil + } + } + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func addTagToNLB(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + nlbMapLock.Lock() + defer nlbMapLock.Unlock() + infoList, ok := nlbInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + info.TagList = append(info.TagList, tag) + return tag, nil + } + } + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func addTagToDisk(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + diskMapLock.Lock() + defer diskMapLock.Unlock() + infoList, ok := diskInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + info.TagList = append(info.TagList, tag) + return tag, nil + } + } + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func addTagToMyImage(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + myImageMapLock.Lock() + defer myImageMapLock.Unlock() + infoList, ok := myImageInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + info.TagList = append(info.TagList, tag) + return tag, nil + } + } + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func addTagToCluster(mockName string, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + infoList, ok := clusterInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + info.TagList = append(info.TagList, tag) + return tag, nil + } + } + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func (tagHandler *MockTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]irs.KeyValue, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called ListTag()!") + + mockName := tagHandler.MockName + + switch resType { + case irs.VPC: + return listTagsFromVPC(mockName, resIID) + case irs.SUBNET: + return listTagsFromSubnet(mockName, resIID) + case irs.SG: + return listTagsFromSG(mockName, resIID) + case irs.KEY: + return listTagsFromKeyPair(mockName, resIID) + case irs.VM: + return listTagsFromVM(mockName, resIID) + case irs.NLB: + return listTagsFromNLB(mockName, resIID) + case irs.DISK: + return listTagsFromDisk(mockName, resIID) + case irs.MYIMAGE: + return listTagsFromMyImage(mockName, resIID) + case irs.CLUSTER: + return listTagsFromCluster(mockName, resIID) + default: + return nil, fmt.Errorf("unsupported resource type %s", resType) + } +} + +func listTagsFromVPC(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + vpcMapLock.RLock() + defer vpcMapLock.RUnlock() + infoList, ok := vpcInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + return info.TagList, nil + } + } + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func listTagsFromSubnet(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + vpcMapLock.RLock() + defer vpcMapLock.RUnlock() + vpcInfoList, ok := vpcInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("VPC not found for %s", resIID.NameId) + } + for _, vpcInfo := range vpcInfoList { + for _, subnetInfo := range vpcInfo.SubnetInfoList { + if subnetInfo.IId.NameId == resIID.NameId { + return subnetInfo.TagList, nil + } + } + } + return nil, fmt.Errorf("Subnet not found for %s", resIID.NameId) +} + +func listTagsFromSG(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + sgMapLock.RLock() + defer sgMapLock.RUnlock() + infoList, ok := securityInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + return info.TagList, nil + } + } + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func listTagsFromKeyPair(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + keyMapLock.RLock() + defer keyMapLock.RUnlock() + infoList, ok := keyPairInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + return info.TagList, nil + } + } + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func listTagsFromVM(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + vmMapLock.RLock() + defer vmMapLock.RUnlock() + infoList, ok := vmInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + return info.TagList, nil + } + } + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func listTagsFromNLB(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() + infoList, ok := nlbInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + return info.TagList, nil + } + } + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func listTagsFromDisk(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + diskMapLock.RLock() + defer diskMapLock.RUnlock() + infoList, ok := diskInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + return info.TagList, nil + } + } + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func listTagsFromMyImage(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + myImageMapLock.RLock() + defer myImageMapLock.RUnlock() + infoList, ok := myImageInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + return info.TagList, nil + } + } + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func listTagsFromCluster(mockName string, resIID irs.IID) ([]irs.KeyValue, error) { + clusterMapLock.RLock() + defer clusterMapLock.RUnlock() + infoList, ok := clusterInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + return info.TagList, nil + } + } + return nil, fmt.Errorf("resource not found for %s", resIID.NameId) +} + +func (tagHandler *MockTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (irs.KeyValue, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called GetTag()!") + + mockName := tagHandler.MockName + + switch resType { + case irs.VPC: + return getTagFromVPC(mockName, resIID, key) + case irs.SUBNET: + return getTagFromSubnet(mockName, resIID, key) + case irs.SG: + return getTagFromSG(mockName, resIID, key) + case irs.KEY: + return getTagFromKeyPair(mockName, resIID, key) + case irs.VM: + return getTagFromVM(mockName, resIID, key) + case irs.NLB: + return getTagFromNLB(mockName, resIID, key) + case irs.DISK: + return getTagFromDisk(mockName, resIID, key) + case irs.MYIMAGE: + return getTagFromMyImage(mockName, resIID, key) + case irs.CLUSTER: + return getTagFromCluster(mockName, resIID, key) + default: + return irs.KeyValue{}, fmt.Errorf("unsupported resource type %s", resType) + } +} + +func getTagFromVPC(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + vpcMapLock.RLock() + defer vpcMapLock.RUnlock() + infoList, ok := vpcInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for _, tag := range info.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.VPC, resIID.NameId) +} + +func getTagFromSubnet(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + vpcMapLock.RLock() + defer vpcMapLock.RUnlock() + vpcInfoList, ok := vpcInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("VPC not found for %s", resIID.NameId) + } + for _, vpcInfo := range vpcInfoList { + for _, subnetInfo := range vpcInfo.SubnetInfoList { + if subnetInfo.IId.NameId == resIID.NameId { + for _, tag := range subnetInfo.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.SUBNET, resIID.NameId) +} + +func getTagFromSG(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + sgMapLock.RLock() + defer sgMapLock.RUnlock() + infoList, ok := securityInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for _, tag := range info.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.SG, resIID.NameId) +} + +func getTagFromKeyPair(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + keyMapLock.RLock() + defer keyMapLock.RUnlock() + infoList, ok := keyPairInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for _, tag := range info.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.KEY, resIID.NameId) +} + +func getTagFromVM(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + vmMapLock.RLock() + defer vmMapLock.RUnlock() + infoList, ok := vmInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for _, tag := range info.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.VM, resIID.NameId) +} + +func getTagFromNLB(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() + infoList, ok := nlbInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for _, tag := range info.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.NLB, resIID.NameId) +} + +func getTagFromDisk(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + diskMapLock.RLock() + defer diskMapLock.RUnlock() + infoList, ok := diskInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for _, tag := range info.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.DISK, resIID.NameId) +} + +func getTagFromMyImage(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + myImageMapLock.RLock() + defer myImageMapLock.RUnlock() + infoList, ok := myImageInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for _, tag := range info.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.MYIMAGE, resIID.NameId) +} + +func getTagFromCluster(mockName string, resIID irs.IID, key string) (irs.KeyValue, error) { + clusterMapLock.RLock() + defer clusterMapLock.RUnlock() + infoList, ok := clusterInfoMap[mockName] + if !ok { + return irs.KeyValue{}, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for _, tag := range info.TagList { + if tag.Key == key { + return tag, nil + } + } + } + } + return irs.KeyValue{}, fmt.Errorf("tag %s not found for %s %s", key, irs.CLUSTER, resIID.NameId) +} + +func (tagHandler *MockTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string) (bool, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called RemoveTag()!") + + mockName := tagHandler.MockName + + switch resType { + case irs.VPC: + return removeTagFromVPC(mockName, resIID, key) + case irs.SUBNET: + return removeTagFromSubnet(mockName, resIID, key) + case irs.SG: + return removeTagFromSG(mockName, resIID, key) + case irs.KEY: + return removeTagFromKeyPair(mockName, resIID, key) + case irs.VM: + return removeTagFromVM(mockName, resIID, key) + case irs.NLB: + return removeTagFromNLB(mockName, resIID, key) + case irs.DISK: + return removeTagFromDisk(mockName, resIID, key) + case irs.MYIMAGE: + return removeTagFromMyImage(mockName, resIID, key) + case irs.CLUSTER: + return removeTagFromCluster(mockName, resIID, key) + default: + return false, fmt.Errorf("unsupported resource type %s", resType) + } +} + +func removeTagFromVPC(mockName string, resIID irs.IID, key string) (bool, error) { + vpcMapLock.Lock() + defer vpcMapLock.Unlock() + infoList, ok := vpcInfoMap[mockName] + if !ok { + return false, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for idx, tag := range info.TagList { + if tag.Key == key { + info.TagList = append(info.TagList[:idx], info.TagList[idx+1:]...) + return true, nil + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.VPC, resIID.NameId) +} + +func removeTagFromSubnet(mockName string, resIID irs.IID, key string) (bool, error) { + vpcMapLock.Lock() + defer vpcMapLock.Unlock() + vpcInfoList, ok := vpcInfoMap[mockName] + if !ok { + return false, fmt.Errorf("VPC not found for %s", resIID.NameId) + } + for _, vpcInfo := range vpcInfoList { + for _, subnetInfo := range vpcInfo.SubnetInfoList { + if subnetInfo.IId.NameId == resIID.NameId { + for idx, tag := range subnetInfo.TagList { + if tag.Key == key { + subnetInfo.TagList = append(subnetInfo.TagList[:idx], subnetInfo.TagList[idx+1:]...) + return true, nil + } + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.SUBNET, resIID.NameId) +} + +func removeTagFromSG(mockName string, resIID irs.IID, key string) (bool, error) { + sgMapLock.Lock() + defer sgMapLock.Unlock() + infoList, ok := securityInfoMap[mockName] + if !ok { + return false, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for idx, tag := range info.TagList { + if tag.Key == key { + info.TagList = append(info.TagList[:idx], info.TagList[idx+1:]...) + return true, nil + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.SG, resIID.NameId) +} + +func removeTagFromKeyPair(mockName string, resIID irs.IID, key string) (bool, error) { + keyMapLock.Lock() + defer keyMapLock.Unlock() + infoList, ok := keyPairInfoMap[mockName] + if !ok { + return false, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for idx, tag := range info.TagList { + if tag.Key == key { + info.TagList = append(info.TagList[:idx], info.TagList[idx+1:]...) + return true, nil + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.KEY, resIID.NameId) +} + +func removeTagFromVM(mockName string, resIID irs.IID, key string) (bool, error) { + vmMapLock.Lock() + defer vmMapLock.Unlock() + infoList, ok := vmInfoMap[mockName] + if !ok { + return false, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for idx, tag := range info.TagList { + if tag.Key == key { + info.TagList = append(info.TagList[:idx], info.TagList[idx+1:]...) + return true, nil + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.VM, resIID.NameId) +} + +func removeTagFromNLB(mockName string, resIID irs.IID, key string) (bool, error) { + nlbMapLock.Lock() + defer nlbMapLock.Unlock() + infoList, ok := nlbInfoMap[mockName] + if !ok { + return false, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for idx, tag := range info.TagList { + if tag.Key == key { + info.TagList = append(info.TagList[:idx], info.TagList[idx+1:]...) + return true, nil + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.NLB, resIID.NameId) +} + +func removeTagFromDisk(mockName string, resIID irs.IID, key string) (bool, error) { + diskMapLock.Lock() + defer diskMapLock.Unlock() + infoList, ok := diskInfoMap[mockName] + if !ok { + return false, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for idx, tag := range info.TagList { + if tag.Key == key { + info.TagList = append(info.TagList[:idx], info.TagList[idx+1:]...) + return true, nil + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.DISK, resIID.NameId) +} + +func removeTagFromMyImage(mockName string, resIID irs.IID, key string) (bool, error) { + myImageMapLock.Lock() + defer myImageMapLock.Unlock() + infoList, ok := myImageInfoMap[mockName] + if !ok { + return false, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for idx, tag := range info.TagList { + if tag.Key == key { + info.TagList = append(info.TagList[:idx], info.TagList[idx+1:]...) + return true, nil + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.MYIMAGE, resIID.NameId) +} + +func removeTagFromCluster(mockName string, resIID irs.IID, key string) (bool, error) { + clusterMapLock.Lock() + defer clusterMapLock.Unlock() + infoList, ok := clusterInfoMap[mockName] + if !ok { + return false, fmt.Errorf("resource not found for %s", resIID.NameId) + } + for _, info := range infoList { + if info.IId.NameId == resIID.NameId { + for idx, tag := range info.TagList { + if tag.Key == key { + info.TagList = append(info.TagList[:idx], info.TagList[idx+1:]...) + return true, nil + } + } + } + } + return false, fmt.Errorf("tag %s not found for %s %s", key, irs.CLUSTER, resIID.NameId) +} + +func (tagHandler *MockTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + cblogger := cblog.GetLogger("CB-SPIDER") + cblogger.Info("Mock Driver: called FindTag()!") + + mockName := tagHandler.MockName + + switch resType { + case irs.VPC: + return findTagInVPC(mockName, keyword) + case irs.SUBNET: + return findTagInSubnet(mockName, keyword) + case irs.SG: + return findTagInSG(mockName, keyword) + case irs.KEY: + return findTagInKeyPair(mockName, keyword) + case irs.VM: + return findTagInVM(mockName, keyword) + case irs.NLB: + return findTagInNLB(mockName, keyword) + case irs.DISK: + return findTagInDisk(mockName, keyword) + case irs.MYIMAGE: + return findTagInMyImage(mockName, keyword) + case irs.CLUSTER: + return findTagInCluster(mockName, keyword) + default: + return nil, fmt.Errorf("unsupported resource type %s", resType) + } +} + +func findTagInVPC(mockName string, keyword string) ([]*irs.TagInfo, error) { + vpcMapLock.RLock() + defer vpcMapLock.RUnlock() + infoList, ok := vpcInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.VPC) + } + var result []*irs.TagInfo + for _, info := range infoList { + for _, tag := range info.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.VPC, + ResIId: info.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + return result, nil +} + +func findTagInSubnet(mockName string, keyword string) ([]*irs.TagInfo, error) { + vpcMapLock.RLock() + defer vpcMapLock.RUnlock() + vpcInfoList, ok := vpcInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.SUBNET) + } + var result []*irs.TagInfo + for _, vpcInfo := range vpcInfoList { + for _, subnetInfo := range vpcInfo.SubnetInfoList { + for _, tag := range subnetInfo.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.SUBNET, + ResIId: subnetInfo.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + } + return result, nil +} + +func findTagInSG(mockName string, keyword string) ([]*irs.TagInfo, error) { + sgMapLock.RLock() + defer sgMapLock.RUnlock() + infoList, ok := securityInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.SG) + } + var result []*irs.TagInfo + for _, info := range infoList { + for _, tag := range info.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.SG, + ResIId: info.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + return result, nil +} + +func findTagInKeyPair(mockName string, keyword string) ([]*irs.TagInfo, error) { + keyMapLock.RLock() + defer keyMapLock.RUnlock() + infoList, ok := keyPairInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.KEY) + } + var result []*irs.TagInfo + for _, info := range infoList { + for _, tag := range info.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.KEY, + ResIId: info.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + return result, nil +} + +func findTagInVM(mockName string, keyword string) ([]*irs.TagInfo, error) { + vmMapLock.RLock() + defer vmMapLock.RUnlock() + infoList, ok := vmInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.VM) + } + var result []*irs.TagInfo + for _, info := range infoList { + for _, tag := range info.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.VM, + ResIId: info.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + return result, nil +} + +func findTagInNLB(mockName string, keyword string) ([]*irs.TagInfo, error) { + nlbMapLock.RLock() + defer nlbMapLock.RUnlock() + infoList, ok := nlbInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.NLB) + } + var result []*irs.TagInfo + for _, info := range infoList { + for _, tag := range info.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.NLB, + ResIId: info.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + return result, nil +} + +func findTagInDisk(mockName string, keyword string) ([]*irs.TagInfo, error) { + diskMapLock.RLock() + defer diskMapLock.RUnlock() + infoList, ok := diskInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.DISK) + } + var result []*irs.TagInfo + for _, info := range infoList { + for _, tag := range info.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.DISK, + ResIId: info.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + return result, nil +} + +func findTagInMyImage(mockName string, keyword string) ([]*irs.TagInfo, error) { + myImageMapLock.RLock() + defer myImageMapLock.RUnlock() + infoList, ok := myImageInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.MYIMAGE) + } + var result []*irs.TagInfo + for _, info := range infoList { + for _, tag := range info.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.MYIMAGE, + ResIId: info.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + return result, nil +} + +func findTagInCluster(mockName string, keyword string) ([]*irs.TagInfo, error) { + clusterMapLock.RLock() + defer clusterMapLock.RUnlock() + infoList, ok := clusterInfoMap[mockName] + if !ok { + return nil, fmt.Errorf("no tags found for resType %s", irs.CLUSTER) + } + var result []*irs.TagInfo + for _, info := range infoList { + for _, tag := range info.TagList { + if keyword == "" || keyword == "*" || tag.Key == keyword || tag.Value == keyword { + result = append(result, &irs.TagInfo{ + ResType: irs.CLUSTER, + ResIId: info.IId, + TagList: []irs.KeyValue{tag}, + }) + } + } + } + return result, nil +} diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/VMHandler.go index 5027a5abf..d69652280 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/VMHandler.go @@ -182,6 +182,7 @@ func (vmHandler *MockVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, er DataDiskIIDs: validatedDiskIIDs, + TagList: vmReqInfo.TagList, KeyValueList: nil, } @@ -526,6 +527,7 @@ func CloneVMInfo(srcInfo irs.VMInfo) irs.VMInfo { SSHAccessPoint: srcInfo.SSHAccessPoint, + TagList: srcInfo.TagList, // clone TagList KeyValueList: srcInfo.KeyValueList, // now, do not need cloning } diff --git a/cloud-control-manager/cloud-driver/drivers/mock/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/mock/resources/VPCHandler.go index 4ed6668e0..4e43595a6 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/resources/VPCHandler.go @@ -49,15 +49,16 @@ func (vpcHandler *MockVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.VPCI // (1) create vpcInfo object vpcInfo := irs.VPCInfo{ - vpcReqInfo.IId, - vpcReqInfo.IPv4_CIDR, - vpcReqInfo.SubnetInfoList, - nil, - nil} + IId: vpcReqInfo.IId, + IPv4_CIDR: vpcReqInfo.IPv4_CIDR, + SubnetInfoList: vpcReqInfo.SubnetInfoList, + TagList: vpcReqInfo.TagList, + KeyValueList: nil, + } // (2) insert VPCInfo into global Map -vpcMapLock.Lock() -defer vpcMapLock.Unlock() + vpcMapLock.Lock() + defer vpcMapLock.Unlock() infoList, _ := vpcInfoMap[mockName] infoList = append(infoList, &vpcInfo) vpcInfoMap[mockName] = infoList @@ -70,8 +71,8 @@ func (vpcHandler *MockVPCHandler) ListVPC() ([]*irs.VPCInfo, error) { cblogger.Info("Mock Driver: called ListVPC()!") mockName := vpcHandler.MockName -vpcMapLock.RLock() -defer vpcMapLock.RUnlock() + vpcMapLock.RLock() + defer vpcMapLock.RUnlock() infoList, ok := vpcInfoMap[mockName] if !ok { return []*irs.VPCInfo{}, nil @@ -81,84 +82,86 @@ defer vpcMapLock.RUnlock() return CloneVPCInfoList(infoList), nil } -func CloneVPCInfoList(srcInfoList []*irs.VPCInfo) ([]*irs.VPCInfo) { - clonedInfoList := []*irs.VPCInfo{} - for _, srcInfo := range srcInfoList { - clonedInfo := CloneVPCInfo(*srcInfo) - clonedInfoList = append(clonedInfoList, &clonedInfo) - } - return clonedInfoList +func CloneVPCInfoList(srcInfoList []*irs.VPCInfo) []*irs.VPCInfo { + clonedInfoList := []*irs.VPCInfo{} + for _, srcInfo := range srcInfoList { + clonedInfo := CloneVPCInfo(*srcInfo) + clonedInfoList = append(clonedInfoList, &clonedInfo) + } + return clonedInfoList } -func CloneVPCInfo(srcInfo irs.VPCInfo) (irs.VPCInfo) { - /* - type VPCInfo struct { - IId IID // {NameId, SystemId} - IPv4_CIDR string - SubnetInfoList []SubnetInfo +func CloneVPCInfo(srcInfo irs.VPCInfo) irs.VPCInfo { + /* + type VPCInfo struct { + IId IID // {NameId, SystemId} + IPv4_CIDR string + SubnetInfoList []SubnetInfo - KeyValueList []KeyValue + TagList []KeyValue + KeyValueList []KeyValue + } + */ + + // clone VPCInfo + clonedInfo := irs.VPCInfo{ + IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, + IPv4_CIDR: srcInfo.IPv4_CIDR, + SubnetInfoList: CloneSubnetInfoList(srcInfo.SubnetInfoList), + TagList: srcInfo.TagList, // clone TagList + KeyValueList: srcInfo.KeyValueList, } - */ - - // clone VPCInfo - clonedInfo := irs.VPCInfo { - IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, - IPv4_CIDR: srcInfo.IPv4_CIDR, - SubnetInfoList: CloneSubnetInfoList(srcInfo.SubnetInfoList), - // Need not clone - KeyValueList: srcInfo.KeyValueList, - } - - return clonedInfo + return clonedInfo } -func CloneSubnetInfoList(srcInfoList []irs.SubnetInfo) ([]irs.SubnetInfo) { - clonedInfoList := []irs.SubnetInfo{} - for _, srcInfo := range srcInfoList { - clonedInfo := CloneSubnetInfo(srcInfo) - clonedInfoList = append(clonedInfoList, clonedInfo) - } - return clonedInfoList +func CloneSubnetInfoList(srcInfoList []irs.SubnetInfo) []irs.SubnetInfo { + clonedInfoList := []irs.SubnetInfo{} + for _, srcInfo := range srcInfoList { + clonedInfo := CloneSubnetInfo(srcInfo) + clonedInfoList = append(clonedInfoList, clonedInfo) + } + return clonedInfoList } -func CloneSubnetInfo(srcInfo irs.SubnetInfo) (irs.SubnetInfo) { - /* - type SubnetInfo struct { - IId IID // {NameId, SystemId} - IPv4_CIDR string +func CloneSubnetInfo(srcInfo irs.SubnetInfo) irs.SubnetInfo { + /* + type SubnetInfo struct { + IId IID // {NameId, SystemId} + Zone string // Target Zone Name + IPv4_CIDR string - KeyValueList []KeyValue + TagList []KeyValue + KeyValueList []KeyValue + } + */ + + // clone SubnetInfo + clonedInfo := irs.SubnetInfo{ + IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, + Zone: srcInfo.Zone, + IPv4_CIDR: srcInfo.IPv4_CIDR, + TagList: srcInfo.TagList, // clone TagList + KeyValueList: srcInfo.KeyValueList, } - */ - // clone SubnetInfo - clonedInfo := irs.SubnetInfo { - IId: irs.IID{srcInfo.IId.NameId, srcInfo.IId.SystemId}, - IPv4_CIDR: srcInfo.IPv4_CIDR, - - // Need not clone - KeyValueList: srcInfo.KeyValueList, - } - - return clonedInfo + return clonedInfo } func (vpcHandler *MockVPCHandler) GetVPC(iid irs.IID) (irs.VPCInfo, error) { cblogger := cblog.GetLogger("CB-SPIDER") cblogger.Info("Mock Driver: called GetVPC()!") -vpcMapLock.RLock() -defer vpcMapLock.RUnlock() - mockName := vpcHandler.MockName - infoList, ok := vpcInfoMap[mockName] - if !ok { + vpcMapLock.RLock() + defer vpcMapLock.RUnlock() + mockName := vpcHandler.MockName + infoList, ok := vpcInfoMap[mockName] + if !ok { return irs.VPCInfo{}, fmt.Errorf("%s VPC does not exist!!", iid.NameId) - } + } for _, info := range infoList { - if (*info).IId.NameId == iid.NameId { + if info.IId.NameId == iid.NameId { return CloneVPCInfo(*info), nil } } @@ -170,14 +173,14 @@ func (vpcHandler *MockVPCHandler) DeleteVPC(iid irs.IID) (bool, error) { cblogger := cblog.GetLogger("CB-SPIDER") cblogger.Info("Mock Driver: called DeleteVPC()!") -vpcMapLock.Lock() -defer vpcMapLock.Unlock() + vpcMapLock.Lock() + defer vpcMapLock.Unlock() - mockName := vpcHandler.MockName - infoList, ok := vpcInfoMap[mockName] - if !ok { + mockName := vpcHandler.MockName + infoList, ok := vpcInfoMap[mockName] + if !ok { return false, fmt.Errorf("%s VPC does not exist!!", iid.NameId) - } + } for idx, info := range infoList { if info.IId.SystemId == iid.SystemId { @@ -193,18 +196,18 @@ func (vpcHandler *MockVPCHandler) AddSubnet(iid irs.IID, subnetInfo irs.SubnetIn cblogger := cblog.GetLogger("CB-SPIDER") cblogger.Info("Mock Driver: called AddSubnet()!") -vpcMapLock.Lock() -defer vpcMapLock.Unlock() + vpcMapLock.Lock() + defer vpcMapLock.Unlock() - mockName := vpcHandler.MockName - infoList, ok := vpcInfoMap[mockName] - if !ok { + mockName := vpcHandler.MockName + infoList, ok := vpcInfoMap[mockName] + if !ok { return irs.VPCInfo{}, fmt.Errorf("%s VPC does not exist!!", iid.NameId) - } + } subnetInfo.IId.SystemId = subnetInfo.IId.NameId for _, info := range infoList { - if (*info).IId.NameId == iid.NameId { + if info.IId.NameId == iid.NameId { info.SubnetInfoList = append(info.SubnetInfoList, subnetInfo) return CloneVPCInfo(*info), nil @@ -218,21 +221,20 @@ func (vpcHandler *MockVPCHandler) RemoveSubnet(iid irs.IID, subnetIID irs.IID) ( cblogger := cblog.GetLogger("CB-SPIDER") cblogger.Info("Mock Driver: called RemoveSubnet()!") -vpcMapLock.Lock() -defer vpcMapLock.Unlock() + vpcMapLock.Lock() + defer vpcMapLock.Unlock() - mockName := vpcHandler.MockName - infoList, ok := vpcInfoMap[mockName] - if !ok { + mockName := vpcHandler.MockName + infoList, ok := vpcInfoMap[mockName] + if !ok { return false, fmt.Errorf("%s VPC does not exist!!", iid.NameId) - } + } for _, info := range infoList { - if (*info).IId.NameId == iid.NameId { + if info.IId.NameId == iid.NameId { for idx, subInfo := range info.SubnetInfoList { if subInfo.IId.SystemId == subnetIID.SystemId { info.SubnetInfoList = append(info.SubnetInfoList[:idx], info.SubnetInfoList[idx+1:]...) - return true, nil } } diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/priceinfo_test.go b/cloud-control-manager/cloud-driver/drivers/mock/test/priceinfo_test.go index 28e195cea..9a086a967 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/test/priceinfo_test.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/test/priceinfo_test.go @@ -29,7 +29,7 @@ func TestListProductFamily(t *testing.T) { initLog() handler := &mockres.MockPriceInfoHandler{} - results, err := handler.ListProductFamily() + results, err := handler.ListProductFamily("mercury") if err != nil { t.Errorf("ListProductFamily returned an error: %v", err) } @@ -54,13 +54,13 @@ func TestGetComputeInstancePriceInfo(t *testing.T) { filterList []irs.KeyValue expectedMatch int // expected product count of result }{ - {"mercury", nil, 2}, - {"mercury", []irs.KeyValue{}, 2}, + {"mercury", nil, 3}, + {"mercury", []irs.KeyValue{}, 3}, {"mercury", []irs.KeyValue{{Key: "productId", Value: "mock.enhnace1.mercury"}}, 1}, {"mercury", []irs.KeyValue{{Key: "noField", Value: "mock.enhnace1.mercury"}}, 0}, - {"mercury", []irs.KeyValue{{Key: "vcpu", Value: "8"}}, 2}, - {"mercury", []irs.KeyValue{{Key: "pricingPolicy", Value: "OnDemand"}}, 2}, - {"mercury", []irs.KeyValue{{Key: "LeaseContractLength", Value: "1 Year"}}, 2}, + {"mercury", []irs.KeyValue{{Key: "vcpu", Value: "8"}}, 1}, + {"mercury", []irs.KeyValue{{Key: "pricingPolicy", Value: "OnDemand"}}, 3}, + {"mercury", []irs.KeyValue{{Key: "LeaseContractLength", Value: "1 Year"}}, 3}, } for _, tc := range testCases { diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/security_test.go b/cloud-control-manager/cloud-driver/drivers/mock/test/security_test.go index ff9bf9829..e8ea378c0 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/test/security_test.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/test/security_test.go @@ -11,11 +11,11 @@ package mocktest import ( idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" // icon "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/connect" - irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" mockdrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/mock" + irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" "testing" - "fmt" + cblog "github.com/cloud-barista/cb-log" ) @@ -25,19 +25,19 @@ func init() { // make the log level lower to print clearly cblog.SetLevel("error") - cred := idrv.CredentialInfo{ - MockName: "MockDriver-01", - } - connInfo := idrv.ConnectionInfo { - CredentialInfo: cred, - RegionInfo: idrv.RegionInfo{}, + cred := idrv.CredentialInfo{ + MockName: "MockDriver-01", + } + connInfo := idrv.ConnectionInfo{ + CredentialInfo: cred, + RegionInfo: idrv.RegionInfo{}, } cloudConn, _ := (&mockdrv.MockDriver{}).ConnectCloud(connInfo) securityHandler, _ = cloudConn.CreateSecurityHandler() } type SecurityTestInfo struct { - IId string + IId string VpcIID string } @@ -52,10 +52,10 @@ var securityTestInfoList = []SecurityTestInfo{ func TestSecurityCreateList(t *testing.T) { // create for _, info := range securityTestInfoList { - reqInfo := irs.SecurityReqInfo { - IId : irs.IID{info.IId, ""}, - VpcIID : irs.IID{info.VpcIID, ""}, - SecurityRules : &[]irs.SecurityRuleInfo{ {FromPort: "1", ToPort : "65535", IPProtocol : "tcp", Direction : "inbound"}, }, + reqInfo := irs.SecurityReqInfo{ + IId: irs.IID{info.IId, ""}, + VpcIID: irs.IID{info.VpcIID, ""}, + SecurityRules: &[]irs.SecurityRuleInfo{{FromPort: "1", ToPort: "65535", IPProtocol: "tcp", Direction: "inbound"}}, } _, err := securityHandler.CreateSecurity(reqInfo) if err != nil { @@ -75,126 +75,126 @@ func TestSecurityCreateList(t *testing.T) { if info.IId.SystemId != securityTestInfoList[i].IId { t.Errorf("System ID %s is not same %s", info.IId.SystemId, securityTestInfoList[i].IId) } -// fmt.Printf("\n\t%#v\n", info) + // fmt.Printf("\n\t%#v\n", info) } } func TestSecurityDeleteGet(t *testing.T) { - // Get & check the Value - info, err := securityHandler.GetSecurity(irs.IID{securityTestInfoList[0].IId, ""}) - if err != nil { - t.Error(err.Error()) - } + // Get & check the Value + info, err := securityHandler.GetSecurity(irs.IID{securityTestInfoList[0].IId, ""}) + if err != nil { + t.Error(err.Error()) + } if info.IId.SystemId != securityTestInfoList[0].IId { t.Errorf("System ID %s is not same %s", info.IId.SystemId, securityTestInfoList[0].IId) } // delete all infoList, err := securityHandler.ListSecurity() - if err != nil { - t.Error(err.Error()) - } - for _, info := range infoList { + if err != nil { + t.Error(err.Error()) + } + for _, info := range infoList { ret, err := securityHandler.DeleteSecurity(info.IId) - if err!=nil { - t.Error(err.Error()) + if err != nil { + t.Error(err.Error()) } if !ret { - t.Errorf("Return is not True!! %s", info.IId.NameId) + t.Errorf("Return is not True!! %s", info.IId.NameId) } - } + } // check the result of Delete Op infoList, err = securityHandler.ListSecurity() - if err != nil { - t.Error(err.Error()) - } - if len(infoList)>0 { + if err != nil { + t.Error(err.Error()) + } + if len(infoList) > 0 { t.Errorf("The number of Infos is not %d. It is %d.", 0, len(infoList)) } } func TestSecurityAddRules(t *testing.T) { - //---- create basic SGs - for _, info := range securityTestInfoList { - reqInfo := irs.SecurityReqInfo { - IId : irs.IID{info.IId, ""}, - VpcIID : irs.IID{info.VpcIID, ""}, - SecurityRules : &[]irs.SecurityRuleInfo{ {FromPort: "1", ToPort : "65535", IPProtocol : "tcp", Direction : "inbound"}, }, - } - _, err := securityHandler.CreateSecurity(reqInfo) - if err != nil { - t.Error(err.Error()) - } - } - // check the list size and values - infoList, err := securityHandler.ListSecurity() - if err != nil { - t.Error(err.Error()) - } - if len(infoList) != len(securityTestInfoList) { - t.Errorf("The number of Infos is not %d. It is %d.", len(securityTestInfoList), len(infoList)) - } + //---- create basic SGs + for _, info := range securityTestInfoList { + reqInfo := irs.SecurityReqInfo{ + IId: irs.IID{info.IId, ""}, + VpcIID: irs.IID{info.VpcIID, ""}, + SecurityRules: &[]irs.SecurityRuleInfo{{FromPort: "1", ToPort: "65535", IPProtocol: "tcp", Direction: "inbound"}}, + } + _, err := securityHandler.CreateSecurity(reqInfo) + if err != nil { + t.Error(err.Error()) + } + } + // check the list size and values + infoList, err := securityHandler.ListSecurity() + if err != nil { + t.Error(err.Error()) + } + if len(infoList) != len(securityTestInfoList) { + t.Errorf("The number of Infos is not %d. It is %d.", len(securityTestInfoList), len(infoList)) + } // print 1 rules - info1, err := securityHandler.GetSecurity(infoList[0].IId) - if err != nil { - t.Error(err.Error()) - } - fmt.Printf("\n\t%#v\n", *info1.SecurityRules) + // info1, err := securityHandler.GetSecurity(infoList[0].IId) + // if err != nil { + // t.Error(err.Error()) + // } + // fmt.Printf("\n\t%#v\n", *info1.SecurityRules) //---- Add 3 Ruls => 4 Rules - SecurityRules := &[]irs.SecurityRuleInfo{ - {Direction : "inbound", IPProtocol : "tcp", FromPort: "22", ToPort : "22"}, - {Direction : "inbound", IPProtocol : "tcp", FromPort: "23", ToPort : "65535"}, - {Direction : "outbound", IPProtocol : "all", FromPort: "-1", ToPort : "-1"}, - } - info2, err := securityHandler.AddRules(infoList[0].IId, SecurityRules) - if err != nil { - t.Error(err.Error()) - } - // check the list size and values - if len(*info2.SecurityRules) != 4 { - t.Errorf("The number of Infos is not %d. It is %d.", 4, len(*info2.SecurityRules)) - } + SecurityRules := &[]irs.SecurityRuleInfo{ + {Direction: "inbound", IPProtocol: "tcp", FromPort: "22", ToPort: "22"}, + {Direction: "inbound", IPProtocol: "tcp", FromPort: "23", ToPort: "65535"}, + {Direction: "outbound", IPProtocol: "all", FromPort: "-1", ToPort: "-1"}, + } + info2, err := securityHandler.AddRules(infoList[0].IId, SecurityRules) + if err != nil { + t.Error(err.Error()) + } + // check the list size and values + if len(*info2.SecurityRules) != 4 { + t.Errorf("The number of Infos is not %d. It is %d.", 4, len(*info2.SecurityRules)) + } // print 4 rules - fmt.Printf("\n\t%#v\n", *info2.SecurityRules) - - //---- Remove 3 Ruls => 1 Rule - SecurityRules2 := &[]irs.SecurityRuleInfo{ - {Direction : "inbound", IPProtocol : "tcp", FromPort: "22", ToPort : "22"}, - {Direction : "inbound", IPProtocol : "tcp", FromPort: "23", ToPort : "65535"}, - {Direction : "outbound", IPProtocol : "all", FromPort: "-1", ToPort : "-1"}, - } - result, err := securityHandler.RemoveRules(infoList[0].IId, SecurityRules2) - if result != true { - t.Error(err.Error()) - } - info3, err := securityHandler.GetSecurity(infoList[0].IId) - if err != nil { - t.Error(err.Error()) - } - // check the list size and values - if len(*info3.SecurityRules) != 1 { - t.Errorf("The number of Infos is not %d. It is %d.", 1, len(*info3.SecurityRules)) - } + // fmt.Printf("\n\t%#v\n", *info2.SecurityRules) + + //---- Remove 3 Ruls => 1 Rule + SecurityRules2 := &[]irs.SecurityRuleInfo{ + {Direction: "inbound", IPProtocol: "tcp", FromPort: "22", ToPort: "22"}, + {Direction: "inbound", IPProtocol: "tcp", FromPort: "23", ToPort: "65535"}, + {Direction: "outbound", IPProtocol: "all", FromPort: "-1", ToPort: "-1"}, + } + result, err := securityHandler.RemoveRules(infoList[0].IId, SecurityRules2) + if result != true { + t.Error(err.Error()) + } + info3, err := securityHandler.GetSecurity(infoList[0].IId) + if err != nil { + t.Error(err.Error()) + } + // check the list size and values + if len(*info3.SecurityRules) != 1 { + t.Errorf("The number of Infos is not %d. It is %d.", 1, len(*info3.SecurityRules)) + } // print 1 Rule - fmt.Printf("\n\t%#v\n", *info3.SecurityRules) - - //---- Remove last Rule - SecurityRules3 := &[]irs.SecurityRuleInfo{ - {Direction : "inbound", IPProtocol : "tcp", FromPort: "1", ToPort : "65535"}, - } - result2, err := securityHandler.RemoveRules(infoList[0].IId, SecurityRules3) - if result2 != true { - t.Error(err.Error()) - } - info4, err := securityHandler.GetSecurity(infoList[0].IId) - if err != nil { - t.Error(err.Error()) - } - // check the list size and values - if len(*info4.SecurityRules) != 0 { - t.Errorf("The number of Infos is not %d. It is %d.", 0, len(*info4.SecurityRules)) - } + // fmt.Printf("\n\t%#v\n", *info3.SecurityRules) + + //---- Remove last Rule + SecurityRules3 := &[]irs.SecurityRuleInfo{ + {Direction: "inbound", IPProtocol: "tcp", FromPort: "1", ToPort: "65535"}, + } + result2, err := securityHandler.RemoveRules(infoList[0].IId, SecurityRules3) + if result2 != true { + t.Error(err.Error()) + } + info4, err := securityHandler.GetSecurity(infoList[0].IId) + if err != nil { + t.Error(err.Error()) + } + // check the list size and values + if len(*info4.SecurityRules) != 0 { + t.Errorf("The number of Infos is not %d. It is %d.", 0, len(*info4.SecurityRules)) + } // pritn 0 Rule - fmt.Printf("\n\t%#v\n", *info4.SecurityRules) + // fmt.Printf("\n\t%#v\n", *info4.SecurityRules) } diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/tag_test.go b/cloud-control-manager/cloud-driver/drivers/mock/test/tag_test.go new file mode 100644 index 000000000..506acc5f8 --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/mock/test/tag_test.go @@ -0,0 +1,268 @@ +// Mock Driver Test of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// by CB-Spider Team, 2024.07. + +package mocktest + +import ( + mockdrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/mock" + idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" + irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" + + _ "fmt" + "testing" + + cblog "github.com/cloud-barista/cb-log" +) + +var tagHandler irs.TagHandler +var vpcTestHandler irs.VPCHandler +var vmTestHandler irs.VMHandler +var securityTestHandler irs.SecurityHandler +var keyPairTestHandler irs.KeyPairHandler + +func init() { + // make the log level lower to print clearly + cblog.SetLevel("error") + + cred := idrv.CredentialInfo{ + MockName: "MockDriver-01", + } + connInfo := idrv.ConnectionInfo{ + CredentialInfo: cred, + RegionInfo: idrv.RegionInfo{}, + } + cloudConn, _ := (&mockdrv.MockDriver{}).ConnectCloud(connInfo) + tagHandler, _ = cloudConn.CreateTagHandler() + vpcTestHandler, _ = cloudConn.CreateVPCHandler() + vmTestHandler, _ = cloudConn.CreateVMHandler() + securityTestHandler, _ = cloudConn.CreateSecurityHandler() + keyPairTestHandler, _ = cloudConn.CreateKeyPairHandler() +} + +type TagTestInfo struct { + ResType irs.RSType + ResID string + Key string + Value string +} + +var tagTestInfoList = []TagTestInfo{ + {ResType: irs.VM, ResID: "mock-vm-01", Key: "Environment", Value: "Dev"}, + {ResType: irs.VM, ResID: "mock-vm-02", Key: "Environment", Value: "Test"}, + {ResType: irs.VM, ResID: "mock-vm-03", Key: "Environment", Value: "Prod"}, + {ResType: irs.VM, ResID: "mock-vm-04", Key: "Owner", Value: "TeamA"}, + {ResType: irs.SUBNET, ResID: "mock-subnet-01", Key: "Environment", Value: "Dev"}, + {ResType: irs.SUBNET, ResID: "mock-subnet-02", Key: "Environment", Value: "Test"}, + {ResType: irs.VPC, ResID: "mock-vpc-01", Key: "Department", Value: "Finance"}, + {ResType: irs.SG, ResID: "mock-sg-01", Key: "Environment", Value: "Production"}, + {ResType: irs.KEY, ResID: "mock-keypair-01", Key: "Owner", Value: "Admin"}, +} + +type ResourceInfo struct { + IId string + ImageIID string + VpcIID string + SubnetIID string + SecurityGroupIIDs []string + VMSpecName string + KeyPairIID string +} + +var resourceInfoList = []ResourceInfo{ + {"mock-vm-01", "mock-img-01", "mock-vpc-01", "mock-subnet-01", []string{"mock-sg-01"}, "mock-vmspec-01", "mock-keypair-01"}, + {"mock-vm-02", "mock-img-01", "mock-vpc-01", "mock-subnet-01", []string{"mock-sg-01"}, "mock-vmspec-01", "mock-keypair-01"}, + {"mock-vm-03", "mock-img-01", "mock-vpc-01", "mock-subnet-01", []string{"mock-sg-01"}, "mock-vmspec-01", "mock-keypair-01"}, + {"mock-vm-04", "mock-img-01", "mock-vpc-01", "mock-subnet-01", []string{"mock-sg-01"}, "mock-vmspec-01", "mock-keypair-01"}, +} + +func TestSetup(t *testing.T) { + // Create VPC + vpcReqInfo := irs.VPCReqInfo{ + IId: irs.IID{NameId: "mock-vpc-01"}, + IPv4_CIDR: "192.168.0.0/16", + SubnetInfoList: []irs.SubnetInfo{ + {IId: irs.IID{NameId: "mock-subnet-01"}, IPv4_CIDR: "192.168.1.0/24"}, + {IId: irs.IID{NameId: "mock-subnet-02"}, IPv4_CIDR: "192.168.2.0/24"}, + }, + } + _, err := vpcTestHandler.CreateVPC(vpcReqInfo) + if err != nil { + t.Error(err.Error()) + } + + // Create Security Group + sgReqInfo := irs.SecurityReqInfo{ + IId: irs.IID{NameId: "mock-sg-01"}, + VpcIID: irs.IID{NameId: "mock-vpc-01"}, + SecurityRules: &[]irs.SecurityRuleInfo{{FromPort: "1", ToPort: "65535", IPProtocol: "tcp", Direction: "inbound"}}, + } + _, err = securityTestHandler.CreateSecurity(sgReqInfo) + if err != nil { + t.Error(err.Error()) + } + + // Create KeyPair + keypairReqInfo := irs.KeyPairReqInfo{ + IId: irs.IID{NameId: "mock-keypair-01"}, + } + _, err = keyPairTestHandler.CreateKey(keypairReqInfo) + if err != nil { + t.Error(err.Error()) + } + + // Create VM + for _, info := range resourceInfoList { + sgIIDs := []irs.IID{} + for _, sgIId := range info.SecurityGroupIIDs { + sgIIDs = append(sgIIDs, irs.IID{sgIId, ""}) + } + + vmReqInfo := irs.VMReqInfo{ + IId: irs.IID{info.IId, ""}, + + ImageIID: irs.IID{info.ImageIID, ""}, + VpcIID: irs.IID{info.VpcIID, ""}, + SubnetIID: irs.IID{info.SubnetIID, ""}, + SecurityGroupIIDs: sgIIDs, + + VMSpecName: info.VMSpecName, + KeyPairIID: irs.IID{info.KeyPairIID, ""}, + + VMUserId: "user01", + VMUserPasswd: "pass01", + } + _, err := vmTestHandler.StartVM(vmReqInfo) + if err != nil { + t.Error(err.Error()) + } + } + + // Check VM List + infoList, err := vmTestHandler.ListVM() + if err != nil { + t.Error(err.Error()) + } + if len(infoList) != len(resourceInfoList) { + t.Errorf("The number of Infos is not %d. It is %d.", len(resourceInfoList), len(infoList)) + } + for i, info := range infoList { + if info.IId.SystemId != resourceInfoList[i].IId { + t.Errorf("System ID %s is not same %s", info.IId.SystemId, resourceInfoList[i].IId) + } + } +} + +func TestTagCreateList(t *testing.T) { + // Create tags + for _, info := range tagTestInfoList { + tag := irs.KeyValue{Key: info.Key, Value: info.Value} + _, err := tagHandler.AddTag(info.ResType, irs.IID{NameId: info.ResID}, tag) + if err != nil { + t.Error(err.Error()) + } + } + + // Check the list size and values + for _, info := range tagTestInfoList { + tagList, err := tagHandler.ListTag(info.ResType, irs.IID{NameId: info.ResID}) + if err != nil { + t.Error(err.Error()) + } + found := false + for _, tag := range tagList { + if tag.Key == info.Key && tag.Value == info.Value { + found = true + break + } + } + if !found { + t.Errorf("Tag %s=%s not found for %s %s", info.Key, info.Value, info.ResType, info.ResID) + } + } +} + +func TestTagDeleteGet(t *testing.T) { + // Get & check the Value + info := tagTestInfoList[0] + tag, err := tagHandler.GetTag(info.ResType, irs.IID{NameId: info.ResID}, info.Key) + if err != nil { + t.Error(err.Error()) + } + if tag.Key != info.Key || tag.Value != info.Value { + t.Errorf("Tag %s=%s not found for %s %s", info.Key, info.Value, info.ResType, info.ResID) + } + + // Delete all + for _, info := range tagTestInfoList { + _, err := tagHandler.RemoveTag(info.ResType, irs.IID{NameId: info.ResID}, info.Key) + if err != nil { + t.Error(err.Error()) + } + } + + // Check the result of Delete Op + for _, info := range tagTestInfoList { + tagList, err := tagHandler.ListTag(info.ResType, irs.IID{NameId: info.ResID}) + if err != nil { + t.Error(err.Error()) + } + if len(tagList) > 0 { + t.Errorf("Tags are not deleted for %s %s", info.ResType, info.ResID) + } + } +} + +func TestCleanup(t *testing.T) { + // Terminate VMs + infoList, err := vmTestHandler.ListVM() + if err != nil { + t.Error(err.Error()) + } + for _, info := range infoList { + _, err := vmTestHandler.TerminateVM(info.IId) + if err != nil { + t.Error(err.Error()) + } + } + + // Delete Security Groups + sgList, err := securityTestHandler.ListSecurity() + if err != nil { + t.Error(err.Error()) + } + for _, info := range sgList { + _, err := securityTestHandler.DeleteSecurity(info.IId) + if err != nil { + t.Error(err.Error()) + } + } + + // Delete KeyPairs + keyList, err := keyPairTestHandler.ListKey() + if err != nil { + t.Error(err.Error()) + } + for _, info := range keyList { + _, err := keyPairTestHandler.DeleteKey(info.IId) + if err != nil { + t.Error(err.Error()) + } + } + + // Delete VPCs + vpcInfoList, err := vpcTestHandler.ListVPC() + if err != nil { + t.Error(err.Error()) + } + for _, info := range vpcInfoList { + _, err := vpcTestHandler.DeleteVPC(info.IId) + if err != nil { + t.Error(err.Error()) + } + } +} diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/test-priceinfo_test.go.sh b/cloud-control-manager/cloud-driver/drivers/mock/test/test-priceinfo_test.go.sh old mode 100644 new mode 100755 diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/test-tag_test.go.sh b/cloud-control-manager/cloud-driver/drivers/mock/test/test-tag_test.go.sh new file mode 100755 index 000000000..28d46cb1f --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/mock/test/test-tag_test.go.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go test tag_test.go diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/test-v-tag_test.go.sh b/cloud-control-manager/cloud-driver/drivers/mock/test/test-v-tag_test.go.sh new file mode 100755 index 000000000..83acaf81f --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/mock/test/test-v-tag_test.go.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go test -v tag_test.go diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/test-vm_test.go.sh b/cloud-control-manager/cloud-driver/drivers/mock/test/test-vm_test.go.sh new file mode 100755 index 000000000..a87a95125 --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/mock/test/test-vm_test.go.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go test vm_test.go diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/vm_test.go b/cloud-control-manager/cloud-driver/drivers/mock/test/vm_test.go index b86b85ad8..d08e2471e 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/test/vm_test.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/test/vm_test.go @@ -9,19 +9,20 @@ package mocktest import ( - idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" mockdrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/mock" + idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" "testing" + cblog "github.com/cloud-barista/cb-log" ) var vmHandler irs.VMHandler func init() { - // make the log level lower to print clearly - cblog.SetLevel("error") + // make the log level lower to print clearly + cblog.SetLevel("error") cred := idrv.CredentialInfo{ MockName: "MockDriver-77", // *** 주의 *** : 다른 테스트의 데이터와 충돌 방지를 위해 별도 이름 지정 diff --git a/cloud-control-manager/cloud-driver/drivers/mock/test/vpc_test.go b/cloud-control-manager/cloud-driver/drivers/mock/test/vpc_test.go index 2a2e0ac84..09f6a55e3 100644 --- a/cloud-control-manager/cloud-driver/drivers/mock/test/vpc_test.go +++ b/cloud-control-manager/cloud-driver/drivers/mock/test/vpc_test.go @@ -16,14 +16,15 @@ import ( _ "fmt" "testing" - cblog "github.com/cloud-barista/cb-log" + + cblog "github.com/cloud-barista/cb-log" ) var vpcHandler irs.VPCHandler func init() { - // make the log level lower to print clearly - cblog.SetLevel("error") + // make the log level lower to print clearly + cblog.SetLevel("error") cred := idrv.CredentialInfo{ MockName: "MockDriver-01", @@ -33,7 +34,7 @@ func init() { RegionInfo: idrv.RegionInfo{}, } cloudConn, _ := (&mockdrv.MockDriver{}).ConnectCloud(connInfo) - vpcHandler, _ = cloudConn.CreateVPCHandler() + vpcTestHandler, _ = cloudConn.CreateVPCHandler() } type VPCTestInfo struct { @@ -70,14 +71,14 @@ func TestVPCCreateList(t *testing.T) { IPv4_CIDR: info.VpcCIDR, SubnetInfoList: []irs.SubnetInfo{{IId: irs.IID{info.SubnetIID, ""}, IPv4_CIDR: info.SubnetCIDR}}, } - _, err := vpcHandler.CreateVPC(reqInfo) + _, err := vpcTestHandler.CreateVPC(reqInfo) if err != nil { t.Error(err.Error()) } } // check the list size and values - infoList, err := vpcHandler.ListVPC() + infoList, err := vpcTestHandler.ListVPC() if err != nil { t.Error(err.Error()) } @@ -99,14 +100,14 @@ func TestVPCAddSubnet(t *testing.T) { IId: irs.IID{info.SubnetIID, ""}, IPv4_CIDR: info.SubnetCIDR, } - _, err := vpcHandler.AddSubnet(irs.IID{info.VpcIID, ""}, subnetInfo) + _, err := vpcTestHandler.AddSubnet(irs.IID{info.VpcIID, ""}, subnetInfo) if err != nil { t.Error(err.Error()) } } // check the result of two AddSubnet() - info, err := vpcHandler.GetVPC(irs.IID{subnetTestInfoList[0].VpcIID, ""}) + info, err := vpcTestHandler.GetVPC(irs.IID{subnetTestInfoList[0].VpcIID, ""}) if err != nil { t.Error(err.Error()) } @@ -125,14 +126,14 @@ func TestVPCAddSubnet(t *testing.T) { func TestVPCRemoveSubnet(t *testing.T) { // remove subnet for _, info := range subnetTestInfoList { - _, err := vpcHandler.RemoveSubnet(irs.IID{info.VpcIID, ""}, irs.IID{"", info.SubnetIID}) + _, err := vpcTestHandler.RemoveSubnet(irs.IID{info.VpcIID, ""}, irs.IID{"", info.SubnetIID}) if err != nil { t.Error(err.Error()) } } // check the result of two RemoveSubnet() - info, err := vpcHandler.GetVPC(irs.IID{subnetTestInfoList[0].VpcIID, ""}) + info, err := vpcTestHandler.GetVPC(irs.IID{subnetTestInfoList[0].VpcIID, ""}) if err != nil { t.Error(err.Error()) } @@ -150,7 +151,7 @@ func TestVPCRemoveSubnet(t *testing.T) { func TestVPCDeleteGet(t *testing.T) { // Get & check the Value - info, err := vpcHandler.GetVPC(irs.IID{vpcTestInfoList[0].IId, ""}) + info, err := vpcTestHandler.GetVPC(irs.IID{vpcTestInfoList[0].IId, ""}) if err != nil { t.Error(err.Error()) } @@ -159,12 +160,12 @@ func TestVPCDeleteGet(t *testing.T) { } // delete all - infoList, err := vpcHandler.ListVPC() + infoList, err := vpcTestHandler.ListVPC() if err != nil { t.Error(err.Error()) } for _, info := range infoList { - ret, err := vpcHandler.DeleteVPC(info.IId) + ret, err := vpcTestHandler.DeleteVPC(info.IId) if err != nil { t.Error(err.Error()) } @@ -173,7 +174,7 @@ func TestVPCDeleteGet(t *testing.T) { } } // check the result of Delete Op - infoList, err = vpcHandler.ListVPC() + infoList, err = vpcTestHandler.ListVPC() if err != nil { t.Error(err.Error()) } From bd303adb125fe265e8233cea3aed28307b0aba1b Mon Sep 17 00:00:00 2001 From: powerkimhub Date: Tue, 23 Jul 2024 22:55:30 +0900 Subject: [PATCH 34/56] Remove unused Environments and Change the images path to copy all images in it --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 40f5d98b4..d8e5d1c7f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ COPY --from=builder /go/src/github.com/cloud-barista/cb-spider/conf/ /root/go/sr COPY --from=builder /go/src/github.com/cloud-barista/cb-spider/api-runtime/cb-spider /root/go/src/github.com/cloud-barista/cb-spider/api-runtime/ -COPY --from=builder /go/src/github.com/cloud-barista/cb-spider/api-runtime/rest-runtime/admin-web/images/cb-spider-circle-logo.png /root/go/src/github.com/cloud-barista/cb-spider/api-runtime/rest-runtime/admin-web/images/ +COPY --from=builder /go/src/github.com/cloud-barista/cb-spider/api-runtime/rest-runtime/admin-web/images/ /root/go/src/github.com/cloud-barista/cb-spider/api-runtime/rest-runtime/admin-web/images/ COPY --from=builder /go/src/github.com/cloud-barista/cb-spider/api-runtime/rest-runtime/admin-web/html/ /root/go/src/github.com/cloud-barista/cb-spider/api-runtime/rest-runtime/admin-web/html/ @@ -52,10 +52,8 @@ COPY --from=builder /go/src/github.com/cloud-barista/cb-spider/api-runtime/rest- ENV CBSPIDER_ROOT /root/go/src/github.com/cloud-barista/cb-spider ENV CBLOG_ROOT /root/go/src/github.com/cloud-barista/cb-spider ENV PLUGIN_SW OFF -ENV MEERKAT OFF ENTRYPOINT [ "/root/go/src/github.com/cloud-barista/cb-spider/api-runtime/cb-spider" ] EXPOSE 1024 EXPOSE 2048 -EXPOSE 4096 From 460fa14d36bb6aeb857f8477b8b9aa851ba5bd76 Mon Sep 17 00:00:00 2001 From: powerkimhub Date: Tue, 23 Jul 2024 23:33:37 +0900 Subject: [PATCH 35/56] Add new Connection Managements Part of AdminWeb2 --- api-runtime/rest-runtime/CBSpiderRuntime.go | 21 +- api-runtime/rest-runtime/CIMRest.go | 63 +- .../rest-runtime/admin-web/AdminWeb-CIM.go | 536 ------------- .../admin-web/AdminWeb-Connection.go | 733 ++++++++++++++++++ .../admin-web/AdminWeb-Credential.go | 102 +++ .../admin-web/AdminWeb-Dashboard.go | 72 +- .../rest-runtime/admin-web/AdminWeb-Driver.go | 205 +++++ .../rest-runtime/admin-web/AdminWeb-Main.go | 46 ++ .../rest-runtime/admin-web/AdminWeb-Region.go | 89 +++ .../admin-web/html/body_frame.html | 103 +++ .../admin-web/html/connection.html | 584 ++++++++++++++ .../admin-web/html/credential.html | 515 ++++++++++++ .../rest-runtime/admin-web/html/driver.html | 356 +++++++++ .../admin-web/html/left_menu.html | 151 ++++ .../rest-runtime/admin-web/html/main.html | 312 ++++++++ .../rest-runtime/admin-web/html/region.html | 710 +++++++++++++++++ .../rest-runtime/admin-web/images/alibaba.png | Bin 0 -> 8386 bytes .../rest-runtime/admin-web/images/aws.png | Bin 0 -> 9206 bytes .../rest-runtime/admin-web/images/azure.png | Bin 0 -> 9827 bytes .../images/cb-spider-circle-logo.png | Bin 8545 -> 0 bytes .../admin-web/images/connection.png | Bin 0 -> 12230 bytes .../admin-web/images/connection_small.png | Bin 0 -> 8685 bytes .../rest-runtime/admin-web/images/gcp.png | Bin 0 -> 11521 bytes .../rest-runtime/admin-web/images/ibm.png | Bin 0 -> 17426 bytes .../rest-runtime/admin-web/images/ktcloud.png | Bin 0 -> 9789 bytes .../admin-web/images/ktcloudvpc.png | Bin 0 -> 6908 bytes .../admin-web/images/left-menu/cluster.png | Bin 0 -> 12221 bytes .../images/left-menu/cluster_selected.png | Bin 0 -> 11688 bytes .../admin-web/images/left-menu/connection.png | Bin 0 -> 8745 bytes .../images/left-menu/connection_selected.png | Bin 0 -> 8382 bytes .../admin-web/images/left-menu/dashboard.png | Bin 0 -> 8737 bytes .../images/left-menu/dashboard_selected.png | Bin 0 -> 8739 bytes .../admin-web/images/left-menu/disk.png | Bin 0 -> 4031 bytes .../images/left-menu/disk_selected.png | Bin 0 -> 3950 bytes .../admin-web/images/left-menu/keypair.png | Bin 0 -> 5400 bytes .../images/left-menu/keypair_selected.png | Bin 0 -> 5408 bytes .../admin-web/images/left-menu/myimage.png | Bin 0 -> 6693 bytes .../images/left-menu/myimage_selected.png | Bin 0 -> 6562 bytes .../admin-web/images/left-menu/nlb.png | Bin 0 -> 4193 bytes .../images/left-menu/nlb_selected.png | Bin 0 -> 4449 bytes .../images/left-menu/securitygroup.png | Bin 0 -> 5460 bytes .../left-menu/securitygroup_selected.png | Bin 0 -> 5320 bytes .../admin-web/images/left-menu/vm.png | Bin 0 -> 6354 bytes .../images/left-menu/vm_selected.png | Bin 0 -> 6165 bytes .../admin-web/images/left-menu/vpc.png | Bin 0 -> 6992 bytes .../images/left-menu/vpc_selected.png | Bin 0 -> 6820 bytes .../rest-runtime/admin-web/images/logo.png | Bin 0 -> 8477 bytes .../rest-runtime/admin-web/images/mgmt.png | Bin 0 -> 3613 bytes .../rest-runtime/admin-web/images/mock.png | Bin 0 -> 7862 bytes .../rest-runtime/admin-web/images/ncp.png | Bin 0 -> 10611 bytes .../rest-runtime/admin-web/images/ncpvpc.png | Bin 0 -> 7603 bytes .../admin-web/images/nhncloud.png | Bin 0 -> 6707 bytes .../admin-web/images/openstack.png | Bin 0 -> 11061 bytes .../rest-runtime/admin-web/images/tencent.png | Bin 0 -> 30104 bytes .../admin-web/images/top-menu/price.png | Bin 0 -> 5884 bytes .../images/top-menu/price_selected.png | Bin 0 -> 6141 bytes .../admin-web/images/top-menu/region-zone.png | Bin 0 -> 5014 bytes .../images/top-menu/region-zone_selected.png | Bin 0 -> 4775 bytes .../admin-web/images/top-menu/vm-image.png | Bin 0 -> 4919 bytes .../images/top-menu/vm-image_selected.png | Bin 0 -> 4762 bytes .../admin-web/images/top-menu/vm-specs.png | Bin 0 -> 5112 bytes .../images/top-menu/vm-specs_selected.png | Bin 0 -> 4842 bytes 62 files changed, 3995 insertions(+), 603 deletions(-) create mode 100644 api-runtime/rest-runtime/admin-web/AdminWeb-Connection.go create mode 100644 api-runtime/rest-runtime/admin-web/AdminWeb-Credential.go create mode 100644 api-runtime/rest-runtime/admin-web/AdminWeb-Driver.go create mode 100644 api-runtime/rest-runtime/admin-web/AdminWeb-Main.go create mode 100644 api-runtime/rest-runtime/admin-web/AdminWeb-Region.go create mode 100644 api-runtime/rest-runtime/admin-web/html/body_frame.html create mode 100644 api-runtime/rest-runtime/admin-web/html/connection.html create mode 100644 api-runtime/rest-runtime/admin-web/html/credential.html create mode 100644 api-runtime/rest-runtime/admin-web/html/driver.html create mode 100644 api-runtime/rest-runtime/admin-web/html/left_menu.html create mode 100644 api-runtime/rest-runtime/admin-web/html/main.html create mode 100644 api-runtime/rest-runtime/admin-web/html/region.html create mode 100644 api-runtime/rest-runtime/admin-web/images/alibaba.png create mode 100644 api-runtime/rest-runtime/admin-web/images/aws.png create mode 100644 api-runtime/rest-runtime/admin-web/images/azure.png delete mode 100755 api-runtime/rest-runtime/admin-web/images/cb-spider-circle-logo.png create mode 100644 api-runtime/rest-runtime/admin-web/images/connection.png create mode 100644 api-runtime/rest-runtime/admin-web/images/connection_small.png create mode 100644 api-runtime/rest-runtime/admin-web/images/gcp.png create mode 100644 api-runtime/rest-runtime/admin-web/images/ibm.png create mode 100644 api-runtime/rest-runtime/admin-web/images/ktcloud.png create mode 100644 api-runtime/rest-runtime/admin-web/images/ktcloudvpc.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/cluster.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/cluster_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/connection.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/connection_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/dashboard.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/dashboard_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/disk.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/disk_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/keypair.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/keypair_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/myimage.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/myimage_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/nlb.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/nlb_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/securitygroup.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/securitygroup_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/vm.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/vm_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/vpc.png create mode 100644 api-runtime/rest-runtime/admin-web/images/left-menu/vpc_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/logo.png create mode 100644 api-runtime/rest-runtime/admin-web/images/mgmt.png create mode 100644 api-runtime/rest-runtime/admin-web/images/mock.png create mode 100644 api-runtime/rest-runtime/admin-web/images/ncp.png create mode 100644 api-runtime/rest-runtime/admin-web/images/ncpvpc.png create mode 100644 api-runtime/rest-runtime/admin-web/images/nhncloud.png create mode 100644 api-runtime/rest-runtime/admin-web/images/openstack.png create mode 100644 api-runtime/rest-runtime/admin-web/images/tencent.png create mode 100644 api-runtime/rest-runtime/admin-web/images/top-menu/price.png create mode 100644 api-runtime/rest-runtime/admin-web/images/top-menu/price_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/top-menu/region-zone.png create mode 100644 api-runtime/rest-runtime/admin-web/images/top-menu/region-zone_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/top-menu/vm-image.png create mode 100644 api-runtime/rest-runtime/admin-web/images/top-menu/vm-image_selected.png create mode 100644 api-runtime/rest-runtime/admin-web/images/top-menu/vm-specs.png create mode 100644 api-runtime/rest-runtime/admin-web/images/top-menu/vm-specs_selected.png diff --git a/api-runtime/rest-runtime/CBSpiderRuntime.go b/api-runtime/rest-runtime/CBSpiderRuntime.go index 22529faa7..a8fb70ba5 100644 --- a/api-runtime/rest-runtime/CBSpiderRuntime.go +++ b/api-runtime/rest-runtime/CBSpiderRuntime.go @@ -11,6 +11,7 @@ package restruntime import ( "crypto/subtle" "fmt" + "path/filepath" "strings" "time" @@ -434,6 +435,12 @@ func RunServer() { {"GET", "/countcluster", CountAllClusters}, {"GET", "/countcluster/:ConnectionName", CountClustersByConnection}, + //----------Tag Handler + {"POST", "/tag", AddTag}, + {"GET", "/tag", ListTag}, + {"GET", "/tag/:Name", GetTag}, + {"DELETE", "/tag/:Name", RemoveTag}, + //----------Destory All Resources in a Connection {"DELETE", "/destroy", Destroy}, @@ -459,12 +466,24 @@ func RunServer() { {"GET", "/adminweb/top", aw.Top}, {"GET", "/adminweb/log", aw.Log}, + {"GET", "/adminweb2", aw.MainPage}, + {"GET", "/adminweb2/", aw.MainPage}, + {"GET", "/adminweb/left_menu", aw.LeftMenu}, + {"GET", "/adminweb/body_frame", aw.BodyFrame}, + {"GET", "/adminweb/dashboard", aw.Dashboard}, {"GET", "/adminweb/driver", aw.Driver}, + {"GET", "/adminweb2/driver", aw.DriverManagement}, + {"GET", "/adminweb/credential", aw.Credential}, + {"GET", "/adminweb2/credential", aw.CredentialManagement}, + {"GET", "/adminweb/region", aw.Region}, + {"GET", "/adminweb2/region", aw.RegionManagement}, + {"GET", "/adminweb/connectionconfig", aw.Connectionconfig}, + {"GET", "/adminweb2/connectionconfig", aw.ConnectionManagement}, {"GET", "/adminweb/dashboard", aw.Dashboard}, @@ -568,7 +587,7 @@ func ApiServer(routes []route) { } // for spider logo - e.File("/spider/adminweb/images/logo.png", cbspiderRoot+"/api-runtime/rest-runtime/admin-web/images/cb-spider-circle-logo.png") + e.Static("/spider/adminweb/images", filepath.Join(cbspiderRoot, "api-runtime/rest-runtime/admin-web/images")) // for admin-web e.File("/spider/adminweb/html/priceinfo-filter-gen.html", cbspiderRoot+"/api-runtime/rest-runtime/admin-web/html/priceinfo-filter-gen.html") diff --git a/api-runtime/rest-runtime/CIMRest.go b/api-runtime/rest-runtime/CIMRest.go index 38fea5b9f..e08227f3b 100644 --- a/api-runtime/rest-runtime/CIMRest.go +++ b/api-runtime/rest-runtime/CIMRest.go @@ -76,9 +76,24 @@ func RegisterCloudDriver(c echo.Context) error { func ListCloudDriver(c echo.Context) error { cblog.Info("call ListCloudDriver()") - infoList, err := dim.ListCloudDriver() - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + var providerName string + providerName = c.QueryParam("provider") + if providerName == "" { + providerName = c.QueryParam("ProviderName") + } + + infoList := []*dim.CloudDriverInfo{} + var err error + if providerName != "" { + infoList, err = dim.ListCloudDriverByProvider(providerName) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } else { + infoList, err = dim.ListCloudDriver() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } } var jsonResult struct { @@ -166,9 +181,24 @@ func RegisterCredential(c echo.Context) error { func ListCredential(c echo.Context) error { cblog.Info("call ListCredential()") - infoList, err := cim.ListCredential() - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + var providerName string + providerName = c.QueryParam("provider") + if providerName == "" { + providerName = c.QueryParam("ProviderName") + } + + infoList := []*cim.CredentialInfo{} + var err error + if providerName != "" { + infoList, err = cim.ListCredentialByProvider(providerName) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } else { + infoList, err = cim.ListCredential() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } } var jsonResult struct { @@ -227,9 +257,24 @@ func RegisterRegion(c echo.Context) error { func ListRegion(c echo.Context) error { cblog.Info("call ListRegion()") - infoList, err := rim.ListRegion() - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + var providerName string + providerName = c.QueryParam("provider") + if providerName == "" { + providerName = c.QueryParam("ProviderName") + } + + infoList := []*rim.RegionInfo{} + var err error + if providerName != "" { + infoList, err = rim.ListRegionByProvider(providerName) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } else { + infoList, err = rim.ListRegion() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } } var jsonResult struct { diff --git a/api-runtime/rest-runtime/admin-web/AdminWeb-CIM.go b/api-runtime/rest-runtime/admin-web/AdminWeb-CIM.go index ed71d5ee4..98ad71115 100644 --- a/api-runtime/rest-runtime/admin-web/AdminWeb-CIM.go +++ b/api-runtime/rest-runtime/admin-web/AdminWeb-CIM.go @@ -932,153 +932,6 @@ func Region(c echo.Context) error { return c.HTML(http.StatusOK, htmlStr) } -// make the string of javascript function -func makeOnInitialInputBoxSetup_js() string { - strFunc := ` - function onInitialSetup() { - cspSelect = document.getElementById('1') - onchangeProvider(cspSelect) - } - ` - return strFunc -} - -// make the string of javascript function -func makeOnchangeConnectionConfigProviderFunc_js() string { - strFunc := ` - function onchangeProvider(source) { - var providerName = source.value - // for credential info - var driverNameList = [] - var credentialNameList - var regionNameList - switch(providerName) { - case "AWS": - driverNameList = document.getElementsByName('driverName-AWS'); - credentialNameList = document.getElementsByName('credentialName-AWS'); - regionNameList = document.getElementsByName('regionName-AWS'); - break; - case "AZURE": - driverNameList = document.getElementsByName('driverName-AZURE'); - credentialNameList = document.getElementsByName('credentialName-AZURE'); - regionNameList = document.getElementsByName('regionName-AZURE'); - break; - case "GCP": - driverNameList = document.getElementsByName('driverName-GCP'); - credentialNameList = document.getElementsByName('credentialName-GCP'); - regionNameList = document.getElementsByName('regionName-GCP'); - break; - case "ALIBABA": - driverNameList = document.getElementsByName('driverName-ALIBABA'); - credentialNameList = document.getElementsByName('credentialName-ALIBABA'); - regionNameList = document.getElementsByName('regionName-ALIBABA'); - break; - case "TENCENT": - driverNameList = document.getElementsByName('driverName-TENCENT'); - credentialNameList = document.getElementsByName('credentialName-TENCENT'); - regionNameList = document.getElementsByName('regionName-TENCENT'); - break; - case "IBM": - driverNameList = document.getElementsByName('driverName-IBM'); - credentialNameList = document.getElementsByName('credentialName-IBM'); - regionNameList = document.getElementsByName('regionName-IBM'); - break; - case "OPENSTACK": - driverNameList = document.getElementsByName('driverName-OPENSTACK'); - credentialNameList = document.getElementsByName('credentialName-OPENSTACK'); - regionNameList = document.getElementsByName('regionName-OPENSTACK'); - break; - case "CLOUDIT": - driverNameList = document.getElementsByName('driverName-CLOUDIT'); - credentialNameList = document.getElementsByName('credentialName-CLOUDIT'); - regionNameList = document.getElementsByName('regionName-CLOUDIT'); - break; - case "DOCKER": - driverNameList = document.getElementsByName('driverName-DOCKER'); - credentialNameList = document.getElementsByName('credentialName-DOCKER'); - regionNameList = document.getElementsByName('regionName-DOCKER'); - break; - - case "NCPVPC": - driverNameList = document.getElementsByName('driverName-NCPVPC'); - credentialNameList = document.getElementsByName('credentialName-NCPVPC'); - regionNameList = document.getElementsByName('regionName-NCPVPC'); - break; - case "NCP": - driverNameList = document.getElementsByName('driverName-NCP'); - credentialNameList = document.getElementsByName('credentialName-NCP'); - regionNameList = document.getElementsByName('regionName-NCP'); - break; - case "NHNCLOUD": - driverNameList = document.getElementsByName('driverName-NHNCLOUD'); - credentialNameList = document.getElementsByName('credentialName-NHNCLOUD'); - regionNameList = document.getElementsByName('regionName-NHNCLOUD'); - break; - case "KTCLOUD": - driverNameList = document.getElementsByName('driverName-KTCLOUD'); - credentialNameList = document.getElementsByName('credentialName-KTCLOUD'); - regionNameList = document.getElementsByName('regionName-KTCLOUD'); - break; - case "KTCLOUDVPC": - driverNameList = document.getElementsByName('driverName-KTCLOUDVPC'); - credentialNameList = document.getElementsByName('credentialName-KTCLOUDVPC'); - regionNameList = document.getElementsByName('regionName-KTCLOUDVPC'); - break; - - case "MOCK": - driverNameList = document.getElementsByName('driverName-MOCK'); - credentialNameList = document.getElementsByName('credentialName-MOCK'); - regionNameList = document.getElementsByName('regionName-MOCK'); - break; - case "CLOUDTWIN": - driverNameList = document.getElementsByName('driverName-CLOUDTWIN'); - credentialNameList = document.getElementsByName('credentialName-CLOUDTWIN'); - regionNameList = document.getElementsByName('regionName-CLOUDTWIN'); - break; - default: - driverNameList = document.getElementsByName('driverName-AWS'); - credentialNameList = document.getElementsByName('credentialName-AWS'); - regionNameList = document.getElementsByName('regionName-AWS'); - } - - // Select Tag for drivers - // options remove & create - var len = document.getElementById('2').options.length - for (var i=0; i < len; i++) { - document.getElementById('2').remove(0); - } - for (var i=0; i < driverNameList.length; i++) { - document.getElementById('2').options.add(new Option(driverNameList[i].innerHTML, driverNameList[i].innerHTML)); - } - - // Select Tag for Credentials - // options remove & create - var len = document.getElementById('3').options.length - for (var i=0; i < len; i++) { - document.getElementById('3').remove(0); - } - for (var i=0; i < credentialNameList.length; i++) { - document.getElementById('3').options.add(new Option(credentialNameList[i].innerHTML, credentialNameList[i].innerHTML)); - } - - // Select Tag for Regions - // options remove & create - var len = document.getElementById('4').options.length - for (var i=0; i < len; i++) { - document.getElementById('4').remove(0); - } - for (var i=0; i < regionNameList.length; i++) { - document.getElementById('4').options.add(new Option(regionNameList[i].innerHTML, regionNameList[i].innerHTML)); - } - - //document.getElementById('5').value= providerName.toLowerCase() + "-" + document.getElementById('4').value + "-connection-config-01"; - document.getElementById('5').value= providerName.toLowerCase() + "-config-01"; - - } - ` - return strFunc -} - func getProviderName(connConfig string) (string, error) { resBody, err := getResource_JsonByte("connectionconfig", connConfig) if err != nil { @@ -1131,238 +984,6 @@ func getRegionZone(regionName string) (string, string, error) { return region, zone, nil } -// make the string of javascript function -func makeSetupConnectionConfigFunc_js() string { - - strFunc := ` - function setupConnectionConfig(configName, providerName, region, zone) { - var connConfigLabel = parent.frames["top_frame"].document.getElementById("connConfig"); - connConfigLabel.innerHTML = configName - - var cspText = parent.frames["top_frame"].document.getElementById("connDisplay"); - if (zone) { - cspText.value = providerName + ": " + region + " / " + zone - } else { - cspText.value = providerName + ": " + region - } - - // for vpc - var a = parent.frames["top_frame"].document.getElementById("vpcHref"); - a.href = "vpc/" + configName - a = parent.frames["top_frame"].document.getElementById("vpcmgmtHref"); - a.href = "vpcmgmt/" + configName - - // for securitygroup - a = parent.frames["top_frame"].document.getElementById("securitygroupHref"); - a.href = "securitygroup/" + configName - a = parent.frames["top_frame"].document.getElementById("securitygroupmgmtHref"); - a.href = "securitygroupmgmt/" + configName - - // for KeyPair - a = parent.frames["top_frame"].document.getElementById("keypairHref"); - a.href = "keypair/" + configName - a = parent.frames["top_frame"].document.getElementById("keypairmgmtHref"); - a.href = "keypairmgmt/" + configName - - // for vm - a = parent.frames["top_frame"].document.getElementById("vmHref"); - a.href = "vm/" + configName - a = parent.frames["top_frame"].document.getElementById("vmmgmtHref"); - a.href = "vmmgmt/" + configName - - // for nlb - a = parent.frames["top_frame"].document.getElementById("nlbHref"); - a.href = "nlb/" + configName - a = parent.frames["top_frame"].document.getElementById("nlbmgmtHref"); - a.href = "nlbmgmt/" + configName - - // for disk - a = parent.frames["top_frame"].document.getElementById("diskHref"); - a.href = "disk/" + configName - a = parent.frames["top_frame"].document.getElementById("diskmgmtHref"); - a.href = "diskmgmt/" + configName - - // for myimage - a = parent.frames["top_frame"].document.getElementById("myimageHref"); - a.href = "myimage/" + configName - a = parent.frames["top_frame"].document.getElementById("myimagemgmtHref"); - a.href = "myimagemgmt/" + configName - - // for VMImage - a = parent.frames["top_frame"].document.getElementById("vmimageHref"); - a.href = "vmimage/" + configName - - // for VMSpec - a = parent.frames["top_frame"].document.getElementById("vmspecHref"); - a.href = "vmspec/" + configName - - // for RegionZone - a = parent.frames["top_frame"].document.getElementById("regionzoneHref"); - a.href = "regionzone/" + configName - - // for Price - a = parent.frames["top_frame"].document.getElementById("priceinfoHref"); - a.href = "priceinfo/" + configName - - // for Cluster(PMKS) - a = parent.frames["top_frame"].document.getElementById("clusterHref"); - a.href = "cluster/" + configName - a = parent.frames["top_frame"].document.getElementById("clustermgmtHref"); - a.href = "clustermgmt/" + configName - } - ` - return strFunc -} - -// number, Provider Name, Driver Name, Credential Name, Region Name, Connection Name, checkbox -func makeConnectionConfigTRList_html(bgcolor string, height string, fontSize string, infoList []*ccim.ConnectionConfigInfo) (string, error) { - if bgcolor == "" { - bgcolor = "#FFFFFF" - } - if height == "" { - height = "30" - } - if fontSize == "" { - fontSize = "2" - } - - // make base TR frame for info list - strTR := fmt.Sprintf(` - - - $$NUM$$ - - - $$PROVIDERNAME$$ - - - $$S2$$ - - - $$S3$$ - - - $$S4$$ - - - - $$CONFIGNAME$$ - - - - - - - `, bgcolor, height, fontSize, fontSize, fontSize, fontSize, fontSize, fontSize) - - strData := "" - // set data and make TR list - for i, one := range infoList { - str := strings.ReplaceAll(strTR, "$$NUM$$", strconv.Itoa(i+1)) - str = strings.ReplaceAll(str, "$$PROVIDERNAME$$", one.ProviderName) - str = strings.ReplaceAll(str, "$$S2$$", one.DriverName) - str = strings.ReplaceAll(str, "$$S3$$", one.CredentialName) - str = strings.ReplaceAll(str, "$$S4$$", one.RegionName) - str = strings.ReplaceAll(str, "$$CONFIGNAME$$", one.ConfigName) - - region, zone, err := getRegionZone(one.RegionName) - if err != nil { - cblog.Error(err) - return "", err - } - str = strings.ReplaceAll(str, "$$REGION$$", region) - str = strings.ReplaceAll(str, "$$ZONE$$", zone) - - strData += str - } - - return strData, nil -} - -// make the string of javascript function -func makePostConnectionConfigFunc_js() string { - - // curl -X POST http://$RESTSERVER:1024/spider/connectionconfig -H 'Content-Type: application/json' - // -d '{"ProviderName":"AWS", "DriverName":"aws-driver01", "CredentialName":"aws-credential-01", "RegionName":"aws-ohio", "ConfigName":"aws-ohio-config",}' - - strFunc := ` - function postConnectionConfig() { - var textboxes = document.getElementsByName('text_box'); - sendJson = '{ "ProviderName" : "$$PROVIDER$$", "DriverName" : "$$DRIVERNAME$$", "CredentialName" : "$$CREDENTIALNAME$$", \ - "RegionName" : "$$REGIONNAME$$", "ConfigName" : "$$NAME$$" }' - - for (var i = 0; i < textboxes.length; i++) { // @todo make parallel executions - switch (textboxes[i].id) { - case "1": - sendJson = sendJson.replace("$$PROVIDER$$", textboxes[i].value); - break; - case "2": - sendJson = sendJson.replace("$$DRIVERNAME$$", textboxes[i].value); - break; - case "3": - sendJson = sendJson.replace("$$CREDENTIALNAME$$", textboxes[i].value); - break; - case "4": - sendJson = sendJson.replace("$$REGIONNAME$$", textboxes[i].value); - break; - case "5": - sendJson = sendJson.replace("$$NAME$$", textboxes[i].value); - break; - default: - break; - } - } - var xhr = new XMLHttpRequest(); - xhr.open("POST", "$$SPIDER_SERVER$$/spider/connectionconfig", false); - xhr.setRequestHeader('Content-Type', 'application/json'); - - // client logging - parent.frames["log_frame"].Log("curl -sX POST " + "$$SPIDER_SERVER$$/spider/connectionconfig -H 'Content-Type: application/json' -d '" + sendJson + "'"); - - xhr.send(sendJson); - - // client logging - parent.frames["log_frame"].Log(" => " + xhr.response); - - // setTimeout(function(){ // when async call - location.reload(); - // }, 400); - - } - ` - strFunc = strings.ReplaceAll(strFunc, "$$SPIDER_SERVER$$", "http://"+cr.ServiceIPorName+cr.ServicePort) // cr.ServicePort = ":1024" - return strFunc -} - -// make the string of javascript function -func makeDeleteConnectionConfigFunc_js() string { - // curl -X DELETE http://$RESTSERVER:1024/spider/connectionconfig/aws-connection01 -H 'Content-Type: application/json' - - strFunc := ` - function deleteConnectionConfig() { - var checkboxes = document.getElementsByName('check_box'); - for (var i = 0; i < checkboxes.length; i++) { // @todo make parallel executions - if (checkboxes[i].checked) { - var xhr = new XMLHttpRequest(); - xhr.open("DELETE", "$$SPIDER_SERVER$$/spider/connectionconfig/" + checkboxes[i].value, false); - xhr.setRequestHeader('Content-Type', 'application/json'); - - // client logging - parent.frames["log_frame"].Log("curl -sX DELETE " + "$$SPIDER_SERVER$$/spider/connectionconfig/" + checkboxes[i].value + " -H 'Content-Type: application/json'" ); - - xhr.send(null); - - // client logging - parent.frames["log_frame"].Log(" => " + xhr.response); - } - } - location.reload(); - } - ` - strFunc = strings.ReplaceAll(strFunc, "$$SPIDER_SERVER$$", "http://"+cr.ServiceIPorName+cr.ServicePort) // cr.ServicePort = ":1024" - return strFunc -} - func makeDriverNameHiddenTRList_html(infoList []*dim.CloudDriverInfo) string { // make base Label frame for info list @@ -1411,163 +1032,6 @@ func makeRegionNameHiddenTRList_html(infoList []*rim.RegionInfo) string { return strData } -// ================ Connection Config Info Management -// create Connection page -func Connectionconfig(c echo.Context) error { - cblog.Info("call Connectionconfig()") - - // make page header - htmlStr := ` - - - - - - - - - ` - - // (2) make Table Action TR - // colspan, f5_href, delete_href, fontSize - htmlStr += makeActionTR_html("7", "connectionconfig", "deleteConnectionConfig()", "2") - - // (3) make Table Header TR - nameWidthList := []NameWidth{ - {"Provider Name", "200"}, - {"Driver Name", "200"}, - {"Credential Name", "200"}, - {"Region Name", "200"}, - {"Connection Config Name", "200"}, - } - htmlStr += makeTitleTRList_html("#DDDDDD", "2", nameWidthList, true) - - // (4) make TR list with info list - // (4-1) get info list @todo if empty list - - // client logging - htmlStr += genLoggingGETResURL("connectionconfig") - - resBody, err := getResourceList_JsonByte("connectionconfig") - if err != nil { - cblog.Error(err) - // client logging - htmlStr += genLoggingGETResURL(err.Error()) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - // client logging - htmlStr += genLoggingResult(string(resBody[:len(resBody)-1])) - - var info struct { - ResultList []*ccim.ConnectionConfigInfo `json:"connectionconfig"` - } - json.Unmarshal(resBody, &info) - - // (4-2) make TR list with info list - trStrList, err := makeConnectionConfigTRList_html("", "", "", info.ResultList) - if err != nil { - cblog.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - htmlStr += trStrList - - // (4-3) make hidden TR list with info list - // (a) Driver Name Hidden List - resBody, err = getResourceList_JsonByte("driver") - if err != nil { - cblog.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - var driverInfo struct { - ResultList []*dim.CloudDriverInfo `json:"driver"` - } - json.Unmarshal(resBody, &driverInfo) - htmlStr += makeDriverNameHiddenTRList_html(driverInfo.ResultList) - - // (b) Credential Name Hidden List - resBody, err = getResourceList_JsonByte("credential") - if err != nil { - cblog.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - var credentialInfo struct { - ResultList []*cim.CredentialInfo `json:"credential"` - } - json.Unmarshal(resBody, &credentialInfo) - htmlStr += makeCredentialNameHiddenTRList_html(credentialInfo.ResultList) - - // (c) Region Name Hidden List - resBody, err = getResourceList_JsonByte("region") - if err != nil { - cblog.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - var regionInfo struct { - ResultList []*rim.RegionInfo `json:"region"` - } - json.Unmarshal(resBody, ®ionInfo) - htmlStr += makeRegionNameHiddenTRList_html(regionInfo.ResultList) - - // (5) make input field and add - // attach text box for add - nameList := cloudosList() - htmlStr += ` - - - - - - - - - - - - ` - // make page tail - htmlStr += ` -
-  create:  - - - ` - // Select format of CloudOS name=text_box, id=1 - htmlStr += makeSelect_html("onchangeProvider", nameList, "1") - - htmlStr += ` - - - - - - - - + - -
-
- - - ` - - //fmt.Println(htmlStr) - return c.HTML(http.StatusOK, htmlStr) -} - // ================ This Spider Info func SpiderInfo(c echo.Context) error { cblog.Info("call SpiderInfo()") diff --git a/api-runtime/rest-runtime/admin-web/AdminWeb-Connection.go b/api-runtime/rest-runtime/admin-web/AdminWeb-Connection.go new file mode 100644 index 000000000..9a2432c66 --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/AdminWeb-Connection.go @@ -0,0 +1,733 @@ +// Cloud Info Manager's Rest Runtime of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// Updated: 2024.07.05. +// by CB-Spider Team, 2020.06. + +package adminweb + +import ( + "fmt" + "html/template" + "os" + "path/filepath" + "strconv" + + cr "github.com/cloud-barista/cb-spider/api-runtime/common-runtime" + ccim "github.com/cloud-barista/cb-spider/cloud-info-manager/connection-config-info-manager" + cim "github.com/cloud-barista/cb-spider/cloud-info-manager/credential-info-manager" + dim "github.com/cloud-barista/cb-spider/cloud-info-manager/driver-info-manager" + rim "github.com/cloud-barista/cb-spider/cloud-info-manager/region-info-manager" + + "encoding/json" + "net/http" + "strings" + + "github.com/labstack/echo/v4" +) + +// make the string of javascript function +func makeOnchangeConnectionConfigProviderFunc_js() string { + strFunc := ` + function onchangeProvider(source) { + var providerName = source.value + // for credential info + var driverNameList = [] + var credentialNameList + var regionNameList + switch(providerName) { + case "AWS": + driverNameList = document.getElementsByName('driverName-AWS'); + credentialNameList = document.getElementsByName('credentialName-AWS'); + regionNameList = document.getElementsByName('regionName-AWS'); + break; + case "AZURE": + driverNameList = document.getElementsByName('driverName-AZURE'); + credentialNameList = document.getElementsByName('credentialName-AZURE'); + regionNameList = document.getElementsByName('regionName-AZURE'); + break; + case "GCP": + driverNameList = document.getElementsByName('driverName-GCP'); + credentialNameList = document.getElementsByName('credentialName-GCP'); + regionNameList = document.getElementsByName('regionName-GCP'); + break; + case "ALIBABA": + driverNameList = document.getElementsByName('driverName-ALIBABA'); + credentialNameList = document.getElementsByName('credentialName-ALIBABA'); + regionNameList = document.getElementsByName('regionName-ALIBABA'); + break; + case "TENCENT": + driverNameList = document.getElementsByName('driverName-TENCENT'); + credentialNameList = document.getElementsByName('credentialName-TENCENT'); + regionNameList = document.getElementsByName('regionName-TENCENT'); + break; + case "IBM": + driverNameList = document.getElementsByName('driverName-IBM'); + credentialNameList = document.getElementsByName('credentialName-IBM'); + regionNameList = document.getElementsByName('regionName-IBM'); + break; + case "OPENSTACK": + driverNameList = document.getElementsByName('driverName-OPENSTACK'); + credentialNameList = document.getElementsByName('credentialName-OPENSTACK'); + regionNameList = document.getElementsByName('regionName-OPENSTACK'); + break; + case "CLOUDIT": + driverNameList = document.getElementsByName('driverName-CLOUDIT'); + credentialNameList = document.getElementsByName('credentialName-CLOUDIT'); + regionNameList = document.getElementsByName('regionName-CLOUDIT'); + break; + case "DOCKER": + driverNameList = document.getElementsByName('driverName-DOCKER'); + credentialNameList = document.getElementsByName('credentialName-DOCKER'); + regionNameList = document.getElementsByName('regionName-DOCKER'); + break; + + case "NCPVPC": + driverNameList = document.getElementsByName('driverName-NCPVPC'); + credentialNameList = document.getElementsByName('credentialName-NCPVPC'); + regionNameList = document.getElementsByName('regionName-NCPVPC'); + break; + case "NCP": + driverNameList = document.getElementsByName('driverName-NCP'); + credentialNameList = document.getElementsByName('credentialName-NCP'); + regionNameList = document.getElementsByName('regionName-NCP'); + break; + case "NHNCLOUD": + driverNameList = document.getElementsByName('driverName-NHNCLOUD'); + credentialNameList = document.getElementsByName('credentialName-NHNCLOUD'); + regionNameList = document.getElementsByName('regionName-NHNCLOUD'); + break; + case "KTCLOUD": + driverNameList = document.getElementsByName('driverName-KTCLOUD'); + credentialNameList = document.getElementsByName('credentialName-KTCLOUD'); + regionNameList = document.getElementsByName('regionName-KTCLOUD'); + break; + case "KTCLOUDVPC": + driverNameList = document.getElementsByName('driverName-KTCLOUDVPC'); + credentialNameList = document.getElementsByName('credentialName-KTCLOUDVPC'); + regionNameList = document.getElementsByName('regionName-KTCLOUDVPC'); + break; + + case "MOCK": + driverNameList = document.getElementsByName('driverName-MOCK'); + credentialNameList = document.getElementsByName('credentialName-MOCK'); + regionNameList = document.getElementsByName('regionName-MOCK'); + break; + case "CLOUDTWIN": + driverNameList = document.getElementsByName('driverName-CLOUDTWIN'); + credentialNameList = document.getElementsByName('credentialName-CLOUDTWIN'); + regionNameList = document.getElementsByName('regionName-CLOUDTWIN'); + break; + default: + driverNameList = document.getElementsByName('driverName-AWS'); + credentialNameList = document.getElementsByName('credentialName-AWS'); + regionNameList = document.getElementsByName('regionName-AWS'); + } + + // Select Tag for drivers + // options remove & create + var len = document.getElementById('2').options.length + for (var i=0; i < len; i++) { + document.getElementById('2').remove(0); + } + for (var i=0; i < driverNameList.length; i++) { + document.getElementById('2').options.add(new Option(driverNameList[i].innerHTML, driverNameList[i].innerHTML)); + } + + // Select Tag for Credentials + // options remove & create + var len = document.getElementById('3').options.length + for (var i=0; i < len; i++) { + document.getElementById('3').remove(0); + } + for (var i=0; i < credentialNameList.length; i++) { + document.getElementById('3').options.add(new Option(credentialNameList[i].innerHTML, credentialNameList[i].innerHTML)); + } + + // Select Tag for Regions + // options remove & create + var len = document.getElementById('4').options.length + for (var i=0; i < len; i++) { + document.getElementById('4').remove(0); + } + for (var i=0; i < regionNameList.length; i++) { + document.getElementById('4').options.add(new Option(regionNameList[i].innerHTML, regionNameList[i].innerHTML)); + } + + //document.getElementById('5').value= providerName.toLowerCase() + "-" + document.getElementById('4').value + "-connection-config-01"; + document.getElementById('5').value= providerName.toLowerCase() + "-config-01"; + + } + ` + return strFunc +} + +// make the string of javascript function +func makeSetupConnectionConfigFunc_js() string { + + strFunc := ` + function setupConnectionConfig(configName, providerName, region, zone) { + var connConfigLabel = parent.frames["top_frame"].document.getElementById("connConfig"); + connConfigLabel.innerHTML = configName + + var cspText = parent.frames["top_frame"].document.getElementById("connDisplay"); + if (zone) { + cspText.value = providerName + ": " + region + " / " + zone + } else { + cspText.value = providerName + ": " + region + } + + // for vpc + var a = parent.frames["top_frame"].document.getElementById("vpcHref"); + a.href = "vpc/" + configName + a = parent.frames["top_frame"].document.getElementById("vpcmgmtHref"); + a.href = "vpcmgmt/" + configName + + // for securitygroup + a = parent.frames["top_frame"].document.getElementById("securitygroupHref"); + a.href = "securitygroup/" + configName + a = parent.frames["top_frame"].document.getElementById("securitygroupmgmtHref"); + a.href = "securitygroupmgmt/" + configName + + // for KeyPair + a = parent.frames["top_frame"].document.getElementById("keypairHref"); + a.href = "keypair/" + configName + a = parent.frames["top_frame"].document.getElementById("keypairmgmtHref"); + a.href = "keypairmgmt/" + configName + + // for vm + a = parent.frames["top_frame"].document.getElementById("vmHref"); + a.href = "vm/" + configName + a = parent.frames["top_frame"].document.getElementById("vmmgmtHref"); + a.href = "vmmgmt/" + configName + + // for nlb + a = parent.frames["top_frame"].document.getElementById("nlbHref"); + a.href = "nlb/" + configName + a = parent.frames["top_frame"].document.getElementById("nlbmgmtHref"); + a.href = "nlbmgmt/" + configName + + // for disk + a = parent.frames["top_frame"].document.getElementById("diskHref"); + a.href = "disk/" + configName + a = parent.frames["top_frame"].document.getElementById("diskmgmtHref"); + a.href = "diskmgmt/" + configName + + // for myimage + a = parent.frames["top_frame"].document.getElementById("myimageHref"); + a.href = "myimage/" + configName + a = parent.frames["top_frame"].document.getElementById("myimagemgmtHref"); + a.href = "myimagemgmt/" + configName + + // for VMImage + a = parent.frames["top_frame"].document.getElementById("vmimageHref"); + a.href = "vmimage/" + configName + + // for VMSpec + a = parent.frames["top_frame"].document.getElementById("vmspecHref"); + a.href = "vmspec/" + configName + + // for RegionZone + a = parent.frames["top_frame"].document.getElementById("regionzoneHref"); + a.href = "regionzone/" + configName + + // for Price + a = parent.frames["top_frame"].document.getElementById("priceinfoHref"); + a.href = "priceinfo/" + configName + + // for Cluster(PMKS) + a = parent.frames["top_frame"].document.getElementById("clusterHref"); + a.href = "cluster/" + configName + a = parent.frames["top_frame"].document.getElementById("clustermgmtHref"); + a.href = "clustermgmt/" + configName + } + ` + return strFunc +} + +// make the string of javascript function +func makeOnInitialInputBoxSetup_js() string { + strFunc := ` + function onInitialSetup() { + cspSelect = document.getElementById('1') + onchangeProvider(cspSelect) + } + ` + return strFunc +} + +// number, Provider Name, Driver Name, Credential Name, Region Name, Connection Name, checkbox +func makeConnectionConfigTRList_html(bgcolor string, height string, fontSize string, infoList []*ccim.ConnectionConfigInfo) (string, error) { + if bgcolor == "" { + bgcolor = "#FFFFFF" + } + if height == "" { + height = "30" + } + if fontSize == "" { + fontSize = "2" + } + + // make base TR frame for info list + strTR := fmt.Sprintf(` + + + $$NUM$$ + + + $$PROVIDERNAME$$ + + + $$S2$$ + + + $$S3$$ + + + $$S4$$ + + + + $$CONFIGNAME$$ + + + + + + + `, bgcolor, height, fontSize, fontSize, fontSize, fontSize, fontSize, fontSize) + + strData := "" + // set data and make TR list + for i, one := range infoList { + str := strings.ReplaceAll(strTR, "$$NUM$$", strconv.Itoa(i+1)) + str = strings.ReplaceAll(str, "$$PROVIDERNAME$$", one.ProviderName) + str = strings.ReplaceAll(str, "$$S2$$", one.DriverName) + str = strings.ReplaceAll(str, "$$S3$$", one.CredentialName) + str = strings.ReplaceAll(str, "$$S4$$", one.RegionName) + str = strings.ReplaceAll(str, "$$CONFIGNAME$$", one.ConfigName) + + region, zone, err := getRegionZone(one.RegionName) + if err != nil { + cblog.Error(err) + return "", err + } + str = strings.ReplaceAll(str, "$$REGION$$", region) + str = strings.ReplaceAll(str, "$$ZONE$$", zone) + + strData += str + } + + return strData, nil +} + +// make the string of javascript function +func makePostConnectionConfigFunc_js() string { + + // curl -X POST http://$RESTSERVER:1024/spider/connectionconfig -H 'Content-Type: application/json' + // -d '{"ProviderName":"AWS", "DriverName":"aws-driver01", "CredentialName":"aws-credential-01", "RegionName":"aws-ohio", "ConfigName":"aws-ohio-config",}' + + strFunc := ` + function postConnectionConfig() { + var textboxes = document.getElementsByName('text_box'); + sendJson = '{ "ProviderName" : "$$PROVIDER$$", "DriverName" : "$$DRIVERNAME$$", "CredentialName" : "$$CREDENTIALNAME$$", \ + "RegionName" : "$$REGIONNAME$$", "ConfigName" : "$$NAME$$" }' + + for (var i = 0; i < textboxes.length; i++) { // @todo make parallel executions + switch (textboxes[i].id) { + case "1": + sendJson = sendJson.replace("$$PROVIDER$$", textboxes[i].value); + break; + case "2": + sendJson = sendJson.replace("$$DRIVERNAME$$", textboxes[i].value); + break; + case "3": + sendJson = sendJson.replace("$$CREDENTIALNAME$$", textboxes[i].value); + break; + case "4": + sendJson = sendJson.replace("$$REGIONNAME$$", textboxes[i].value); + break; + case "5": + sendJson = sendJson.replace("$$NAME$$", textboxes[i].value); + break; + default: + break; + } + } + var xhr = new XMLHttpRequest(); + xhr.open("POST", "$$SPIDER_SERVER$$/spider/connectionconfig", false); + xhr.setRequestHeader('Content-Type', 'application/json'); + + // client logging + parent.frames["log_frame"].Log("curl -sX POST " + "$$SPIDER_SERVER$$/spider/connectionconfig -H 'Content-Type: application/json' -d '" + sendJson + "'"); + + xhr.send(sendJson); + + // client logging + parent.frames["log_frame"].Log(" => " + xhr.response); + + // setTimeout(function(){ // when async call + location.reload(); + // }, 400); + + } + ` + strFunc = strings.ReplaceAll(strFunc, "$$SPIDER_SERVER$$", "http://"+cr.ServiceIPorName+cr.ServicePort) // cr.ServicePort = ":1024" + return strFunc +} + +// make the string of javascript function +func makeDeleteConnectionConfigFunc_js() string { + // curl -X DELETE http://$RESTSERVER:1024/spider/connectionconfig/aws-connection01 -H 'Content-Type: application/json' + + strFunc := ` + function deleteConnectionConfig() { + var checkboxes = document.getElementsByName('check_box'); + for (var i = 0; i < checkboxes.length; i++) { // @todo make parallel executions + if (checkboxes[i].checked) { + var xhr = new XMLHttpRequest(); + xhr.open("DELETE", "$$SPIDER_SERVER$$/spider/connectionconfig/" + checkboxes[i].value, false); + xhr.setRequestHeader('Content-Type', 'application/json'); + + // client logging + parent.frames["log_frame"].Log("curl -sX DELETE " + "$$SPIDER_SERVER$$/spider/connectionconfig/" + checkboxes[i].value + " -H 'Content-Type: application/json'" ); + + xhr.send(null); + + // client logging + parent.frames["log_frame"].Log(" => " + xhr.response); + } + } + location.reload(); + } + ` + strFunc = strings.ReplaceAll(strFunc, "$$SPIDER_SERVER$$", "http://"+cr.ServiceIPorName+cr.ServicePort) // cr.ServicePort = ":1024" + return strFunc +} + +// create Connection page +func Connectionconfig(c echo.Context) error { + cblog.Info("call Connectionconfig()") + + // make page header + htmlStr := ` + + + + + + + + + ` + + // (2) make Table Action TR + // colspan, f5_href, delete_href, fontSize + htmlStr += makeActionTR_html("7", "connectionconfig", "deleteConnectionConfig()", "2") + + // (3) make Table Header TR + nameWidthList := []NameWidth{ + {"Provider Name", "200"}, + {"Driver Name", "200"}, + {"Credential Name", "200"}, + {"Region Name", "200"}, + {"Connection Config Name", "200"}, + } + htmlStr += makeTitleTRList_html("#DDDDDD", "2", nameWidthList, true) + + // (4) make TR list with info list + // (4-1) get info list @todo if empty list + + // client logging + htmlStr += genLoggingGETResURL("connectionconfig") + + resBody, err := getResourceList_JsonByte("connectionconfig") + if err != nil { + cblog.Error(err) + // client logging + htmlStr += genLoggingGETResURL(err.Error()) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + // client logging + htmlStr += genLoggingResult(string(resBody[:len(resBody)-1])) + + var info struct { + ResultList []*ccim.ConnectionConfigInfo `json:"connectionconfig"` + } + json.Unmarshal(resBody, &info) + + // (4-2) make TR list with info list + trStrList, err := makeConnectionConfigTRList_html("", "", "", info.ResultList) + if err != nil { + cblog.Error(err) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + htmlStr += trStrList + + // (4-3) make hidden TR list with info list + // (a) Driver Name Hidden List + resBody, err = getResourceList_JsonByte("driver") + if err != nil { + cblog.Error(err) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + var driverInfo struct { + ResultList []*dim.CloudDriverInfo `json:"driver"` + } + json.Unmarshal(resBody, &driverInfo) + htmlStr += makeDriverNameHiddenTRList_html(driverInfo.ResultList) + + // (b) Credential Name Hidden List + resBody, err = getResourceList_JsonByte("credential") + if err != nil { + cblog.Error(err) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + var credentialInfo struct { + ResultList []*cim.CredentialInfo `json:"credential"` + } + json.Unmarshal(resBody, &credentialInfo) + htmlStr += makeCredentialNameHiddenTRList_html(credentialInfo.ResultList) + + // (c) Region Name Hidden List + resBody, err = getResourceList_JsonByte("region") + if err != nil { + cblog.Error(err) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + var regionInfo struct { + ResultList []*rim.RegionInfo `json:"region"` + } + json.Unmarshal(resBody, ®ionInfo) + htmlStr += makeRegionNameHiddenTRList_html(regionInfo.ResultList) + + // (5) make input field and add + // attach text box for add + nameList := cloudosList() + htmlStr += ` + + + + + + + + + + + + ` + // make page tail + htmlStr += ` +
+  create:  + + + ` + // Select format of CloudOS name=text_box, id=1 + htmlStr += makeSelect_html("onchangeProvider", nameList, "1") + + htmlStr += ` + + + + + + + + + + +
+
+ + + ` + + //fmt.Println(htmlStr) + return c.HTML(http.StatusOK, htmlStr) +} + +// ================ Connection Config Info Management +type ConnectionConfig struct { + ConfigName string `json:"ConfigName"` + ProviderName string `json:"ProviderName"` + DriverName string `json:"DriverName"` + CredentialName string `json:"CredentialName"` + RegionName string `json:"RegionName"` +} + +type ConnectionConfigs struct { + ConnectionConfigs []ConnectionConfig `json:"connectionconfig"` +} + +type Providers struct { + Providers []string `json:"cloudos"` +} + +type RegionInfo struct { + RegionName string `json:"RegionName"` + ProviderName string `json:"ProviderName"` + KeyValueInfoList []struct { + Key string `json:"Key"` + Value string `json:"Value"` + } `json:"KeyValueInfoList"` +} + +type Regions struct { + Regions []RegionInfo `json:"region"` +} + +type DriverInfo struct { + DriverName string `json:"DriverName"` + DriverLibFileName string `json:"DriverLibFileName"` + ProviderName string `json:"ProviderName"` +} + +type Drivers struct { + Drivers []DriverInfo `json:"driver"` +} + +func fetchConnectionConfigs() (map[string][]ConnectionConfig, error) { + resp, err := http.Get("http://localhost:1024/spider/connectionconfig") + if err != nil { + return nil, fmt.Errorf("error fetching connection configurations: %v", err) + } + defer resp.Body.Close() + + var configs ConnectionConfigs + if err := json.NewDecoder(resp.Body).Decode(&configs); err != nil { + return nil, fmt.Errorf("error decoding connection configurations: %v", err) + } + + connectionMap := make(map[string][]ConnectionConfig) + for _, config := range configs.ConnectionConfigs { + connectionMap[config.ProviderName] = append(connectionMap[config.ProviderName], config) + } + + return connectionMap, nil +} + +func fetchProviders() ([]string, error) { + resp, err := http.Get("http://localhost:1024/spider/cloudos") + if err != nil { + return nil, fmt.Errorf("error fetching providers: %v", err) + } + defer resp.Body.Close() + + var providers Providers + if err := json.NewDecoder(resp.Body).Decode(&providers); err != nil { + return nil, fmt.Errorf("error decoding providers: %v", err) + } + + return providers.Providers, nil +} + +func fetchRegions() (map[string]string, error) { + resp, err := http.Get("http://localhost:1024/spider/region") + if err != nil { + return nil, fmt.Errorf("error fetching regions: %v", err) + } + defer resp.Body.Close() + + var regions Regions + if err := json.NewDecoder(resp.Body).Decode(®ions); err != nil { + return nil, fmt.Errorf("error decoding regions: %v", err) + } + + regionMap := make(map[string]string) + for _, region := range regions.Regions { + var regionValue, zoneValue string + for _, keyValue := range region.KeyValueInfoList { + if keyValue.Key == "Region" { + regionValue = keyValue.Value + } else if keyValue.Key == "Zone" { + zoneValue = keyValue.Value + } + } + if zoneValue == "" { + zoneValue = "NA" + } + regionMap[region.RegionName] = fmt.Sprintf("%s / %s", regionValue, zoneValue) + } + + return regionMap, nil +} + +func fetchDrivers() (map[string]string, error) { + resp, err := http.Get("http://localhost:1024/spider/driver") + if err != nil { + return nil, fmt.Errorf("error fetching drivers: %v", err) + } + defer resp.Body.Close() + + var drivers Drivers + if err := json.NewDecoder(resp.Body).Decode(&drivers); err != nil { + return nil, fmt.Errorf("error decoding drivers: %v", err) + } + + driverMap := make(map[string]string) + for _, driver := range drivers.Drivers { + driverMap[driver.DriverName] = driver.DriverLibFileName + } + + return driverMap, nil +} + +func ConnectionManagement(c echo.Context) error { + connectionConfigs, err := fetchConnectionConfigs() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + providers, err := fetchProviders() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + regions, err := fetchRegions() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + drivers, err := fetchDrivers() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + data := struct { + ConnectionConfigs map[string][]ConnectionConfig + Providers []string + Regions map[string]string + Drivers map[string]string + }{ + ConnectionConfigs: connectionConfigs, + Providers: providers, + Regions: regions, + Drivers: drivers, + } + + templatePath := filepath.Join(os.Getenv("CBSPIDER_ROOT"), "/api-runtime/rest-runtime/admin-web/html/connection.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error loading template: " + err.Error()}) + } + + return tmpl.Execute(c.Response().Writer, data) +} diff --git a/api-runtime/rest-runtime/admin-web/AdminWeb-Credential.go b/api-runtime/rest-runtime/admin-web/AdminWeb-Credential.go new file mode 100644 index 000000000..a821d1c43 --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/AdminWeb-Credential.go @@ -0,0 +1,102 @@ +// Cloud Info Manager's Rest Runtime of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// Updated: 2024.07.09. +// by CB-Spider Team, 2020.06. + +package adminweb + +import ( + "fmt" + "html/template" + "os" + "path/filepath" + + "encoding/json" + "net/http" + + "github.com/labstack/echo/v4" +) + +type CredentialInfo struct { + CredentialName string `json:"CredentialName"` + ProviderName string `json:"ProviderName"` + KeyValueInfoList []struct { + Key string `json:"Key"` + Value string `json:"Value"` + } `json:"KeyValueInfoList"` +} + +type Credentials struct { + Credentials []CredentialInfo `json:"credential"` +} + +type CredentialMetaInfo struct { + Credential []string `json:"Credential"` +} + +func fetchCredentials() (map[string][]CredentialInfo, error) { + resp, err := http.Get("http://localhost:1024/spider/credential") + if err != nil { + return nil, fmt.Errorf("error fetching credentials: %v", err) + } + defer resp.Body.Close() + + var credentials Credentials + if err := json.NewDecoder(resp.Body).Decode(&credentials); err != nil { + return nil, fmt.Errorf("error decoding credentials: %v", err) + } + + credentialMap := make(map[string][]CredentialInfo) + for _, credential := range credentials.Credentials { + credentialMap[credential.ProviderName] = append(credentialMap[credential.ProviderName], credential) + } + + return credentialMap, nil +} + +func fetchCredentialMetaInfo(provider string) ([]string, error) { + resp, err := http.Get(fmt.Sprintf("http://localhost:1024/spider/cloudos/metainfo/%s", provider)) + if err != nil { + return nil, fmt.Errorf("error fetching credential meta info for provider %s: %v", provider, err) + } + defer resp.Body.Close() + + var metaInfo CredentialMetaInfo + if err := json.NewDecoder(resp.Body).Decode(&metaInfo); err != nil { + return nil, fmt.Errorf("error decoding credential meta info: %v", err) + } + + return metaInfo.Credential, nil +} + +func CredentialManagement(c echo.Context) error { + credentials, err := fetchCredentials() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + providers, err := fetchProviders() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + data := struct { + Credentials map[string][]CredentialInfo + Providers []string + }{ + Credentials: credentials, + Providers: providers, + } + + templatePath := filepath.Join(os.Getenv("CBSPIDER_ROOT"), "/api-runtime/rest-runtime/admin-web/html/credential.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error loading template: " + err.Error()}) + } + + return tmpl.Execute(c.Response().Writer, data) +} diff --git a/api-runtime/rest-runtime/admin-web/AdminWeb-Dashboard.go b/api-runtime/rest-runtime/admin-web/AdminWeb-Dashboard.go index 13a5dd238..a544e0676 100644 --- a/api-runtime/rest-runtime/admin-web/AdminWeb-Dashboard.go +++ b/api-runtime/rest-runtime/admin-web/AdminWeb-Dashboard.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "html/template" - "io/ioutil" "net/http" "os" "path/filepath" @@ -13,20 +12,6 @@ import ( "github.com/labstack/echo/v4" ) -// Define struct for connection configurations -type ConnectionConfig struct { - ConfigName string `json:"ConfigName"` - ProviderName string `json:"ProviderName"` - DriverName string `json:"DriverName"` - CredentialName string `json:"CredentialName"` - RegionName string `json:"RegionName"` -} - -// Wrapper for a slice of ConnectionConfig to match JSON structure -type ConnectionConfigs struct { - ConnectionConfigs []ConnectionConfig `json:"connectionconfig"` -} - // ResourceCounts holds the counts for various resources type ResourceCounts struct { ConnectionName string `json:"connectionName"` @@ -70,48 +55,21 @@ func filterEmptyConnections(resourceCounts map[string][]ResourceCounts) map[stri return filteredCounts } -// Fetch all providers -func fetchProviders() ([]string, error) { - resp, err := http.Get("http://localhost:1024/spider/cloudos") - if err != nil { - return nil, fmt.Errorf("error fetching providers: %v", err) - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("error reading response body: %v", err) - } - - var providers struct { - Providers []string `json:"providers"` - } - if err = json.Unmarshal(body, &providers); err != nil { - return nil, fmt.Errorf("error unmarshalling provider list: %v", err) - } - - return providers.Providers, nil -} - -func fetchConnectionConfigs() (map[string][]ConnectionConfig, error) { - resp, err := http.Get("http://localhost:1024/spider/connectionconfig") - if err != nil { - return nil, fmt.Errorf("error fetching connection configurations: %v", err) - } - defer resp.Body.Close() - - var configs ConnectionConfigs - if err := json.NewDecoder(resp.Body).Decode(&configs); err != nil { - return nil, fmt.Errorf("error decoding connection configurations: %v", err) - } - - connectionMap := make(map[string][]ConnectionConfig) - for _, config := range configs.ConnectionConfigs { - connectionMap[config.ProviderName] = append(connectionMap[config.ProviderName], config) - } - - return connectionMap, nil -} +// // Fetch all providers +// func fetchProviders() ([]string, error) { +// resp, err := http.Get("http://localhost:1024/spider/cloudos") +// if err != nil { +// return nil, fmt.Errorf("error fetching providers: %v", err) +// } +// defer resp.Body.Close() + +// var providers Providers +// if err := json.NewDecoder(resp.Body).Decode(&providers); err != nil { +// return nil, fmt.Errorf("error decoding providers: %v", err) +// } + +// return providers.Providers, nil +// } type CountResponse struct { Count int `json:"count"` diff --git a/api-runtime/rest-runtime/admin-web/AdminWeb-Driver.go b/api-runtime/rest-runtime/admin-web/AdminWeb-Driver.go new file mode 100644 index 000000000..c4fbfcbe4 --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/AdminWeb-Driver.go @@ -0,0 +1,205 @@ +package adminweb + +import ( + "encoding/json" + "fmt" + "html/template" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + cr "github.com/cloud-barista/cb-spider/api-runtime/common-runtime" + dim "github.com/cloud-barista/cb-spider/cloud-info-manager/driver-info-manager" + + "github.com/labstack/echo/v4" +) + +// make the string of javascript function +func makeOnChangeDriverProviderFunc() string { + strFunc := ` + function onChangeProvider(source) { + var providerName = source.value; + document.getElementById('2').value = providerName.toLowerCase() + "-driver-v1.0.so"; + document.getElementById('3').value = providerName.toLowerCase() + "-driver-01"; + } + ` + return strFunc +} + +// make the string of javascript function +func makeToggleCheckBoxFunc() string { + strFunc := ` + function toggleCheckBox(source, tableId) { + var table = document.getElementById(tableId); + var checkboxes = table.querySelectorAll('input[name="deleteCheckbox"]'); + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].checked = source.checked; + } + } + ` + return strFunc +} + +// make the string of javascript function +func makePostDriverFunc() string { + strFunc := ` + function postDriver() { + var textboxes = document.getElementsByName('text_box'); + var sendJson = '{ "ProviderName" : "$$PROVIDER$$", "DriverLibFileName" : "$$DRVFILE$$", "DriverName" : "$$NAME$$" }'; + + for (var i = 0; i < textboxes.length; i++) { + switch (textboxes[i].id) { + case "1": + sendJson = sendJson.replace("$$PROVIDER$$", textboxes[i].value); + break; + case "2": + sendJson = sendJson.replace("$$DRVFILE$$", textboxes[i].value); + break; + case "3": + sendJson = sendJson.replace("$$NAME$$", textboxes[i].value); + break; + default: + break; + } + } + var xhr = new XMLHttpRequest(); + xhr.open("POST", "$$SPIDER_SERVER$$/spider/driver", false); + xhr.setRequestHeader('Content-Type', 'application/json'); + + parent.frames["log_frame"].Log("curl -sX POST " + "$$SPIDER_SERVER$$/spider/driver -H 'Content-Type: application/json' -d '" + sendJson + "'"); + + xhr.send(sendJson); + + parent.frames["log_frame"].Log(" => " + xhr.response); + + location.reload(); + } + ` + strFunc = strings.ReplaceAll(strFunc, "$$SPIDER_SERVER$$", "http://"+cr.ServiceIPorName+cr.ServicePort) // cr.ServicePort = ":1024" + return strFunc +} + +// make the string of javascript function +func makeDeleteDriverFunc() string { + strFunc := ` + function deleteDriver() { + var checkboxes = document.getElementsByName('deleteCheckbox'); + for (var i = 0; i < checkboxes.length; i++) { + if (checkboxes[i].checked) { + var xhr = new XMLHttpRequest(); + xhr.open("DELETE", "$$SPIDER_SERVER$$/spider/driver/" + checkboxes[i].value, false); + xhr.setRequestHeader('Content-Type', 'application/json'); + + parent.frames["log_frame"].Log("curl -sX DELETE " + "$$SPIDER_SERVER$$/spider/driver/" + checkboxes[i].value + " -H 'Content-Type: application/json'" ); + + xhr.send(null); + + parent.frames["log_frame"].Log(" => " + xhr.response); + } + } + location.reload(); + } + ` + strFunc = strings.ReplaceAll(strFunc, "$$SPIDER_SERVER$$", "http://"+cr.ServiceIPorName+cr.ServicePort) // cr.ServicePort = ":1024" + return strFunc +} + +// make the string of javascript function +func makeDriverTRListHTML(bgcolor string, height string, fontSize string, infoList []*dim.CloudDriverInfo) string { + if bgcolor == "" { + bgcolor = "#FFFFFF" + } + if height == "" { + height = "30" + } + if fontSize == "" { + fontSize = "2" + } + + // make base TR frame for info list + strTR := fmt.Sprintf(` + + + $$NUM$$ + + + $$S1$$ + + + $$S2$$ + + + + + + `, bgcolor, height, fontSize, fontSize, fontSize) + + strData := "" + // set data and make TR list + for i, one := range infoList { + str := strings.ReplaceAll(strTR, "$$NUM$$", strconv.Itoa(i+1)) + str = strings.ReplaceAll(str, "$$S1$$", one.DriverName) + str = strings.ReplaceAll(str, "$$S2$$", one.DriverLibFileName) + str = strings.ReplaceAll(str, "$$S3$$", one.DriverName) + strData += str + } + + return strData +} + +func fetchDriverInfos() ([]*dim.CloudDriverInfo, error) { + resp, err := http.Get("http://localhost:1024/spider/driver") + if err != nil { + return nil, fmt.Errorf("error fetching drivers: %v", err) + } + defer resp.Body.Close() + + var drivers struct { + ResultList []*dim.CloudDriverInfo `json:"driver"` + } + if err := json.NewDecoder(resp.Body).Decode(&drivers); err != nil { + return nil, fmt.Errorf("error decoding drivers: %v", err) + } + + return drivers.ResultList, nil +} + +// DriverManagement - Handles the management of drivers +func DriverManagement(c echo.Context) error { + // Fetch driver information + drivers, err := fetchDriverInfos() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + // Fetch provider information + providers, err := fetchProviders() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + driverMap := make(map[string][]*dim.CloudDriverInfo) + for _, driver := range drivers { + driverMap[driver.ProviderName] = append(driverMap[driver.ProviderName], driver) + } + + data := struct { + Drivers map[string][]*dim.CloudDriverInfo + Providers []string + }{ + Drivers: driverMap, + Providers: providers, + } + + // Define template path + templatePath := filepath.Join(os.Getenv("CBSPIDER_ROOT"), "/api-runtime/rest-runtime/admin-web/html/driver.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error loading template: " + err.Error()}) + } + + // Execute template + return tmpl.Execute(c.Response().Writer, data) +} diff --git a/api-runtime/rest-runtime/admin-web/AdminWeb-Main.go b/api-runtime/rest-runtime/admin-web/AdminWeb-Main.go new file mode 100644 index 000000000..92732d5dc --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/AdminWeb-Main.go @@ -0,0 +1,46 @@ +package adminweb + +import ( + "html/template" + "os" + "path/filepath" + + cblogger "github.com/cloud-barista/cb-log" + "github.com/labstack/echo/v4" +) + +func init() { + cblog = cblogger.GetLogger("CLOUD-BARISTA") +} + +// MainPage renders the main page with iframes +func MainPage(c echo.Context) error { + cblog.Info("call MainPage()") + + templatePath := filepath.Join(os.Getenv("CBSPIDER_ROOT"), "/api-runtime/rest-runtime/admin-web/html/main.html") + return c.File(templatePath) +} + +// LeftMenu renders the left menu +func LeftMenu(c echo.Context) error { + cblog.Info("call LeftMenu()") + + templatePath := filepath.Join(os.Getenv("CBSPIDER_ROOT"), "/api-runtime/rest-runtime/admin-web/html/left_menu.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return err + } + return tmpl.Execute(c.Response(), nil) +} + +// BodyFrame renders the body frame +func BodyFrame(c echo.Context) error { + cblog.Info("call BodyFrame()") + + templatePath := filepath.Join(os.Getenv("CBSPIDER_ROOT"), "/api-runtime/rest-runtime/admin-web/html/body_frame.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return err + } + return tmpl.Execute(c.Response(), nil) +} diff --git a/api-runtime/rest-runtime/admin-web/AdminWeb-Region.go b/api-runtime/rest-runtime/admin-web/AdminWeb-Region.go new file mode 100644 index 000000000..53590226c --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/AdminWeb-Region.go @@ -0,0 +1,89 @@ +// Cloud Info Manager's Rest Runtime of CB-Spider. +// The CB-Spider is a sub-Framework of the Cloud-Barista Multi-Cloud Project. +// The CB-Spider Mission is to connect all the clouds with a single interface. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// Updated: 2024.07.11. +// by CB-Spider Team, 2020.06. + +package adminweb + +import ( + "fmt" + "html/template" + "os" + "path/filepath" + + "encoding/json" + "net/http" + + "github.com/labstack/echo/v4" +) + +type RegionMetaInfo struct { + Region []string `json:"Region"` +} + +func fetchRegionInfos() (map[string][]RegionInfo, error) { + resp, err := http.Get("http://localhost:1024/spider/region") + if err != nil { + return nil, fmt.Errorf("error fetching regions: %v", err) + } + defer resp.Body.Close() + + var regions Regions + if err := json.NewDecoder(resp.Body).Decode(®ions); err != nil { + return nil, fmt.Errorf("error decoding regions: %v", err) + } + + regionMap := make(map[string][]RegionInfo) + for _, region := range regions.Regions { + regionMap[region.ProviderName] = append(regionMap[region.ProviderName], region) + } + + return regionMap, nil +} + +func fetchRegionMetaInfo(provider string) ([]string, error) { + resp, err := http.Get(fmt.Sprintf("http://localhost:1024/spider/cloudos/metainfo/%s", provider)) + if err != nil { + return nil, fmt.Errorf("error fetching region meta info for provider %s: %v", provider, err) + } + defer resp.Body.Close() + + var metaInfo RegionMetaInfo + if err := json.NewDecoder(resp.Body).Decode(&metaInfo); err != nil { + return nil, fmt.Errorf("error decoding region meta info: %v", err) + } + + return metaInfo.Region, nil +} + +func RegionManagement(c echo.Context) error { + regions, err := fetchRegionInfos() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + providers, err := fetchProviders() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + data := struct { + Regions map[string][]RegionInfo + Providers []string + }{ + Regions: regions, + Providers: providers, + } + + templatePath := filepath.Join(os.Getenv("CBSPIDER_ROOT"), "/api-runtime/rest-runtime/admin-web/html/region.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error loading template: " + err.Error()}) + } + + return tmpl.Execute(c.Response().Writer, data) +} diff --git a/api-runtime/rest-runtime/admin-web/html/body_frame.html b/api-runtime/rest-runtime/admin-web/html/body_frame.html new file mode 100644 index 000000000..10723b63e --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/html/body_frame.html @@ -0,0 +1,103 @@ + + + + + + CB-Spider AdminWeb Tool - How To Use + + + +
+

CB-Spider AdminWeb Tool

+

How To Use

+
+
+
+

1. Create a Connection

+
    +
  1. Register Driver Infos
  2. +
  3. Register Credential Infos
  4. +
  5. Register Region Infos
  6. +
  7. Select Driver, Credential, and Region
  8. +
+
+
+

2. Select a Connection

+
+
+

3. Create Basic Resources

+
    +
  1. Create a VPC & Subnet
  2. +
  3. Create a Security Group
  4. +
  5. Create a VM Keypair
  6. +
+
+
+

4. Provision Infrastructures

+
    +
  1. VM Infra
  2. +
  3. Container Infra (Kubernetes Cluster)
  4. +
+
+
+ + diff --git a/api-runtime/rest-runtime/admin-web/html/connection.html b/api-runtime/rest-runtime/admin-web/html/connection.html new file mode 100644 index 000000000..575d54e69 --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/html/connection.html @@ -0,0 +1,584 @@ + + + + +Connection Management + + + + +
+
+ Connection Icon +

Connection Info Management

+
+
+ + +
+
+ +
+ {{range $provider := .Providers}} +
+

{{$provider}}

+ +
+ + + + + + + + + {{if index $.ConnectionConfigs $provider}} + {{range $config := index $.ConnectionConfigs $provider}} + + + + + + + + {{end}} + {{else}} + + + + {{end}} +
Connection Name + Driver Name : Driver Library + + Manage Driver + + + Credential Name + + Manage Credential + + + Region Name : Region / Zone + + Manage Region + +
+ + {{$config.ConfigName}} + + {{$config.DriverName}} : {{index $.Drivers $config.DriverName}}{{$config.CredentialName}}{{$config.RegionName}} : {{index $.Regions $config.RegionName}} + +
No connections found for {{$provider}}
+ {{end}} +
+ +
+
+

Add New Connection

+
+
+ + +
+
+ + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+
+
+ + +
+
+

Select Driver

+
    + +
+ +
+
+ + +
+
+

Select Credential

+
    + +
+ +
+
+ + +
+
+

Select Region

+
    + +
+ +
+
+ + + diff --git a/api-runtime/rest-runtime/admin-web/html/credential.html b/api-runtime/rest-runtime/admin-web/html/credential.html new file mode 100644 index 000000000..6f71f116b --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/html/credential.html @@ -0,0 +1,515 @@ + + + + +Credential Management + + + + +
+
+ Connection Icon + +

Connection Info Management

+
+

>

+

Credential Info Management

+
+
+ + +
+
+ +
+ {{range $provider := .Providers}} +
+

{{$provider}}

+ +
+ + + + + + + {{if index $.Credentials $provider}} + {{range $credential := index $.Credentials $provider}} + + + + + + {{end}} + {{else}} + + + + {{end}} +
Credential NameCredential Info
{{$credential.CredentialName}} + {{range $credential.KeyValueInfoList}} + {{.Key}}:{{.Value}}
+ {{end}} +
+ +
No credentials found for {{$provider}}
+ {{end}} +
+ +
+
+

Add New Credential

+
Drag & Drop a credential file here (or manually enter the details below)
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+
+
+ + diff --git a/api-runtime/rest-runtime/admin-web/html/driver.html b/api-runtime/rest-runtime/admin-web/html/driver.html new file mode 100644 index 000000000..d279a7274 --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/html/driver.html @@ -0,0 +1,356 @@ + + + + +Driver Management + + + + +
+
+ Connection Icon + +

Connection Info Management

+
+

>

+

Driver Info Management

+
+
+ + +
+
+ +
+ {{range $provider := .Providers}} +
+

{{$provider}}

+ +
+ + + + + + + {{if index $.Drivers $provider}} + {{range $driver := index $.Drivers $provider}} + + + + + + {{end}} + {{else}} + + + + {{end}} +
Driver NameDriver Library
{{$driver.DriverName}}{{$driver.DriverLibFileName}} + +
No drivers found for {{$provider}}
+ {{end}} +
+ +
+
+

Add New Driver Info

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + diff --git a/api-runtime/rest-runtime/admin-web/html/left_menu.html b/api-runtime/rest-runtime/admin-web/html/left_menu.html new file mode 100644 index 000000000..58e7f62fc --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/html/left_menu.html @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/api-runtime/rest-runtime/admin-web/html/main.html b/api-runtime/rest-runtime/admin-web/html/main.html new file mode 100644 index 000000000..e6a06547c --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/html/main.html @@ -0,0 +1,312 @@ + + + + + CB-Spider Admin Web Tool + + + + +
+
+ +
+
+
+
+

Please select a connection first.

+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + diff --git a/api-runtime/rest-runtime/admin-web/html/region.html b/api-runtime/rest-runtime/admin-web/html/region.html new file mode 100644 index 000000000..f648e2edd --- /dev/null +++ b/api-runtime/rest-runtime/admin-web/html/region.html @@ -0,0 +1,710 @@ + + + + +Region Management + + + + +
+
+ Connection Icon + +

Connection Info Management

+
+

>

+

Region Info Management

+
+
+ + +
+
+ +
+ {{range $provider := .Providers}} +
+

{{$provider}}

+ + +
+ + + + + + + + {{if index $.Regions $provider}} + {{range $region := index $.Regions $provider}} + + + + + + + {{end}} + {{else}} + + + + {{end}} +
Region NameRegionZone
{{$region.RegionName}}{{(index $region.KeyValueInfoList 0).Value}} + {{if lt 1 (len $region.KeyValueInfoList) }} + {{(index $region.KeyValueInfoList 1).Value}} + {{else}} + N/A + {{end}} + + +
No regions found for {{$provider}}
+ {{end}} +
+ +
+
+

Add New Region

+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+
+
+ +
+
+

Import Regions:

+
+
+ + + + + + +
+ +
+
+ +
+
+
+ + diff --git a/api-runtime/rest-runtime/admin-web/images/alibaba.png b/api-runtime/rest-runtime/admin-web/images/alibaba.png new file mode 100644 index 0000000000000000000000000000000000000000..a8874832eaf9fc2d814a021fcb5246ec2935d860 GIT binary patch literal 8386 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 diff --git a/api-runtime/rest-runtime/admin-web/images/connection.png b/api-runtime/rest-runtime/admin-web/images/connection.png new file mode 100644 index 0000000000000000000000000000000000000000..14da24a17b97291081daa58d6c9153714b3ced0c GIT binary patch literal 12230 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 wl)m^ic!nS>{~j?F^$+-e_>z>% literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/connection_small.png b/api-runtime/rest-runtime/admin-web/images/connection_small.png new file mode 100644 index 0000000000000000000000000000000000000000..74f414f95af35770df4a4c8bf2710cd3161c3f8b GIT binary patch literal 8685 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/gcp.png b/api-runtime/rest-runtime/admin-web/images/gcp.png new file mode 100644 index 0000000000000000000000000000000000000000..19255a7ffb00bf1da4ba881ed0464478b3a9e959 GIT binary patch literal 11521 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­vk_^0 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/ibm.png b/api-runtime/rest-runtime/admin-web/images/ibm.png new file mode 100644 index 0000000000000000000000000000000000000000..f4266a897103bf38fa0e22b4a3db3a2dd0303be5 GIT binary patch literal 17426 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 WH<<3<06a{Cfx*+&&t;ucLK6T%hU&=x literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/ktcloud.png b/api-runtime/rest-runtime/admin-web/images/ktcloud.png new file mode 100644 index 0000000000000000000000000000000000000000..f169b703bdd725b3e5fcd4c55b740db25ed888ff GIT binary patch literal 9789 zcmbt)WmFwe(%?gb2KN9DcMa~r-CcsayF0;xJA~j69D+XFEx1c?m+)}6eVLi>n>qXK z{@AnK=X6(>-Kx5`@2RTpC}l+{R3t(q004k0BQ35906^hE;BN@%0DPzcfS@n{;1PlfIs^dRSpk4!BLILe0|3Bt%5GH=fDn|; zwPh?66ae%P7y$qcg$aOxKv0kq5Md2~{Ra#HKr}%Pm^`R|Q}dwziwcF82lHPr;M-qD zmH_Ml005fZT3y>sTS1=B)X|>V*v!$yoY~9X=`RC7z>5z8+MBx>1HJ6+99;Rl1j+xw z!3Tl=qFKm+|6p;m6(rYIPzH)Qx|jpGm|2-w$%T-BK%jt&nFXJ!xa7aYAy0zjR&H)i zd@L-Uo}SE}ADJCpELqrid3jk_*;&}xnIIfYuHFuA#$HSgt`z@N@?Uzy&0S4htexDf z9UXvw^%|Qvy1NOIlm8v)zmI>~Y3^nH?~xo_|K%3ML6*N17B*&9mj9EOo3+LNMeHx- zpJM;;>z~63{8h%MZ0%)krz37{Z|>j|IsH78?Jb0M~WiTnrZKe+$e&8OsIZ4L?P-@yH4{~x%2@e8p0jp~0y`JYqx z4=iL3g^&bT{(C(LA>~9B$O8Zr%rfGi)V-ii4G{gXB=7>;WRwn5Q!4D~rFeIhVR3MT zMdF&gLP*Jf{eUjmya+Buh>XrF^(y#4P7)cp{o)N&e*bBJ2t!F-#I7a(9g~#Y@3A{- zWtrr)aq6~ZyCm)|!>qMT?yL2u$y;-e{c$3IY#4w{1OXlT|NoXp3LO%lpduF*#0ZV6 zC+Pr>^}P(Jecq?qLq~}SU4etozi%em2&^-0Y}^jc%F6m4aaXk-!Nj(_Pzq#32kv0v zu3-y_mhkvA*CDa35n%BZURa^ZTlK*%hyutYt8h`YIkVE%LV$M0G4{Kp`6V8vBRTV3 zrbLZ4zsXvs$VCRJ8tx+m@5H6LVtCNu^DYi45T=MltT4rk>4SdGTUDn0Bs{pBF{nTa z9sZiN5D6f_iv>uU-ppBaC}A+NX>AXi06RO}`pyyrq<9J*;B;8v4}NMzdsZ$J#QGas zd>6Y^&LBWfQow~`WBORrWfc7FT;qpu7M4od%qH|;ca&r`09IRBRr`jydxbDm0_cH> zkl>sM_)@ouafstM8b|b{hJWYWn0KI~KW34~u%1|oBR&n+-WSnGE|tJM1~+pUBrd1) zRAs5CpH(<7{UK)+H6&rn^JYqGDO7U*BB`LNeq=bTr20#j$`ICf2Wl>Tp9#?5oUrTL!+D2N z&a@wXNMWqOBSOg~tQ1@HAwx}rhrgL&7$e4Mu7sx_9v4Y;xG(}gljhFUHPrc?cplqe zq~gL_?H0gwAe4^2sAG!g$6V2Okc$v&(C3|;4QZs#Q%qrb)s!QNW1~Zx=|hJwvD0tO zTuOw{w#U?bI!eq>B-LeIGDM#EWG(@h`>66&`bDIhqZ27$SiXo4Xym!8h|AO~rDCnk z69CPD31^(j#Bk?Byjes|iJr%chp0@>Uy(k)Z^cb|00hU_3HsYc=;1O(_7x+J@X|?* zHdpo%Ht5W7+m+KDHcE4C;3I^l|Fn^X~ zoeVLlst~4zB^W}eYaAzqe97Xp;WPB*>!Un%MP(T-4tuI>$Eme>M#KiGvVBICbY|0?J2JM!|1MCYh(u#$Ds?_Nxr*P!p;J_H{VlwTMNfVt7+j!IqTF&?VD|c

d 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 KkXi+c(EkGoUpT=4 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/dashboard.png b/api-runtime/rest-runtime/admin-web/images/left-menu/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..787e9766b5a5c12494b0f288ee743a45cedaba38 GIT binary patch literal 8737 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 V0pCL@{`^6qt$t6fR>eB({{UQN+#CP^ literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/dashboard_selected.png b/api-runtime/rest-runtime/admin-web/images/left-menu/dashboard_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..8856c16a00edb3d9de67fd4fa8103b1340114d8a GIT binary patch literal 8739 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 N@Jtm}CG#Tq{{bLo7Z?Bl literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/disk.png b/api-runtime/rest-runtime/admin-web/images/left-menu/disk.png new file mode 100644 index 0000000000000000000000000000000000000000..9bb89b18534fbc7b5f123b4a4c0b0e94dd50eec7 GIT binary patch literal 4031 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 MGqNIah#qnO112ZWg8%>k literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/disk_selected.png b/api-runtime/rest-runtime/admin-web/images/left-menu/disk_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..c38e08b55a1f1e4e1675e7e2af790f9025da99ab GIT binary patch literal 3950 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 A_5c6? literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/keypair_selected.png b/api-runtime/rest-runtime/admin-web/images/left-menu/keypair_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..bd6a5f6106c06850225c0494ba96d2d269fb56a4 GIT binary patch literal 5408 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/myimage.png b/api-runtime/rest-runtime/admin-web/images/left-menu/myimage.png new file mode 100644 index 0000000000000000000000000000000000000000..da3993478dcd97c7263e0eb64046889107b97ca4 GIT binary patch literal 6693 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 Hte^cKc?J3A literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/nlb.png b/api-runtime/rest-runtime/admin-web/images/left-menu/nlb.png new file mode 100644 index 0000000000000000000000000000000000000000..d1831f05ad656983f2b62280fea1e7dc61f6a47d GIT binary patch literal 4193 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 R4r#z;Q&UDNRVtVT{~s5FVvzs< literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/vm_selected.png b/api-runtime/rest-runtime/admin-web/images/left-menu/vm_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..2f14c115bfcf647c03bfa1b859ec1f385cf8be72 GIT binary patch literal 6165 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 SVSj#!0hHv_WNW2gy#7BiN}31& literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/left-menu/vpc_selected.png b/api-runtime/rest-runtime/admin-web/images/left-menu/vpc_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..4429426471f8f1748fea9d0808f3ad8e2968dcf9 GIT binary patch literal 6820 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/mgmt.png b/api-runtime/rest-runtime/admin-web/images/mgmt.png new file mode 100644 index 0000000000000000000000000000000000000000..0a39f294e6b4e5d8369e8582aa478f9ffb6f6e82 GIT binary patch literal 3613 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 XI1dP6$Q_lxUn^~OJ+&$o+pzxu{r0dY literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/mock.png b/api-runtime/rest-runtime/admin-web/images/mock.png new file mode 100644 index 0000000000000000000000000000000000000000..4ffa9609ae4e11f1bef9723e0ac60ff2b96faa42 GIT binary patch literal 7862 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/ncp.png b/api-runtime/rest-runtime/admin-web/images/ncp.png new file mode 100644 index 0000000000000000000000000000000000000000..7c4d3349b30b475a2474eac9f174c15a4b9aadd9 GIT binary patch literal 10611 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 CK9Cmx literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/nhncloud.png b/api-runtime/rest-runtime/admin-web/images/nhncloud.png new file mode 100644 index 0000000000000000000000000000000000000000..804d23d02733ebe96752e4d62c6fdd18b14ceef9 GIT binary patch literal 6707 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/tencent.png b/api-runtime/rest-runtime/admin-web/images/tencent.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b48d19f5cf2bc7398f4a501822f075650faff6 GIT binary patch literal 30104 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/top-menu/price.png b/api-runtime/rest-runtime/admin-web/images/top-menu/price.png new file mode 100644 index 0000000000000000000000000000000000000000..fbffacd54808427fb74ef9a4b7503d65704a501f GIT binary patch literal 5884 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/top-menu/region-zone.png b/api-runtime/rest-runtime/admin-web/images/top-menu/region-zone.png new file mode 100644 index 0000000000000000000000000000000000000000..fa62fbe20f914e8606650e017322eb965175f482 GIT binary patch literal 5014 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 Y7Ktl+@+twuzg7S>B`w7Yv}O2z0j(InK>z>% literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/top-menu/region-zone_selected.png b/api-runtime/rest-runtime/admin-web/images/top-menu/region-zone_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..276f86ffda747df39d085d8dd2f4aad7864c1727 GIT binary patch literal 4775 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/top-menu/vm-image.png b/api-runtime/rest-runtime/admin-web/images/top-menu/vm-image.png new file mode 100644 index 0000000000000000000000000000000000000000..4bf40b2c0fb50a10f2f7da6fa69d5e77614c0677 GIT binary patch literal 4919 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/top-menu/vm-image_selected.png b/api-runtime/rest-runtime/admin-web/images/top-menu/vm-image_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..c3b9d0679301e20e8f49291fb4d7f8b26fb58aa6 GIT binary patch literal 4762 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 literal 0 HcmV?d00001 diff --git a/api-runtime/rest-runtime/admin-web/images/top-menu/vm-specs.png b/api-runtime/rest-runtime/admin-web/images/top-menu/vm-specs.png new file mode 100644 index 0000000000000000000000000000000000000000..f496d2fd5034d78e2cb6cf912aa0d348f6882246 GIT binary patch literal 5112 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 Date: Wed, 24 Jul 2024 04:47:44 +0000 Subject: [PATCH 36/56] Added NLB and Cluster related Add / Remove functions --- .../drivers/aws/connect/AwsCloudConnection.go | 4 +- .../drivers/aws/main/Test_Resources.go | 28 +- .../drivers/aws/resources/TagHandler.go | 248 +++++++++++++++++- 3 files changed, 253 insertions(+), 27 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go b/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go index 424000f90..bf03129a4 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go @@ -115,12 +115,12 @@ func (cloudConn *AwsCloudConnection) CreateSecurityHandler() (irs.SecurityHandle } func (cloudConn *AwsCloudConnection) CreateTagHandler() (irs.TagHandler, error) { - handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient, NLBClient: cloudConn.NLBClient} + handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient, NLBClient: cloudConn.NLBClient, EKSClient: cloudConn.EKSClient} return &handler, nil } func (cloudConn *AwsCloudConnection) CreateAwsTagHandler() ars.AwsTagHandler { - handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient, NLBClient: cloudConn.NLBClient} + handler := ars.AwsTagHandler{Region: cloudConn.Region, Client: cloudConn.VMClient, NLBClient: cloudConn.NLBClient, EKSClient: cloudConn.EKSClient} return handler } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go index 0e0f8a51a..64837edcc 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go @@ -1449,32 +1449,33 @@ func handleCluster() { } subnets := []irs.IID{} - subnets = append(subnets, irs.IID{SystemId: "subnet-0d30ee6b367974a39"}) //a1 - subnets = append(subnets, irs.IID{SystemId: "subnet-06d5c04b32019b81f"}) //c1 - subnets = append(subnets, irs.IID{SystemId: "subnet-05c5d26bd2f014591"}) //a2 + subnets = append(subnets, irs.IID{SystemId: "subnet-02127b9d8c84f7440"}) //2a + subnets = append(subnets, irs.IID{SystemId: "subnet-0c2b7e03a5f397e25"}) //2c //vpc-0c4d36a3ac3924419 clusterReqInfo := irs.ClusterInfo{ - IId: irs.IID{NameId: "cb-eks-cluster-test01"}, + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: securityName+"123"}}, + TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}}, + IId: irs.IID{NameId: "cb-eks-cluster-tag-test", SystemId: "cb-eks-cluster-tag-test"}, //Version : "1.23.3", //K8s version Network: irs.NetworkInfo{ - VpcIID: irs.IID{SystemId: "vpc-0c4d36a3ac3924419"}, + VpcIID: irs.IID{SystemId: "vpc-0a115f43d4fcbab36"}, //SubnetIID: [irs.IID{SystemId: "subnet-262d6d7a"},irs.IID{SystemId: "vpc-c0479cab"}], SubnetIIDs: subnets, }, } // nlbReqInfo reqNodeGroupInfo := irs.NodeGroupInfo{ - IId: irs.IID{NameId: "cb-eks-node-test01"}, + IId: irs.IID{NameId: "cb-eks-node-tag-test"}, MinNodeSize: 1, MaxNodeSize: 2, // VM config. - ImageIID: irs.IID{SystemId: "ami-00e07ff65a55e3ca5"}, // Amazon Linux 2 (AL2_x86_64) - https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html + ImageIID: irs.IID{SystemId: "ami-056a29f2eddc40520"}, // Amazon Linux 2 (AL2_x86_64) - https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html VMSpecName: "t3.medium", RootDiskType: "SSD(gp2)", // "SSD(gp2)", "Premium SSD", ... RootDiskSize: "20", // "", "default", "50", "1000" (GB) - KeyPairIID: irs.IID{SystemId: "japan-test"}, + KeyPairIID: irs.IID{SystemId: "CB-KeyPairTagTest"}, //Status NodeGroupStatus @@ -1512,7 +1513,7 @@ func handleCluster() { case 1: result, err := handler.ListCluster() if err != nil { - cblogger.Infof(" Cluster List Lookup Failed : ", err) + cblogger.Info(" Cluster List Lookup Failed : ", err) } else { cblogger.Info("Cluster List Lookup Result") cblogger.Debug(result) @@ -1532,7 +1533,7 @@ func handleCluster() { if err != nil { cblogger.Infof(clusterReqInfo.IId.NameId, " Cluster Create Fail : ", err) } else { - cblogger.Infof("Cluster Create Success : ", result) + cblogger.Info("Cluster Create Success : ", result) clusterReqInfo.IId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 if cblogger.Level.String() == "debug" { spew.Dump(result) @@ -2076,7 +2077,8 @@ func readConfigFile() Config { } func main() { - handleTag() + // myimage / disk + //handleTag() // handlePublicIP() // PublicIP 생성 후 conf //handleKeyPair() @@ -2086,9 +2088,9 @@ func main() { // handleImage() //AMI // handleVNic() //Lancard // handleVMSpec() - handleNLB() + //handleNLB() handleCluster() //handleRegionZone() //handlePriceInfo() - handleTag() + //handleTag() } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go index 76cdc47c2..5c9151887 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go @@ -24,6 +24,7 @@ import ( //"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/eks" "github.com/aws/aws-sdk-go/service/elbv2" call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" @@ -35,6 +36,7 @@ type AwsTagHandler struct { Region idrv.RegionInfo Client *ec2.EC2 NLBClient *elbv2.ELBV2 + EKSClient *eks.EKS } // Map of RSType to AWS resource types @@ -76,6 +78,20 @@ func (tagHandler *AwsTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag return irs.KeyValue{}, errors.New(msg) } + if resType == irs.NLB || resType == irs.CLUSTER { + var err error + if resType == irs.NLB { + err = tagHandler.AddNLBTag(resIID, tag) + } else if resType == irs.CLUSTER { + err = tagHandler.AddClusterTag(resIID, tag) + } + + if err != nil { + return irs.KeyValue{}, err + } + return tagHandler.GetTag(resType, resIID, tag.Key) + } + resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.SystemId, "CreateTags()") @@ -106,6 +122,41 @@ func (tagHandler *AwsTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag return tag, nil } +func (tagHandler *AwsTagHandler) AddNLBTag(resIID irs.IID, tag irs.KeyValue) error { + input := &elbv2.AddTagsInput{ + ResourceArns: []*string{aws.String(resIID.SystemId)}, + Tags: []*elbv2.Tag{ + { + Key: aws.String(tag.Key), + Value: aws.String(tag.Value), + }, + }, + } + + _, err := tagHandler.NLBClient.AddTags(input) + if err != nil { + return fmt.Errorf("failed to add tag to NLB: %w", err) + } + + return nil +} + +func (tagHandler *AwsTagHandler) AddClusterTag(resIID irs.IID, tag irs.KeyValue) error { + input := &eks.TagResourceInput{ + ResourceArn: aws.String(resIID.SystemId), + Tags: map[string]*string{ + tag.Key: aws.String(tag.Value), + }, + } + + _, err := tagHandler.EKSClient.TagResource(input) + if err != nil { + return fmt.Errorf("failed to add tag to EKS cluster: %w", err) + } + + return nil +} + func (tagHandler *AwsTagHandler) GetAllNLBTags() ([]*irs.TagInfo, error) { // Step 1: List all load balancers and store their ARNs and Names lbArnToName := make(map[string]string) @@ -157,7 +208,87 @@ func (tagHandler *AwsTagHandler) GetAllNLBTags() ([]*irs.TagInfo, error) { return allTagInfos, nil } -func (tagHandler *AwsTagHandler) GetNLBTags(resIID irs.IID) ([]irs.KeyValue, error) { +// GetAllClusterTags retrieves tags for all EKS clusters. +func (tagHandler *AwsTagHandler) GetAllClusterTags() ([]*irs.TagInfo, error) { + var allTagInfos []*irs.TagInfo + + err := tagHandler.EKSClient.ListClustersPages(&eks.ListClustersInput{}, func(page *eks.ListClustersOutput, lastPage bool) bool { + for _, clusterName := range page.Clusters { + resIID := irs.IID{ + NameId: *clusterName, + SystemId: *clusterName, + } + + tagInfos, err := tagHandler.GetClusterTags(resIID) + if err != nil { + cblogger.Errorf("failed to get tags for EKS cluster %s: %v", *clusterName, err) + continue + } + + // Process only if Tag exists + if len(tagInfos) == 0 { + continue + } + + tagInfo := &irs.TagInfo{ + ResType: irs.CLUSTER, + ResIId: resIID, + TagList: tagInfos, + } + + allTagInfos = append(allTagInfos, tagInfo) + } + return !lastPage + }) + + if err != nil { + return nil, fmt.Errorf("failed to list EKS clusters: %w", err) + } + + if len(allTagInfos) == 0 { + return nil, fmt.Errorf("no EKS clusters found") + } + + return allTagInfos, nil +} + +func (tagHandler *AwsTagHandler) GetClusterTags(resIID irs.IID, key ...string) ([]irs.KeyValue, error) { + cblogger.Debugf("Req resIID:[%s]", resIID) + input := &eks.DescribeClusterInput{ + Name: aws.String(resIID.SystemId), + } + + result, err := tagHandler.EKSClient.DescribeCluster(input) + if err != nil { + return nil, fmt.Errorf("failed to describe EKS cluster: %w", err) + } + + if cblogger.Level.String() == "debug" { + cblogger.Debug(result) + } + + var retTagList []irs.KeyValue + if len(key) > 0 { // If the key value exists + specificKey := key[0] + if value, exists := result.Cluster.Tags[specificKey]; exists { + retTagList = append(retTagList, irs.KeyValue{ + Key: specificKey, + Value: *value, + }) + } + } else { // If the key value not exists + for k, v := range result.Cluster.Tags { + retTagList = append(retTagList, irs.KeyValue{ + Key: k, + Value: *v, + }) + } + } + + return retTagList, nil +} + +func (tagHandler *AwsTagHandler) GetNLBTags(resIID irs.IID, key ...string) ([]irs.KeyValue, error) { input := &elbv2.DescribeTagsInput{ ResourceArns: []*string{ aws.String(resIID.SystemId), @@ -173,16 +304,27 @@ func (tagHandler *AwsTagHandler) GetNLBTags(resIID irs.IID) ([]irs.KeyValue, err return nil, fmt.Errorf("no tags found for load balancer: %s", resIID.SystemId) } - if cblogger.Level.String() == "debug" { - cblogger.Debug(result) - } + cblogger.Debug(result) var retTagList []irs.KeyValue - for _, tag := range result.TagDescriptions[0].Tags { - retTagList = append(retTagList, irs.KeyValue{ - Key: aws.StringValue(tag.Key), - Value: aws.StringValue(tag.Value), - }) + if len(key) > 0 { // If the key value exists + specificKey := key[0] + for _, tag := range result.TagDescriptions[0].Tags { + if aws.StringValue(tag.Key) == specificKey { + retTagList = append(retTagList, irs.KeyValue{ + Key: aws.StringValue(tag.Key), + Value: aws.StringValue(tag.Value), + }) + break + } + } + } else { // If the key value not exists + for _, tag := range result.TagDescriptions[0].Tags { + retTagList = append(retTagList, irs.KeyValue{ + Key: aws.StringValue(tag.Key), + Value: aws.StringValue(tag.Value), + }) + } } return retTagList, nil @@ -199,6 +341,8 @@ func (tagHandler *AwsTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([] if resType == irs.NLB { return tagHandler.GetNLBTags(resIID) + } else if resType == irs.CLUSTER { + return tagHandler.GetClusterTags(resIID) } resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error @@ -252,6 +396,29 @@ func (tagHandler *AwsTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key cblogger.Error(msg) return irs.KeyValue{}, errors.New(msg) } + + // NLB and Cluster use different APIs + if resType == irs.NLB || resType == irs.CLUSTER { + var tagList []irs.KeyValue + var err error + + if resType == irs.NLB { + tagList, err = tagHandler.GetNLBTags(resIID, key) + } else if resType == irs.CLUSTER { + tagList, err = tagHandler.GetClusterTags(resIID, key) + } + + if err != nil { + return irs.KeyValue{}, fmt.Errorf("failed to get tags: %w", err) + } + + if len(tagList) == 0 { + return irs.KeyValue{}, nil + } else { + return tagList[0], nil + } + } + resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error input := &ec2.DescribeTagsInput{ @@ -353,6 +520,13 @@ func (tagHandler *AwsTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, k cblogger.Error(msg) return false, errors.New(msg) } + + if resType == irs.NLB { + return tagHandler.RemoveNLBTag(resIID, key) + } else if resType == irs.CLUSTER { + return tagHandler.RemoveClusterTag(resIID, key) + } + resIID = tagHandler.GetRealResourceId(resType, resIID) // fix some resource id error input := &ec2.DeleteTagsInput{ @@ -389,6 +563,38 @@ func (tagHandler *AwsTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, k return true, nil } +func (tagHandler *AwsTagHandler) RemoveNLBTag(resIID irs.IID, tagKey string) (bool, error) { + input := &elbv2.RemoveTagsInput{ + ResourceArns: []*string{aws.String(resIID.SystemId)}, + TagKeys: []*string{ + aws.String(tagKey), + }, + } + + _, err := tagHandler.NLBClient.RemoveTags(input) + if err != nil { + return false, fmt.Errorf("failed to remove tag from NLB: %w", err) + } + + return true, nil +} + +func (tagHandler *AwsTagHandler) RemoveClusterTag(resIID irs.IID, tagKey string) (bool, error) { + input := &eks.UntagResourceInput{ + ResourceArn: aws.String(resIID.SystemId), + TagKeys: []*string{ + aws.String(tagKey), + }, + } + + _, err := tagHandler.EKSClient.UntagResource(input) + if err != nil { + return false, fmt.Errorf("failed to remove tag from EKS cluster: %w", err) + } + + return true, nil +} + // Extracts a list of Key or Value tags corresponding to keyword from the tagInfos array. func (tagHandler *AwsTagHandler) ExtractTagKeyValue(tagInfos []*irs.TagInfo, keyword string) []*irs.TagInfo { var matchingTagInfos []*irs.TagInfo @@ -398,6 +604,7 @@ func (tagHandler *AwsTagHandler) ExtractTagKeyValue(tagInfos []*irs.TagInfo, key } /* + // All tags for _, tagInfo := range tagInfos { for _, kv := range tagInfo.TagList { if kv.Key == keyword || kv.Value == keyword { @@ -452,6 +659,15 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] //spew.Dump(nlbTaginfos) return tagHandler.ExtractTagKeyValue(nlbTaginfos, keyword), nil } + } else if resType == irs.CLUSTER { + // Add a list of k8s Tags if this is a all search + if keyword == "" || keyword == "*" { + return tagHandler.GetAllClusterTags() + } else { + k8sTaginfos, _ := tagHandler.GetAllClusterTags() + //spew.Dump(k8sTaginfos) + return tagHandler.ExtractTagKeyValue(k8sTaginfos, keyword), nil + } } if awsResType, ok := rsTypeToAwsResourceTypeMap[resType]; ok { @@ -472,9 +688,9 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] processTags := func(result *ec2.DescribeTagsOutput) { if cblogger.Level.String() == "debug" { //cblogger.Debug(result) - cblogger.Debug("=================================") - spew.Dump(result) - cblogger.Debug("=================================") + //cblogger.Debug("=================================") + //spew.Dump(result) + //cblogger.Debug("=================================") } for _, tag := range result.Tags { @@ -578,11 +794,19 @@ func (tagHandler *AwsTagHandler) FindTag(resType irs.RSType, keyword string) ([] // Add a list of NLB Tags if this is a all search if resType == irs.ALL { + //nlb nlbTaginfos, _ := tagHandler.GetAllNLBTags() if keyword != "" && keyword != "*" { nlbTaginfos = tagHandler.ExtractTagKeyValue(nlbTaginfos, keyword) } tagInfos = append(tagInfos, nlbTaginfos...) + + //cluster + k8sTaginfos, _ := tagHandler.GetAllClusterTags() + if keyword != "" && keyword != "*" { + k8sTaginfos = tagHandler.ExtractTagKeyValue(k8sTaginfos, keyword) + } + tagInfos = append(tagInfos, k8sTaginfos...) } return tagInfos, nil From 70a4c3a741c00e1860a4759bf1f577fa11cda445 Mon Sep 17 00:00:00 2001 From: SungWoongz Date: Wed, 24 Jul 2024 14:12:21 +0900 Subject: [PATCH 37/56] add tags when create vm, cluster, disk, keypair, myimage, nlb, vpc, sg --- .../alibaba/resources/ClusterHandler.go | 32 +++++++- .../drivers/alibaba/resources/DiskHandler.go | 11 +++ .../alibaba/resources/KeyPairHandler.go | 24 ++++++ .../alibaba/resources/MyImageHandler.go | 55 +++++++++---- .../drivers/alibaba/resources/NLBHandler.go | 15 ++++ .../alibaba/resources/SecurityHandler.go | 19 +++++ .../drivers/alibaba/resources/VMHandler.go | 26 ++++++ .../drivers/alibaba/resources/VPCHandler.go | 82 ++++++++++++++++--- 8 files changed, 235 insertions(+), 29 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/ClusterHandler.go index 9887807d1..3d425d7dc 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/ClusterHandler.go @@ -160,6 +160,21 @@ func (ach *AlibabaClusterHandler) CreateCluster(clusterReqInfo irs.ClusterInfo) snatEntry := true epPublicAccess := true + var tagList *[]cs2015.Tag + if clusterReqInfo.TagList != nil && len(clusterReqInfo.TagList) > 0 { + + clusterTags := []cs2015.Tag{} + for _, clusterTag := range clusterReqInfo.TagList { + tag0 := cs2015.Tag{ + Key: &clusterTag.Key, + Value: &clusterTag.Value, + } + clusterTags = append(clusterTags, tag0) + + } + tagList = &clusterTags + } + runtimeName, runtimeVersion, err := getLatestRuntime(ach.CsClient, regionId, clusterType, k8sVersion) if err != nil { err := fmt.Errorf("Failed to Create Cluster: %v", err) @@ -171,7 +186,7 @@ func (ach *AlibabaClusterHandler) CreateCluster(clusterReqInfo irs.ClusterInfo) nodepools := getNodepoolsFromNodeGroupList(clusterReqInfo.NodeGroupList, runtimeName, runtimeVersion, vswitchIds) - clusterId, err := aliCreateCluster(ach.CsClient, clusterName, regionId, clusterType, clusterSpec, k8sVersion, runtimeName, runtimeVersion, vpcId, containerCidr, serviceCidr, secGroupId, snatEntry, epPublicAccess, vswitchIds, tagKeyCbSpiderPmksCluster, tagValueOwned, nodepools) + clusterId, err := aliCreateCluster(ach.CsClient, clusterName, regionId, clusterType, clusterSpec, k8sVersion, runtimeName, runtimeVersion, vpcId, containerCidr, serviceCidr, secGroupId, snatEntry, epPublicAccess, vswitchIds, tagKeyCbSpiderPmksCluster, tagValueOwned, tagList, nodepools) if err != nil { err := fmt.Errorf("Failed to Create Cluster: %v", err) cblogger.Error(err) @@ -1178,14 +1193,25 @@ func getNodepoolsFromNodeGroupList(nodeGroupInfoList []irs.NodeGroupInfo, runtim return nodepools } -func aliCreateCluster(csClient *cs2015.Client, name, regionId, clusterType, clusterSpec, k8sVersion, runtimeName, runtimeVersion, vpcId, containerCidr, serviceCidr, secGroupId string, snatEntry, endpointPublicAccess bool, masterVswitchIds []string, tagKey, tagValue string, nodepools []*cs2015.Nodepool) (*string, error) { +func aliCreateCluster(csClient *cs2015.Client, name, regionId, clusterType, clusterSpec, k8sVersion, runtimeName, runtimeVersion, vpcId, containerCidr, serviceCidr, secGroupId string, snatEntry, endpointPublicAccess bool, masterVswitchIds []string, tagKey, tagValue string, tagList *[]cs2015.Tag, nodepools []*cs2015.Nodepool) (*string, error) { tags := []*cs2015.Tag{ - &cs2015.Tag{ + { Key: tea.String(tagKey), Value: tea.String(tagValue), }, } + // tagList가 nil이 아니고 요소가 있으면 tags에 추가 + if tagList != nil && len(*tagList) > 0 { + for _, tag := range *tagList { + newTag := &cs2015.Tag{ + Key: tag.Key, + Value: tag.Value, + } + tags = append(tags, newTag) + } + } + createClusterRequest := &cs2015.CreateClusterRequest{ Name: tea.String(name), RegionId: tea.String(regionId), diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/DiskHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/DiskHandler.go index 7f7e889f4..609b25658 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/DiskHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/DiskHandler.go @@ -648,6 +648,17 @@ func ExtractDiskDescribeInfo(aliDisk *ecs.Disk) (irs.DiskInfo, error) { aliDisk.CreationTime) diskInfo.OwnerVM = irs.IID{SystemId: aliDisk.InstanceId} diskStatus, errStatus := convertAlibabaDiskStatusToDiskStatus(aliDisk.Status) + + tagList := []irs.KeyValue{} + for _, aliTag := range aliDisk.Tags.Tag { + sTag := irs.KeyValue{} + sTag.Key = aliTag.Key + sTag.Value = aliTag.Value + + tagList = append(tagList, sTag) + } + diskInfo.TagList = tagList + if errStatus != nil { return irs.DiskInfo{}, errStatus } diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/KeyPairHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/KeyPairHandler.go index c297b93dd..58f28277c 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/KeyPairHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/KeyPairHandler.go @@ -115,6 +115,19 @@ func (keyPairHandler *AlibabaKeyPairHandler) CreateKey(keyPairReqInfo irs.KeyPai request.KeyPairName = keyPairReqInfo.IId.NameId + if keyPairReqInfo.TagList != nil && len(keyPairReqInfo.TagList) > 0 { + keyPairTags := []ecs.CreateKeyPairTag{} + for _, keyPairTag := range keyPairReqInfo.TagList { + tag0 := ecs.CreateKeyPairTag{ + Key: keyPairTag.Key, + Value: keyPairTag.Value, + } + keyPairTags = append(keyPairTags, tag0) + + } + request.Tag = &keyPairTags + } + // logger for HisCall callogger := call.GetLogger("HISCALL") callLogInfo := call.CLOUDLOGSCHEMA{ @@ -259,6 +272,17 @@ func ExtractKeyPairDescribeInfo(keyPair *ecs.KeyPair) (irs.KeyPairInfo, error) { Fingerprint: keyPair.KeyPairFingerPrint, } + tagList := []irs.KeyValue{} + cblogger.Info("eeeeeeeeeee", keyPair.Tags) + for _, aliTag := range keyPair.Tags.Tag { + kTag := irs.KeyValue{} + kTag.Key = aliTag.TagKey + kTag.Value = aliTag.TagValue + + tagList = append(tagList, kTag) + } + keyPairInfo.TagList = tagList + /* 2021-10-27 이슈#480에 의해 Local Key 로직 제거 // Local Keyfile 처리 keyPairPath := os.Getenv("CBSPIDER_ROOT") + CBKeyPairPath diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/MyImageHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/MyImageHandler.go index 1378cc2eb..596ac6c09 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/MyImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/MyImageHandler.go @@ -45,25 +45,41 @@ func (myImageHandler AlibabaMyImageHandler) SnapshotVM(snapshotReqInfo irs.MyIma request.RegionId = myImageHandler.Region.Region request.InstanceId = snapshotReqInfo.SourceVM.SystemId request.ImageName = snapshotReqInfo.IId.NameId + // 0717 tag 추가 - // TAG에 연관 instanceID set 할 것 - request.Tag = &[]ecs.CreateImageTag{ // Default Hidden Tags Info - { - Key: CBMetaDefaultTagName, // "cbCat", - Value: CBMetaDefaultTagValue, // "cbAlibaba", - }, - { - Key: IMAGE_TAG_DEFAULT, // "Name", - Value: snapshotReqInfo.IId.NameId, - }, - { - Key: IMAGE_TAG_SOURCE_VM, - Value: snapshotReqInfo.SourceVM.SystemId, - }, + if snapshotReqInfo.TagList != nil && len(snapshotReqInfo.TagList) > 0 { + + myImageTags := []ecs.CreateImageTag{} + for _, myImageTag := range snapshotReqInfo.TagList { + tag0 := ecs.CreateImageTag{ + Key: myImageTag.Key, + Value: myImageTag.Value, + } + myImageTags = append(myImageTags, tag0) + + } + request.Tag = &myImageTags } - //cblogger.Debug(request) + /// + + // TAG에 연관 instanceID set 할 것 + // request.Tag = &[]ecs.CreateImageTag{ // Default Hidden Tags Info + // { + // Key: CBMetaDefaultTagName, // "cbCat", + // Value: CBMetaDefaultTagValue, // "cbAlibaba", + // }, + // { + // Key: IMAGE_TAG_DEFAULT, // "Name", + // Value: snapshotReqInfo.IId.NameId, + // }, + // { + // Key: IMAGE_TAG_SOURCE_VM, + // Value: snapshotReqInfo.SourceVM.SystemId, + // }, + // } result, err := myImageHandler.Client.CreateImage(request) + hiscallInfo.ElapsedTime = call.Elapsed(start) if err != nil { cblogger.Error(err) @@ -237,6 +253,15 @@ func (myImageHandler AlibabaMyImageHandler) DeleteMyImage(myImageIID irs.IID) (b func ExtractMyImageDescribeInfo(aliMyImage *ecs.Image) (irs.MyImageInfo, error) { returnMyImageInfo := irs.MyImageInfo{} + tagList := []irs.KeyValue{} + for _, aliTag := range aliMyImage.Tags.Tag { + sTag := irs.KeyValue{} + sTag.Key = aliTag.Key + sTag.Value = aliTag.Value + + tagList = append(tagList, sTag) + } + returnMyImageInfo.TagList = tagList //IId IID // {NameId, SystemId} // //SourceVM IID diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/NLBHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/NLBHandler.go index 8779e04c1..7e4957831 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/NLBHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/NLBHandler.go @@ -3,6 +3,7 @@ package resources import ( "encoding/json" "errors" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" "github.com/aliyun/alibaba-cloud-sdk-go/services/slb" @@ -113,6 +114,20 @@ func (NLBHandler *AlibabaNLBHandler) CreateNLB(nlbReqInfo irs.NLBInfo) (irs.NLBI // If InstanceChargeType is set to PayByCLCU, the LoadBalancerSpec parameter is invalid and you do not need to set this parameter. loadBalancerRequest.LoadBalancerSpec = "slb.s1.small" // required + if nlbReqInfo.TagList != nil && len(nlbReqInfo.TagList) > 0 { + + nlbTags := []slb.CreateLoadBalancerTag{} + for _, nlbTag := range nlbReqInfo.TagList { + tag0 := slb.CreateLoadBalancerTag{ + Key: nlbTag.Key, + Value: nlbTag.Value, + } + nlbTags = append(nlbTags, tag0) + + } + loadBalancerRequest.Tag = &nlbTags + } + response, err := NLBHandler.Client.CreateLoadBalancer(loadBalancerRequest) if err != nil { callLogInfo.ErrorMSG = err.Error() diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/SecurityHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/SecurityHandler.go index 13a25c266..8f987447c 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/SecurityHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/SecurityHandler.go @@ -47,6 +47,25 @@ func (securityHandler *AlibabaSecurityHandler) CreateSecurity(securityReqInfo ir request.SecurityGroupName = securityReqInfo.IId.NameId request.VpcId = securityReqInfo.VpcIID.SystemId request.SecurityGroupType = "enterprise" + + /// 0717 /// + + if securityReqInfo.TagList != nil && len(securityReqInfo.TagList) > 0 { + + sgTags := []ecs.CreateSecurityGroupTag{} + for _, sgTag := range securityReqInfo.TagList { + tag0 := ecs.CreateSecurityGroupTag{ + Key: sgTag.Key, + Value: sgTag.Value, + } + sgTags = append(sgTags, tag0) + + } + request.Tag = &sgTags + } + + /// 0717 /// + cblogger.Debugf("Security group creation request information", request) // logger for HisCall diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VMHandler.go index 29de20fb8..8a7a6b24c 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VMHandler.go @@ -208,6 +208,22 @@ func (vmHandler *AlibabaVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, request.InternetChargeType = "PayByBandwidth" //Public Ip요금 방식을 1시간 단위(PayByBandwidth) 요금으로 설정 / PayByTraffic(기본값) : 1GB단위 시간당 트래픽 요금 청구 request.InternetMaxBandwidthOut = requests.Integer("5") // 0보다 크면 Public IP가 할당 됨 - 최대 아웃 바운드 공용 대역폭 단위 : Mbit / s 유효한 값 : 0 ~ 100 + /// 0717 /// + + if vmReqInfo.TagList != nil && len(vmReqInfo.TagList) > 0 { + vmTags := []ecs.RunInstancesTag{} + for _, vmTag := range vmReqInfo.TagList { + tag0 := ecs.RunInstancesTag{ + Key: vmTag.Key, + Value: vmTag.Value, + } + vmTags = append(vmTags, tag0) + + } + request.Tag = &vmTags + } + + /// 0717 /// //============================= // instance 사용 가능 검사 //============================= @@ -858,6 +874,16 @@ func (vmHandler *AlibabaVMHandler) ExtractDescribeInstances(instanceInfo *ecs.In KeyValueList: []irs.KeyValue{{Key: "", Value: ""}}, } + tagList := []irs.KeyValue{} + + for _, aliTag := range instanceInfo.Tags.Tag { + sTag := irs.KeyValue{} + sTag.Key = aliTag.TagKey + sTag.Value = aliTag.TagValue + + tagList = append(tagList, sTag) + } + vmInfo.TagList = tagList // Platform 정보 추가 if instanceInfo.OSType == "windows" { diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VPCHandler.go index 4eac1a0dc..64e123d30 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VPCHandler.go @@ -19,8 +19,9 @@ import ( "strings" "time" + //vpc "github.com/alibabacloud-go/vpc-20160428/v6/client" + "github.com/aliyun/alibaba-cloud-sdk-go/services/vpc" - //"github.com/aliyun/alibaba-cloud-sdk-go/services/vpc" call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" @@ -45,6 +46,22 @@ func (VPCHandler *AlibabaVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.V request.VpcName = vpcReqInfo.IId.NameId request.CidrBlock = vpcReqInfo.IPv4_CIDR + // tag 받아서 배열로 추가 + + if vpcReqInfo.TagList != nil && len(vpcReqInfo.TagList) > 0 { + + vpcTags := []vpc.CreateVpcTag{} + for _, vpcTag := range vpcReqInfo.TagList { + tag0 := vpc.CreateVpcTag{ + Key: vpcTag.Key, + Value: vpcTag.Value, + } + vpcTags = append(vpcTags, tag0) + + } + request.Tag = &vpcTags + } + // logger for HisCall callogger := call.GetLogger("HISCALL") callLogInfo := call.CLOUDLOGSCHEMA{ @@ -99,16 +116,17 @@ func (VPCHandler *AlibabaVPCHandler) CreateVPC(vpcReqInfo irs.VPCReqInfo) (irs.V } retVpcInfo.IId.NameId = vpcReqInfo.IId.NameId // NameId는 요청 받은 값으로 리턴해야 함. - if vpcReqInfo.TagList != nil && len(vpcReqInfo.TagList) > 0 { - //tagHandler.Client, tagHandler.Region, resType, resIID, key - for _, vpcTag := range vpcReqInfo.TagList { - response, err := AddVpcTags(VPCHandler.Client, VPCHandler.Region, irs.RSType("VPC"), retVpcInfo.IId, vpcTag) - if err != nil { - cblogger.Error(errVpc) - } - cblogger.Debug("vpc add tag response ", response) - } - } + // if vpcReqInfo.TagList != nil && len(vpcReqInfo.TagList) > 0 { + // //tagHandler.Client, tagHandler.Region, resType, resIID, key + // for _, vpcTag := range vpcReqInfo.TagList { + // response, err := AddVpcTags(VPCHandler.Client, VPCHandler.Region, irs.RSType("VPC"), retVpcInfo.IId, vpcTag) + // cblogger.Info("AddVpcTage", response) + // if err != nil { + // cblogger.Error(errVpc) + // } + // cblogger.Debug("vpc add tag response ", response) + // } + // } return retVpcInfo, nil } @@ -135,6 +153,24 @@ func (VPCHandler *AlibabaVPCHandler) CreateSubnet(vpcId string, reqSubnetInfo ir request.CidrBlock = reqSubnetInfo.IPv4_CIDR request.VSwitchName = reqSubnetInfo.IId.NameId request.ZoneId = zoneId + // 0717 tag 추가 + + if reqSubnetInfo.TagList != nil && len(reqSubnetInfo.TagList) > 0 { + + subnetTags := []vpc.CreateVSwitchTag{} + for _, vpcTag := range reqSubnetInfo.TagList { + tag0 := vpc.CreateVSwitchTag{ + Key: vpcTag.Key, + Value: vpcTag.Value, + } + subnetTags = append(subnetTags, tag0) + + } + request.Tag = &subnetTags + } + + ///// + cblogger.Info(request) // logger for HisCall @@ -224,6 +260,16 @@ func ExtractVpcDescribeInfo(vpcInfo *vpc.Vpc) irs.VPCInfo { IPv4_CIDR: vpcInfo.CidrBlock, } + tagList := []irs.KeyValue{} + for _, aliTag := range vpcInfo.Tags.Tag { + sTag := irs.KeyValue{} + sTag.Key = aliTag.Key + sTag.Value = aliTag.Value + + tagList = append(tagList, sTag) + } + aliVpcInfo.TagList = tagList + keyValueList := []irs.KeyValue{ {Key: "IsDefault", Value: strconv.FormatBool(vpcInfo.IsDefault)}, {Key: "Status", Value: vpcInfo.Status}, @@ -495,6 +541,20 @@ func ExtractSubnetDescribeInfo(subnetInfo vpc.VSwitch) irs.SubnetInfo { IPv4_CIDR: subnetInfo.CidrBlock, Zone: subnetInfo.ZoneId, } + ///// + // 0717 tag 추출 + + tagList := []irs.KeyValue{} + for _, aliTag := range subnetInfo.Tags.Tag { + sTag := irs.KeyValue{} + sTag.Key = aliTag.Key + sTag.Value = aliTag.Value + + tagList = append(tagList, sTag) + } + vNetworkInfo.TagList = tagList + + ///// keyValueList := []irs.KeyValue{ {Key: "Status", Value: subnetInfo.Status}, From 052e9b5e62da6dec43450148a39dd8a3af371acc Mon Sep 17 00:00:00 2001 From: SungWoongz Date: Wed, 24 Jul 2024 14:18:23 +0900 Subject: [PATCH 38/56] edit test_resource.go for handle tag, disk, myimage, cluster --- .../alibaba/connect/AlibabaCloudConnection.go | 2 +- .../drivers/alibaba/main/Test_Resources.go | 394 ++++++++++++++++-- .../drivers/alibaba/main/conf/Test_Config.go | 9 + 3 files changed, 371 insertions(+), 34 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/connect/AlibabaCloudConnection.go b/cloud-control-manager/cloud-driver/drivers/alibaba/connect/AlibabaCloudConnection.go index 9314262f7..01382edb6 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/connect/AlibabaCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/connect/AlibabaCloudConnection.go @@ -175,6 +175,6 @@ func (cloudConn *AlibabaCloudConnection) CreatePriceInfoHandler() (irs.PriceInfo func (cloudConn *AlibabaCloudConnection) CreateTagHandler() (irs.TagHandler, error) { cblogger.Info("Start") - handler := alirs.AlibabaTagHandler{cloudConn.Region, cloudConn.VMClient, cloudConn.Cs2015Client} + handler := alirs.AlibabaTagHandler{cloudConn.Region, cloudConn.VMClient, cloudConn.Cs2015Client, cloudConn.VpcClient} return &handler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go index 30d9504c4..322639701 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go @@ -380,9 +380,11 @@ func handleSecurity() { case 2: cblogger.Infof("[%s] Security 생성 테스트", securityName) + tag1 := irs.KeyValue{Key: "", Value: ""} securityReqInfo := irs.SecurityReqInfo{ - IId: irs.IID{NameId: securityName}, - VpcIID: irs.IID{SystemId: vpcId}, + IId: irs.IID{NameId: securityName}, + VpcIID: irs.IID{SystemId: vpcId}, + TagList: []irs.KeyValue{tag1}, SecurityRules: &[]irs.SecurityRuleInfo{ //보안 정책 설정 //CIDR 테스트 /*{ @@ -836,6 +838,7 @@ func handleKeyPair() { keyPairName := "" //keyPairName := config.Aws.KeyName + tag1 := irs.KeyValue{Key: "aaa", Value: "bbb"} for { fmt.Println("KeyPair Management") @@ -873,7 +876,8 @@ func handleKeyPair() { case 2: cblogger.Infof("[%s] 키 페어 생성 테스트", keyPairName) keyPairReqInfo := irs.KeyPairReqInfo{ - IId: irs.IID{NameId: keyPairName}, + IId: irs.IID{NameId: keyPairName}, + TagList: []irs.KeyValue{tag1}, } result, err := handler.CreateKey(keyPairReqInfo) if err != nil { @@ -944,7 +948,9 @@ func handleVPC() { cblogger.Debug(subnetReqInfo) cblogger.Debug(subnetReqVpcInfo) cblogger.Debug(reqSubnetId) - + // tag1 := irs.KeyValue{Key: "aaa", Value: "bbb"} + // stag1 := irs.KeyValue{Key: "saa", Value: "sbb"} + // stag2 := irs.KeyValue{Key: "scc", Value: "sdd"} vpcReqInfo := irs.VPCReqInfo{ IId: irs.IID{NameId: "New-CB-VPC"}, IPv4_CIDR: "10.0.0.0/16", @@ -952,20 +958,23 @@ func handleVPC() { { IId: irs.IID{NameId: "New-CB-Subnet"}, IPv4_CIDR: "10.0.1.0/24", + // TagList: []irs.KeyValue{stag1}, }, { IId: irs.IID{NameId: "New-CB-Subnet2"}, IPv4_CIDR: "10.0.2.0/24", + // TagList: []irs.KeyValue{stag2}, }, }, + // TagList: []irs.KeyValue{tag1}, //Id: "subnet-044a2b57145e5afc5", //Name: "CB-VNet-Subnet", // 웹 도구 등 외부에서 전달 받지 않고 드라이버 내부적으로 자동 구현때문에 사용하지 않음. //CidrBlock: "10.0.0.0/16", //CidrBlock: "192.168.0.0/16", } - reqVpcId := irs.IID{SystemId: "vpc-6we11xwqjc9tyma5i68z0"} + reqVpcId := irs.IID{SystemId: "vpc-j6c64o9vym4zqqmvx3j3c"} for { fmt.Println("Handler Management") @@ -1189,28 +1198,35 @@ func handleVM() { return case 1: + tag1 := irs.KeyValue{Key: "vaa", Value: "vbb"} + vmReqInfo := irs.VMReqInfo{ - IId: irs.IID{NameId: ""}, - //ImageIID: irs.IID{SystemId: "aliyun_3_x64_20G_alibase_20210425.vhd"}, + // IId: irs.IID{NameId: ""}, + // ImageIID: irs.IID{SystemId: "aliyun_3_x64_20G_alibase_20210425.vhd"}, + ImageIID: irs.IID{SystemId: "aliyun_2_1903_x64_20G_dengbao_alibase_20240705.vhd"}, //ImageIID: irs.IID{SystemId: "aliyun_2_1903_x64_20G_alibase_20200324.vhd"}, //ImageIID: irs.IID{SystemId: "ubuntu_18_04_x64_20G_alibase_20210318.vhd"}, - ImageIID: irs.IID{SystemId: "ubuntu_18_04_x64_20G_alibase_20210420.vhd"}, - //VpcIID: irs.IID{SystemId: "vpc-0jl4l19l51gn2exrohgci"}, + // ImageIID: irs.IID{SystemId: "ubuntu_18_04_x64_20G_alibase_20210420.vhd"}, + VpcIID: irs.IID{SystemId: "vpc-j6cjneevz77bun1f0hodv"}, //SubnetIID: irs.IID{SystemId: "vsw-0jlj155cbwhjumtipnm6d"}, - SubnetIID: irs.IID{SystemId: ""}, //Tokyo Zone B + // SubnetIID: irs.IID{SystemId: ""}, //Tokyo Zone B + SubnetIID: irs.IID{SystemId: "vsw-j6c37ennotvnfcvuw23ku"}, //hongkong-c //SecurityGroupIIDs: []irs.IID{{SystemId: ""}, {SystemId: ""}}, - SecurityGroupIIDs: []irs.IID{{SystemId: ""}}, + SecurityGroupIIDs: []irs.IID{{SystemId: "sg-j6cauy6gbpy26deg8iz9"}}, // 홍콩 리전 //VMSpecName: "ecs.t5-lc2m1.nano", //VMSpecName: "ecs.g6.large", //cn-wulanchabu 리전 - VMSpecName: "ecs.t5-lc2m1.nano", //도쿄리전 - KeyPairIID: irs.IID{SystemId: ""}, + // VMSpecName: "ecs.t5-lc2m1.nano", //도쿄리전 + VMSpecName: "ecs.t5-lc2m1.nano", //홍콩리전 + // KeyPairIID: irs.IID{SystemId: ""}, + KeyPairIID: irs.IID{SystemId: "keypair-honkong-cqbnlg6iuvoi0eb89lq0"}, //홍콩리전 //VMUserId: "root", //root만 가능 //VMUserPasswd: "Cbuser!@#", //대문자 소문자 모두 사용되어야 함. 그리고 숫자나 특수 기호 중 하나가 포함되어야 함. - RootDiskType: "cloud_efficiency", //cloud / cloud_efficiency / cloud_ssd / cloud_essd - RootDiskSize: "default", + // RootDiskType: "cloud_efficiency", //cloud / cloud_efficiency / cloud_ssd / cloud_essd + // RootDiskSize: "default", //RootDiskType: "cloud_ssd", //cloud / cloud_efficiency / cloud_ssd / cloud_essd //RootDiskSize: "22", + TagList: []irs.KeyValue{tag1}, } vmInfo, err := vmHandler.StartVM(vmReqInfo) @@ -1330,20 +1346,22 @@ func handleNLB() { } handler := ResourceHandler.(irs.NLBHandler) cblogger.Info(handler) + tag1 := irs.KeyValue{Key: "naa", Value: "vbb"} + nlbReqInfo := irs.NLBInfo{ // TCP IId: irs.IID{NameId: ""}, - VpcIID: irs.IID{SystemId: ""}, + VpcIID: irs.IID{SystemId: "j6cjneevz77bun1f0hodv"}, Type: "PUBLIC", Listener: irs.ListenerInfo{Protocol: "TCP", Port: "80"}, - HealthChecker: irs.HealthCheckerInfo{Protocol: "HTTP", Port: "80", Interval: 5, Timeout: 2, Threshold: 3}, + HealthChecker: irs.HealthCheckerInfo{Protocol: "TCP", Port: "80", Interval: 5, Timeout: 2, Threshold: 3}, VMGroup: irs.VMGroupInfo{ Protocol: "TCP", Port: "80", - VMs: &[]irs.IID{{SystemId: ""}, {SystemId: ""}}, + VMs: &[]irs.IID{{SystemId: "i-j6camjyolcjjhbs3ack7"}, {SystemId: "i-j6cgtrzdfo2tvrjzjw34"}}, }, - + TagList: []irs.KeyValue{tag1}, // UDP //IId: irs.IID{NameId: ""}, //VpcIID: irs.IID{SystemId: ""}, @@ -1659,10 +1677,12 @@ func handleTagInfo() { panic(err) } - //resourceType := irs.RSType("VM") - // resourceIID := irs.IID{NameId: "vm-issue-test", SystemId: ""} - resourceType := irs.RSType("CLUSTER") - resourceIID := irs.IID{NameId: "cs-issue-test", SystemId: ""} + // resourceType := irs.RSType("VM") + resourceType := irs.ALL + + resourceIID := irs.IID{NameId: "", SystemId: "i-j6cd7rv72uwjyx8qy304"} + // resourceType := irs.RSType("CLUSTER") + // resourceIID := irs.IID{NameId: "cs-issue-test", SystemId: ""} if inputCnt == 1 { switch commandNum { @@ -1690,15 +1710,15 @@ func handleTagInfo() { cblogger.Info(result) } case 3: - tagName := "tagntest3" - tagName = "" + tagName := "aaa" + result, err := handler.FindTag(resourceType, tagName) if err != nil { cblogger.Info(" Tag 조회 실패 : ", err) } else { cblogger.Info("FindTag 조회 결과") - //spew.Dump(result) - cblogger.Info(result) + spew.Dump(result) + // cblogger.Info(result) } case 4: newTag := irs.KeyValue{} @@ -1727,23 +1747,331 @@ func handleTagInfo() { } } +func handleDisk() { + cblogger.Debug("Start handleDisk Test") + ResourceHandler, err := testconf.GetResourceHandler("Disk") + if err != nil { + //panic(err) + cblogger.Error(err) + } + handler := ResourceHandler.(irs.DiskHandler) + cblogger.Info(handler) + + tag1 := irs.KeyValue{Key: "daa", Value: "dbb"} + diskReqInfo := irs.DiskInfo{ + // IID: irs.IID{}, + TagList: []irs.KeyValue{tag1}, + } + + for { + fmt.Println("Handler Management") + fmt.Println("0. Quit") + fmt.Println("1. Disk List") + fmt.Println("2. GetDisk ") + fmt.Println("3. CreateDisk ") + fmt.Println("4. AddDisk ") + fmt.Println("5. RemoveDisk ") + + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + panic(err) + } + // resourceType := irs.RSType("disk") + resourceIID := irs.IID{NameId: "", SystemId: "i-j6cd7rv72uwjyx8qy304"} + // resourceType := irs.RSType("CLUSTER") + // resourceIID := irs.IID{NameId: "cs-issue-test", SystemId: ""} + + if inputCnt == 1 { + switch commandNum { + case 0: + return + case 1: + + result, err := handler.ListDisk() + if err != nil { + cblogger.Infof(" Tag 목록 조회 실패 : ", err) + } else { + cblogger.Info("Tag 목록 조회 결과") + spew.Dump(result) + } + + case 2: + // tagName := "tagntest3" + // tagName = "" + result, err := handler.GetDisk(resourceIID) + if err != nil { + cblogger.Info(" Tag 조회 실패 : ", err) + } else { + cblogger.Info("GetTag 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + case 3: + // tagName := "tagntest3" + // tagName = "" + result, err := handler.CreateDisk(diskReqInfo) + if err != nil { + cblogger.Info(" Tag 조회 실패 : ", err) + } else { + cblogger.Info("FindTag 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + // case 4: + // newTag := irs.KeyValue{} + // newTag.Key = "addKeyT1" + // newTag.Value = "addValueT1" + // result, err := handler.AddTag(resourceType, resourceIID, newTag) + // if err != nil { + // cblogger.Info(" Tag 조회 실패 : ", err) + // } else { + // cblogger.Info("AddTag 조회 결과") + // //spew.Dump(result) + // cblogger.Info(result) + // } + // case 5: + // tagName := "addKeyT1" + // result, err := handler.RemoveTag(resourceType, resourceIID, tagName) + // if err != nil { + // cblogger.Info(" Tag 조회 실패 : ", err) + // } else { + // cblogger.Info("RemoveTag 조회 결과") + // //spew.Dump(result) + // cblogger.Info(result) + // } + } + } + } +} + +func handleMyImage() { + cblogger.Debug("Start handleMyImage Test") + ResourceHandler, err := testconf.GetResourceHandler("MyImage") + if err != nil { + //panic(err) + cblogger.Error(err) + } + handler := ResourceHandler.(irs.MyImageHandler) + cblogger.Info(handler) + + tag1 := irs.KeyValue{Key: "daa", Value: "dbb"} + snapshotReqInfo := irs.MyImageInfo{ + SourceVM: irs.IID{SystemId: "i-j6camjyolcjjhbs3ack7"}, + IId: irs.IID{NameId: "tagtestimage2"}, + TagList: []irs.KeyValue{tag1}, + } + + for { + fmt.Println("Handler Management") + fmt.Println("0. Quit") + fmt.Println("1. MyImage List") + fmt.Println("2. GetMyImage ") + fmt.Println("3. CreateMyImage ") + + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + panic(err) + } + // resourceType := irs.RSType("disk") + resourceIID := irs.IID{NameId: "", SystemId: "i-j6cd7rv72uwjyx8qy304"} + // resourceType := irs.RSType("CLUSTER") + // resourceIID := irs.IID{NameId: "cs-issue-test", SystemId: ""} + + if inputCnt == 1 { + switch commandNum { + case 0: + return + case 1: + + result, err := handler.ListMyImage() + if err != nil { + cblogger.Infof(" MyImage 목록 조회 실패 : ", err) + } else { + cblogger.Info("MyImage 목록 조회 결과") + spew.Dump(result) + } + + case 2: + // tagName := "tagntest3" + // tagName = "" + result, err := handler.GetMyImage(resourceIID) + if err != nil { + cblogger.Info(" MyImage 조회 실패 : ", err) + } else { + cblogger.Info("GetMyImage 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + case 3: + // tagName := "tagntest3" + // tagName = "" + result, err := handler.SnapshotVM(snapshotReqInfo) + if err != nil { + cblogger.Info(" MyImage 조회 실패 : ", err) + } else { + cblogger.Info("CreateMyImage 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + // case 4: + // newTag := irs.KeyValue{} + // newTag.Key = "addKeyT1" + // newTag.Value = "addValueT1" + // result, err := handler.AddTag(resourceType, resourceIID, newTag) + // if err != nil { + // cblogger.Info(" Tag 조회 실패 : ", err) + // } else { + // cblogger.Info("AddTag 조회 결과") + // //spew.Dump(result) + // cblogger.Info(result) + // } + // case 5: + // tagName := "addKeyT1" + // result, err := handler.RemoveTag(resourceType, resourceIID, tagName) + // if err != nil { + // cblogger.Info(" Tag 조회 실패 : ", err) + // } else { + // cblogger.Info("RemoveTag 조회 결과") + // //spew.Dump(result) + // cblogger.Info(result) + // } + } + } + } +} + +func handleCluster() { + cblogger.Debug("Start Cluster Test") + ResourceHandler, err := testconf.GetResourceHandler("Cluster") + if err != nil { + //panic(err) + cblogger.Error(err) + } + handler := ResourceHandler.(irs.ClusterHandler) + cblogger.Info(handler) + + vpcID := "vpc-j6ctiyol6osnuy9sfj2xm" + vswitchID := "vsw-j6cgu2z3paapkojcj918q" + securityGroupIIDs := "sg-j6cauy6gbpy261k9k74c" + + tag1 := irs.KeyValue{Key: "caa", Value: "cbb"} + + clusterReqInfo := irs.ClusterInfo{ + IId: irs.IID{NameId: "tagtestcluster22"}, + Network: irs.NetworkInfo{ + VpcIID: irs.IID{SystemId: vpcID}, + SubnetIIDs: []irs.IID{ + {SystemId: vswitchID}, + }, + SecurityGroupIIDs: []irs.IID{ + {SystemId: securityGroupIIDs}, + }, + // Optionally, add other network-related fields if necessary + }, + TagList: []irs.KeyValue{tag1}, + } + + for { + fmt.Println("Handler Management") + fmt.Println("0. Quit") + fmt.Println("1. Cluster List") + fmt.Println("2. GetCluster ") + fmt.Println("3. CreateCluster") + + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + panic(err) + } + // resourceType := irs.RSType("disk") + resourceIID := irs.IID{NameId: "", SystemId: "i-j6cd7rv72uwjyx8qy304"} + // resourceType := irs.RSType("CLUSTER") + // resourceIID := irs.IID{NameId: "cs-issue-test", SystemId: ""} + + if inputCnt == 1 { + switch commandNum { + case 0: + return + case 1: + + result, err := handler.ListCluster() + if err != nil { + cblogger.Infof(" MyImage 목록 조회 실패 : ", err) + } else { + cblogger.Info("MyImage 목록 조회 결과") + spew.Dump(result) + } + case 2: + // tagName := "tagntest3" + // tagName = "" + result, err := handler.GetCluster(resourceIID) + if err != nil { + cblogger.Info(" MyImage 조회 실패 : ", err) + } else { + cblogger.Info("GetMyImage 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + case 3: + // tagName := "tagntest3" + // tagName = "" + result, err := handler.CreateCluster(clusterReqInfo) + if err != nil { + cblogger.Info(" MyImage 조회 실패 : ", err) + } else { + cblogger.Info("CreateMyImage 조회 결과") + //spew.Dump(result) + cblogger.Info(result) + } + // case 4: + // newTag := irs.KeyValue{} + // newTag.Key = "addKeyT1" + // newTag.Value = "addValueT1" + // result, err := handler.AddTag(resourceType, resourceIID, newTag) + // if err != nil { + // cblogger.Info(" Tag 조회 실패 : ", err) + // } else { + // cblogger.Info("AddTag 조회 결과") + // //spew.Dump(result) + // cblogger.Info(result) + // } + // case 5: + // tagName := "addKeyT1" + // result, err := handler.RemoveTag(resourceType, resourceIID, tagName) + // if err != nil { + // cblogger.Info(" Tag 조회 실패 : ", err) + // } else { + // cblogger.Info("RemoveTag 조회 결과") + // //spew.Dump(result) + // cblogger.Info(result) + // } + } + } + } +} + func main() { cblogger.Info("Alibaba Cloud Resource Test") cblogger.Debug("Debug mode") - handleVPC() //VPC + // handleVPC() //VPC //handleVMSpec() //handleImage() //AMI - //handleSecurity() - //handleKeyPair() - //handleVM() - //handleNLB() + // handleSecurity() + // handleKeyPair() + // handleVM() + // handleNLB() + // handleDisk() + // handleMyImage() + // handleCluster() //handlePublicIP() // PublicIP 생성 후 conf //handleVNic() //Lancard //handleRegionZone() //handlePriceInfo() - //handleTagInfo() + handleTagInfo() /* //StartTime := "2020-05-07T01:35:00Z" StartTime := "2020-05-07T01:35Z" diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/main/conf/Test_Config.go b/cloud-control-manager/cloud-driver/drivers/alibaba/main/conf/Test_Config.go index d2a706b7b..b63d73e49 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/main/conf/Test_Config.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/main/conf/Test_Config.go @@ -68,6 +68,7 @@ type Config struct { func ReadConfigFile() Config { // Set Environment Value of Project Root Path rootPath := os.Getenv("CBSPIDER_PATH") + cblogger.Debugf("Test Data 설정파일 : [%s]", rootPath+"/config/config.yaml") data, err := ioutil.ReadFile(rootPath + "/config/config.yaml") @@ -139,6 +140,14 @@ func GetResourceHandler(handlerType string) (interface{}, error) { resourceHandler, err = cloudConnection.CreateRegionZoneHandler() case "PriceInfo": resourceHandler, err = cloudConnection.CreatePriceInfoHandler() + case "Disk": + resourceHandler, err = cloudConnection.CreateDiskHandler() + case "MyImage": + resourceHandler, err = cloudConnection.CreateMyImageHandler() + case "Cluster": + resourceHandler, err = cloudConnection.CreateClusterHandler() + case "Tag": + resourceHandler, err = cloudConnection.CreateTagHandler() } if err != nil { From 669f50646e50ee26a5372107b60a40689013b9b4 Mon Sep 17 00:00:00 2001 From: SungWoongz Date: Wed, 24 Jul 2024 14:18:51 +0900 Subject: [PATCH 39/56] =?UTF-8?q?ALL,=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20?= =?UTF-8?q?=EB=B3=84=20find=20tag=20=EC=B6=94=EA=B0=80=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alibaba/resources/CommonAlibabaFunc.go | 65 +-- .../alibaba/resources/CommonHandler.go | 264 +++++++++++- .../drivers/alibaba/resources/TagHandler.go | 384 ++++++++++-------- 3 files changed, 492 insertions(+), 221 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go index 04ec05622..94ed1317f 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go @@ -286,25 +286,26 @@ func GetAlibabaApiVPCEndpoint(regionId string, productCode string) string { // Alibaba에서 사용되는 리소스별 api product type func GetAlibabaProductCode(resType irs.RSType) (string, error) { switch resType { - case irs.RSType("VM"): + case irs.RSType("VM"), irs.VM: return "ecs", nil - case irs.RSType("VPC"): + case irs.RSType("VPC"), irs.VPC: return "vpc", nil - case irs.RSType("SUBNET"): - return "ecs", nil - case irs.RSType("SG"): + case irs.RSType("SUBNET"), irs.SUBNET: + // return "ecs", nil + return "vpc", nil + case irs.RSType("SG"), irs.SG: return "ecs", nil - case irs.RSType("KEY"): + case irs.RSType("KEY"), irs.KEY: return "ecs", nil - case irs.RSType("NLB"): + case irs.RSType("NLB"), irs.NLB: return "slb", nil - case irs.RSType("DISK"): + case irs.RSType("DISK"), irs.DISK: return "ecs", nil - case irs.RSType("MYIMAGE"): + case irs.RSType("MYIMAGE"), irs.MYIMAGE: return "ecs", nil - case irs.RSType("CLUSTER"): + case irs.RSType("CLUSTER"), irs.CLUSTER: return "ack", nil - case irs.RSType("NODEGROUP"): + case irs.RSType("NODEGROUP"), irs.NODEGROUP: return "ack", nil default: //return "", nil @@ -315,24 +316,28 @@ func GetAlibabaProductCode(resType irs.RSType) (string, error) { // cb-spider의 resourceType 을 alibaba의 resourceType으로 func GetAlibabaResourceType(resType irs.RSType) (string, error) { switch resType { - case irs.RSType("VM"): + case irs.RSType("VM"), irs.VM: return "instance", nil - // case irs.RSType("VPC"): - // return "vpc", nil - // case irs.RSType("SUBNET"): - // return "ecs", nil - case irs.RSType("SG"): + case irs.RSType("VPC"), irs.VPC: + return "vpc", nil + case irs.RSType("SUBNET"), irs.SUBNET: + // return "ecs", nil + return "vpc", nil + case irs.RSType("SG"), irs.SG: return "securitygroup", nil - case irs.RSType("KEY"): + case irs.RSType("KEY"), irs.KEY: return "keypair", nil // case irs.RSType("NLB"): // return "slb", nil - case irs.RSType("DISK"): + case irs.RSType("DISK"), irs.DISK: return "disk", nil - case irs.RSType("MYIMAGE"): - return "snapshot", nil - case irs.RSType("CLUSTER"): + case irs.RSType("MYIMAGE"), irs.MYIMAGE: + // return "snapshot", nil + return "ecs", nil + case irs.RSType("CLUSTER"), irs.CLUSTER: return "CLUSTER", nil + case irs.RSType("ALL"), irs.ALL: + return "", nil // case irs.RSType("NODEGROUP"): // return "", nil default: @@ -348,27 +353,29 @@ func GetAlibabaResourceType(resType irs.RSType) (string, error) { return "", errors.New("not found ResourceType " + string(resType)) } -// resource Type별로 바로보는 api가 다름. ( ecs, bss, ... ) +// resource Type별로 바라보는 api가 다름. ( ecs, bss, ... ) func GetAliTargetApi(resType irs.RSType) (string, error) { switch resType { case irs.RSType("VM"): return "ecs", nil - // case irs.RSType("VPC"): - // return "vpc", nil - // case irs.RSType("SUBNET"): - // return "ecs", nil + case irs.RSType("VPC"): + return "vpc", nil + case irs.RSType("SUBNET"): + return "vpc", nil case irs.RSType("SG"): return "ecs", nil case irs.RSType("KEY"): return "ecs", nil - // case irs.RSType("NLB"): - // return "slb", nil + case irs.RSType("NLB"): + return "slb", nil case irs.RSType("DISK"): return "ecs", nil case irs.RSType("MYIMAGE"): return "ecs", nil case irs.RSType("CLUSTER"): return "cs", nil + case irs.RSType("ALL"): + return "all", nil // case irs.RSType("NODEGROUP"): // return "", nil default: diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go index 6d627b90c..2eeb8d2a4 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go @@ -358,18 +358,20 @@ func DescribeImages(client *ecs.Client, regionInfo idrv.RegionInfo, imageIIDs [] request.RegionId = regionInfo.Region var imageIIDList []string - for _, imageIID := range imageIIDs { - imageIIDList = append(imageIIDList, imageIID.SystemId) - } + if imageIIDs != nil { + for _, imageIID := range imageIIDs { + imageIIDList = append(imageIIDList, imageIID.SystemId) + } - if len(imageIIDList) > 0 { - request.ImageId = strings.Join(imageIIDList, ",") + if len(imageIIDList) > 0 { + request.ImageId = strings.Join(imageIIDList, ",") + } } - // MyImage 여부 if isMyImage { request.ImageOwnerAlias = "self" } + //cblogger.Debug(request) result, err := client.DescribeImages(request) if err != nil { @@ -1033,8 +1035,11 @@ func DescribeDescribeVpcTags(client *vpc.Client, regionInfo idrv.RegionInfo, res // EcsRequest : Elastic Compute Service(ECS) func CallEcsRequest(resType irs.RSType, client *ecs.Client, regionInfo idrv.RegionInfo, apiName string, queryParams map[string]string) (*responses.CommonResponse, error) { regionID := regionInfo.Region - - apiProductCode, err := GetAlibabaProductCode(irs.RSType(resType)) + apiProductCode, err := GetAlibabaProductCode(resType) + if err != nil { + cblogger.Error(err.Error()) + return nil, err + } // call logger set callogger := call.GetLogger("HISCALL") @@ -1052,26 +1057,23 @@ func CallEcsRequest(resType irs.RSType, client *ecs.Client, regionInfo idrv.Regi request.Method = "POST" request.Scheme = "https" // https | http - //request.Domain = "ecs.cn-hongkong.aliyuncs.com" request.Domain = GetAlibabaApiEndPoint(regionID, apiProductCode) request.Version = "2014-05-26" request.ApiName = apiName - request.QueryParams["RegionId"] = regionID + request.QueryParams = queryParams - // Tag가 있으면 - if queryParams != nil { - request.QueryParams = queryParams - } + cblogger.Debug("API Request : ", request) callLogStart := call.Start() response, err := client.ProcessCommonRequest(request) - - callLogInfo.ElapsedTime = call.Elapsed(callLogStart) - callogger.Info(call.String(callLogInfo)) if err != nil { cblogger.Error(err.Error()) return nil, err } + + callLogInfo.ElapsedTime = call.Elapsed(callLogStart) + callogger.Info(call.String(callLogInfo)) + cblogger.Debug(response.GetHttpContentString()) return response, nil } @@ -1097,9 +1099,8 @@ func CallVpcRequest(resType irs.RSType, client *vpc.Client, regionInfo idrv.Regi request.Method = "POST" request.Scheme = "https" // https | http - //request.Domain = "ecs.cn-hongkong.aliyuncs.com" request.Domain = GetAlibabaApiEndPoint(regionID, apiProductCode) - request.Version = "2014-05-26" + request.Version = "2016-04-28" request.ApiName = apiName request.QueryParams["RegionId"] = regionID @@ -1259,3 +1260,228 @@ func aliRemoveCsTag(csClient *cs.Client, regionInfo idrv.RegionInfo, resType irs return true, nil } + +// ALibaba tag 검색 for ecs +// myimage는 aliMyImageTag() 사용 +func aliEcsTagList(Client *ecs.Client, regionInfo idrv.RegionInfo, alibabaResourceType string, resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag()") + regionID := regionInfo.Region + var tagInfo []*irs.TagInfo + + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType //string(resType)//keypair + if keyword != "" { + queryParams["Tag.1.Key"] = keyword + cblogger.Info("쿼리", queryParams) + } + + start := call.Start() + response, err := CallEcsRequest(resType, Client, regionInfo, "DescribeResourceByTags", queryParams) + LoggingInfo(hiscallInfo, start) + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + resResources := AliTagResourcesResponse{} + + tagResponseStr := response.GetHttpContentString() + err = json.Unmarshal([]byte(tagResponseStr), &resResources) + + if err != nil { + cblogger.Error(err.Error()) + return tagInfo, nil + } + + for _, aliTagResource := range resResources.AliTagResources.Resources { + + cblogger.Debug("aliTagResource ", aliTagResource) + aTagInfo, err := ExtractTagResourceInfo(&aliTagResource) + if err != nil { + cblogger.Error(err.Error()) + continue + } + cblogger.Info("aTagInfoaTagInfoaTagInfo", aTagInfo) + // api + aTagInfo.ResType = resType + + queryParams := map[string]string{ + "ResourceId": aTagInfo.ResIId.SystemId, + } + response, err := CallEcsRequest(resType, Client, regionInfo, "DescribeTags", queryParams) + if err != nil { + cblogger.Error(err.Error()) + continue + } + + var tagsResponse DescribeTagsResponse + err = json.Unmarshal([]byte(response.GetHttpContentString()), &tagsResponse) + if err != nil { + cblogger.Error("Failed to unmarshal response: ", err) + continue + } + + aTagInfo.TagList = []irs.KeyValue{} + for _, tag := range tagsResponse.Tags.Tag { + aTagInfo.TagList = append(aTagInfo.TagList, irs.KeyValue{ + Key: tag.TagKey, + Value: tag.TagValue, + }) + } + + cblogger.Debug("Updated tagInfo ", aTagInfo) + tagInfo = append(tagInfo, &aTagInfo) + } + return tagInfo, nil +} + +func aliMyImageTagList(Client *ecs.Client, regionInfo idrv.RegionInfo, keyword string) ([]*irs.TagInfo, error) { + var tagInfo []*irs.TagInfo + // TODO: keyword tag 검색 기능 + res, err := DescribeImages(Client, regionInfo, nil, true) + + cblogger.Info("resresresresres", err) + cblogger.Info("resresresresres", res) + // spew.Dump(res) + + return tagInfo, nil +} + +func aliVpcTagList(VpcClient *vpc.Client, regionInfo idrv.RegionInfo, alibabaResourceType string, resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag()") + regionID := regionInfo.Region + var tagInfo []*irs.TagInfo + + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType //string(resType)//keypair + // queryParams["Tag.1.Key"] = keyword + // cblogger.Info("쿼리", queryParams) + + start := call.Start() + + response, err := CallVpcRequest(resType, VpcClient, regionInfo, "DescribeVpcs", queryParams) + LoggingInfo(hiscallInfo, start) + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + resResources := DescribeVpcsResponse{} + + tagResponseStr := response.GetHttpContentString() + err = json.Unmarshal([]byte(tagResponseStr), &resResources) + + if err != nil { + cblogger.Error(err.Error()) + return tagInfo, nil + } + + for _, vpc := range resResources.Vpcs.Vpc { + for _, tag := range vpc.Tags.Tag { + aliTagResource := AliTagResource{ + ResourceType: "VPC", + ResourceId: vpc.VpcId, + TagKey: tag.Key, + TagValue: tag.Value, + } + aTagInfo, err := ExtractTagResourceInfo(&aliTagResource) + if err != nil { + cblogger.Error(err.Error()) + continue + } + tagInfo = append(tagInfo, &aTagInfo) + } + } + return tagInfo, nil +} + +func aliSubnetTagList(VpcClient *vpc.Client, regionInfo idrv.RegionInfo, alibabaResourceType string, resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag()") + regionID := regionInfo.Region + var tagInfo []*irs.TagInfo + + queryParams := map[string]string{} + queryParams["RegionId"] = regionID + queryParams["ResourceType"] = alibabaResourceType //string(resType)//keypair + // queryParams["Tag.1.Key"] = keyword + // cblogger.Info("쿼리", queryParams) + + start := call.Start() + response, err := CallVpcRequest(resType, VpcClient, regionInfo, "DescribeVSwitches", queryParams) + LoggingInfo(hiscallInfo, start) + if err != nil { + cblogger.Error(err.Error()) + LoggingError(hiscallInfo, err) + } + cblogger.Debug(response.GetHttpContentString()) + resResources := DescribeVSwitchesResponse{} + + tagResponseStr := response.GetHttpContentString() + err = json.Unmarshal([]byte(tagResponseStr), &resResources) + + if err != nil { + cblogger.Error("Failed to unmarshal response: ", err) + } + + for _, vswitch := range resResources.VSwitches.VSwitch { + if len(vswitch.Tags.Tag) == 0 { + continue + } + for _, tag := range vswitch.Tags.Tag { + aliTagResource := AliTagResource{ + ResourceType: "VSwitch", + ResourceId: vswitch.VSwitchId, + TagKey: tag.Key, + TagValue: tag.Value, + } + cblogger.Debug("aliTagResourcealiTagResourcealiTagResourcealiTagResource", resType) + + vswitchTagInfo, err := ExtractTagResourceInfo(&aliTagResource) + cblogger.Debug("vswitchTagInfovswitchTagInfovswitchTagInfo", vswitchTagInfo) + if err != nil { + cblogger.Error(err.Error()) + continue + } + tagInfo = append(tagInfo, &vswitchTagInfo) + } + } + return tagInfo, nil +} + +func aliClusterTagList(CsClient *cs.Client, regionInfo idrv.RegionInfo, resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + hiscallInfo := GetCallLogScheme(regionInfo, call.TAG, keyword, "FindTag()") + var tagInfoList []*irs.TagInfo + + regionID := regionInfo.Region + clusters, err := aliDescribeClustersV1(CsClient, regionID) + if err != nil { + cblogger.Error(err) + LoggingError(hiscallInfo, err) + return nil, err + } + //cblogger.Debug("clusters ", clusters) + // 모든 cluster를 돌면서 Tag 찾기 + for _, cluster := range clusters { + cblogger.Debug("inCluster ") + for _, aliTag := range cluster.Tags { + //cblogger.Debug("aliTag ", aliTag) + //cblogger.Debug("keyword ", keyword) + //cblogger.Debug("aliTag.Key ", *(aliTag.Key)) + if *(aliTag.Key) == keyword { + var aTagInfo irs.TagInfo + aTagInfo.ResIId = irs.IID{SystemId: *cluster.ClusterId} + aTagInfo.ResType = resType + + tagList := []irs.KeyValue{} + tagList = append(tagList, irs.KeyValue{Key: "TagKey", Value: *aliTag.Key}) + tagList = append(tagList, irs.KeyValue{Key: "TagValue", Value: *aliTag.Value}) + aTagInfo.TagList = tagList + //cblogger.Debug("append Tag ", &tagInfo) + // tagInfo = &aTagInfo + } + } + } + return tagInfoList, err +} diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go index 82bdcce4c..eb85b4463 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go @@ -6,6 +6,7 @@ import ( cs "github.com/alibabacloud-go/cs-20151215/v4/client" // cs : container service "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" // ecs : elastic compute service + "github.com/aliyun/alibaba-cloud-sdk-go/services/vpc" call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" @@ -13,9 +14,10 @@ import ( ) type AlibabaTagHandler struct { - Region idrv.RegionInfo - Client *ecs.Client - CsClient *cs.Client + Region idrv.RegionInfo + Client *ecs.Client + CsClient *cs.Client + VpcClient *vpc.Client } type AliTagResponse struct { @@ -50,6 +52,8 @@ type AliTagResourcesResponse struct { PageNumber int `json:"PageNumber" xml:"PageNumber"` TotalCount int `json:"TotalCount" xml:"TotalCount"` AliTagResources AliTagResources `json:"Resources" xml:"Resources"` + + // AliTagResources AliTagResources `json:"Resources" xml:"Resources"` } type AliResourceTypeCount struct { @@ -73,6 +77,88 @@ type DescribeTagsResponse struct { PageNumber int `json:"PageNumber" xml:"PageNumber"` Tags ecs.TagsInDescribeTags `json:"Tags" xml:"Tags"` } +type DescribeVpcsResponse struct { + TotalCount int `json:"TotalCount"` + PageSize int `json:"PageSize"` + RequestId string `json:"RequestId"` + PageNumber int `json:"PageNumber"` + Vpcs struct { + Vpc []struct { + Status string `json:"Status"` + IsDefault bool `json:"IsDefault"` + CenStatus string `json:"CenStatus"` + Description string `json:"Description"` + ResourceGroupId string `json:"ResourceGroupId"` + VSwitchIds struct { + VSwitchId []string `json:"VSwitchId"` + } `json:"VSwitchIds"` + SecondaryCidrBlocks struct { + SecondaryCidrBlock []string `json:"SecondaryCidrBlock"` + } `json:"SecondaryCidrBlocks"` + CidrBlock string `json:"CidrBlock"` + RouterTableIds struct { + RouterTableIds []string `json:"RouterTableIds"` + } `json:"RouterTableIds"` + UserCidrs struct { + UserCidr []string `json:"UserCidr"` + } `json:"UserCidrs"` + NetworkAclNum int `json:"NetworkAclNum"` + AdvancedResource bool `json:"AdvancedResource"` + VRouterId string `json:"VRouterId"` + NatGatewayIds struct { + NatGatewayIds []string `json:"NatGatewayIds"` + } `json:"NatGatewayIds"` + VpcId string `json:"VpcId"` + OwnerId int64 `json:"OwnerId"` + CreationTime string `json:"CreationTime"` + VpcName string `json:"VpcName"` + EnabledIpv6 bool `json:"EnabledIpv6"` + RegionId string `json:"RegionId"` + Ipv6CidrBlock string `json:"Ipv6CidrBlock"` + Tags struct { + Tag []struct { + Value string `json:"Value"` + Key string `json:"Key"` + } `json:"Tag"` + } `json:"Tags"` + } `json:"Vpc"` + } `json:"Vpcs"` +} +type DescribeVSwitchesResponse struct { + TotalCount int `json:"TotalCount"` + PageSize int `json:"PageSize"` + RequestId string `json:"RequestId"` + PageNumber int `json:"PageNumber"` + VSwitches struct { + VSwitch []struct { + Status string `json:"Status"` + IsDefault bool `json:"IsDefault"` + Description string `json:"Description"` + ResourceGroupId string `json:"ResourceGroupId"` + ZoneId string `json:"ZoneId"` + NetworkAclId string `json:"NetworkAclId"` + AvailableIpAddressCount int `json:"AvailableIpAddressCount"` + VSwitchId string `json:"VSwitchId"` + CidrBlock string `json:"CidrBlock"` + RouteTable struct { + RouteTableId string `json:"RouteTableId"` + RouteTableType string `json:"RouteTableType"` + } `json:"RouteTable"` + VpcId string `json:"VpcId"` + OwnerId int64 `json:"OwnerId"` + CreationTime string `json:"CreationTime"` + VSwitchName string `json:"VSwitchName"` + Ipv6CidrBlock string `json:"Ipv6CidrBlock"` + Tags struct { + Tag []struct { + Value string `json:"Value"` + Key string `json:"Key"` + } `json:"Tag"` + } `json:"Tags"` + ShareType string `json:"ShareType"` + } `json:"VSwitch"` + } `json:"VSwitches"` +} /* * ECS Instances 아래에 Tags and ResourceGroup이 있음. @@ -243,8 +329,8 @@ func (tagHandler *AlibabaTagHandler) ListTag(resType irs.RSType, resIID irs.IID) resTagResources := AliTagResourcesResponse{} cblogger.Debug("resTagResources ", resTagResources) - cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) - cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) + // cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) + // cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) for _, aliTagResource := range response.TagResource { cblogger.Debug("aliTagResource ", aliTagResource) @@ -335,8 +421,8 @@ func (tagHandler *AlibabaTagHandler) GetTag(resType irs.RSType, resIID irs.IID, resTagResources := AliTagResourcesResponse{} cblogger.Debug("resTagResources ", resTagResources) - cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) - cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) + // cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) + // cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) for _, aliTagResource := range response.TagResource { cblogger.Debug("aliTagResource ", aliTagResource) @@ -428,205 +514,158 @@ func (tagHandler *AlibabaTagHandler) RemoveTag(resType irs.RSType, resIID irs.II // if you want to find all tags, set keyword to "" or "*". // 해당 Resource Type에 tag가 있는 것들. ListTag는 resourceId가 있으나 당 function은 더 넒음 func (tagHandler *AlibabaTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { - var tagInfoList []*irs.TagInfo - //start := call.Start() - //regionID := tagHandler.Region.Region - //regionID = "ap-northeast-1" // for the test - - // alibabaResourceType, err := GetAlibabaResourceType(resType) - // if err != nil { - // return tagInfoList, err - // } - - // alibabaApiType, err := GetAliTargetApi(resType) - // if err != nil { - // return tagInfoList, err - // } - - // switch alibabaApiType { - // case "ecs": - - // queryParams := map[string]string{} - // queryParams["RegionId"] = regionID - // queryParams["ResourceType"] = alibabaResourceType //string(resType) - // queryParams["Tag.1.Key"] = keyword + var tagInfoList []*irs.TagInfo - // start := call.Start() - // response, err := CallEcsRequest(resType, tagHandler.Client, tagHandler.Region, "DescribeResourceByTags", queryParams) - // LoggingInfo(hiscallInfo, start) + regionInfo := tagHandler.Region + regionID := tagHandler.Region.Region - // if err != nil { - // cblogger.Error(err.Error()) - // LoggingError(hiscallInfo, err) - // } - // cblogger.Debug(response.GetHttpContentString()) + alibabaResourceType, err := GetAlibabaResourceType(resType) - // resTagResources := AliTagResourcesResponse{} + if err != nil { + return tagInfoList, err + } - // tagResponseStr := response.GetHttpContentString() - // err = json.Unmarshal([]byte(tagResponseStr), &resTagResources) - // if err != nil { - // cblogger.Error(err.Error()) - // return tagInfoList, nil - // } - // cblogger.Debug("resTagResources ", resTagResources) - // cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) - // cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) - - // for _, aliTagResource := range resTagResources.AliTagResources.Resources { - // cblogger.Debug("aliTagResource ", aliTagResource) - // aTagInfo, err := ExtractTagResourceInfo(&aliTagResource) - // if err != nil { - // cblogger.Error(err.Error()) - // continue - // } - - // aTagInfo.ResType = resType - - // cblogger.Debug("tagInfo ", aTagInfo) - // tagInfoList = append(tagInfoList, &aTagInfo) - // } - // case "cs": // cs : container service - // clusters, err := aliDescribeClustersV1(tagHandler.CsClient, regionID) - // if err != nil { - // cblogger.Error(err) - // LoggingError(hiscallInfo, err) - // return nil, err - // } + cblogger.Debug("resType : ", resType) + switch resType { - // //cblogger.Debug("clusters ", clusters) - // // 모든 cluster를 돌면서 Tag 찾기 - // for _, cluster := range clusters { - // cblogger.Debug("inCluster ") - // for _, aliTag := range cluster.Tags { - // //cblogger.Debug("aliTag ", aliTag) - // //cblogger.Debug("keyword ", keyword) - // //cblogger.Debug("aliTag.Key ", *(aliTag.Key)) - // if *(aliTag.Key) == keyword { - // var tagInfo irs.TagInfo - // tagInfo.ResIId = irs.IID{SystemId: *cluster.ClusterId} - // tagInfo.ResType = resType - - // tagList := []irs.KeyValue{} - // tagList = append(tagList, irs.KeyValue{Key: "TagKey", Value: *aliTag.Key}) - // tagList = append(tagList, irs.KeyValue{Key: "TagValue", Value: *aliTag.Value}) - // tagInfo.TagList = tagList - // //cblogger.Debug("append Tag ", &tagInfo) - // tagInfoList = append(tagInfoList, &tagInfo) - // } - // } - // } - // } + case "VM", irs.VM: + responseTagList, err := aliEcsTagList(tagHandler.Client, regionInfo, alibabaResourceType, resType, keyword) + if err != nil { + cblogger.Error(err) + } + cblogger.Debug("aliEcsTag response : ", responseTagList) - switch string(resType) { - case "ALL": - // for 모든 resource - default: + tagInfoList = append(tagInfoList, responseTagList...) - tagInfo, err := FindTag(tagHandler, resType, keyword) + case "KEY", irs.KEY: + responseTagList, err := aliEcsTagList(tagHandler.Client, regionInfo, alibabaResourceType, resType, keyword) if err != nil { - cblogger.Error(err.Error()) - return tagInfoList, err + cblogger.Error(err) } - tagInfoList = append(tagInfoList, tagInfo) - } - - return tagInfoList, nil -} + cblogger.Debug("aliEcsTag response : ", responseTagList) -// 1개의 resource Type에 대한 Tag 정보 -func FindTag(tagHandler *AlibabaTagHandler, resType irs.RSType, keyword string) (*irs.TagInfo, error) { - hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, keyword, "FindTag()") - var tagInfo *irs.TagInfo + tagInfoList = append(tagInfoList, responseTagList...) - regionID := tagHandler.Region.Region + case "SG", irs.SG: + responseTagList, err := aliEcsTagList(tagHandler.Client, regionInfo, alibabaResourceType, resType, keyword) + if err != nil { + cblogger.Error(err) + } + cblogger.Debug("aliEcsTag response : ", responseTagList) - alibabaResourceType, err := GetAlibabaResourceType(resType) - if err != nil { - return tagInfo, err - } + tagInfoList = append(tagInfoList, responseTagList...) - alibabaApiType, err := GetAliTargetApi(resType) - if err != nil { - return tagInfo, err - } + case "DISK", irs.DISK: + responseTagList, err := aliEcsTagList(tagHandler.Client, regionInfo, alibabaResourceType, resType, keyword) + if err != nil { + cblogger.Error(err) + } + cblogger.Debug("aliEcsTag response : ", responseTagList) - switch alibabaApiType { - case "ecs": + tagInfoList = append(tagInfoList, responseTagList...) - queryParams := map[string]string{} - queryParams["RegionId"] = regionID - queryParams["ResourceType"] = alibabaResourceType //string(resType) - queryParams["Tag.1.Key"] = keyword + case "MYIMAGE", irs.MYIMAGE: + responseTagList, err := aliMyImageTagList(tagHandler.Client, regionInfo, keyword) + if err != nil { + cblogger.Error(err) + } + cblogger.Debug("aliEcsTag response : ", responseTagList) - start := call.Start() - response, err := CallEcsRequest(resType, tagHandler.Client, tagHandler.Region, "DescribeResourceByTags", queryParams) - LoggingInfo(hiscallInfo, start) + tagInfoList = append(tagInfoList, responseTagList...) + case "VPC", irs.VPC: + responseTagList, err := aliVpcTagList(tagHandler.VpcClient, regionInfo, alibabaResourceType, resType, keyword) if err != nil { - cblogger.Error(err.Error()) - LoggingError(hiscallInfo, err) + cblogger.Error(err) } - cblogger.Debug(response.GetHttpContentString()) + cblogger.Debug("aliEcsTag response : ", responseTagList) - resTagResources := AliTagResourcesResponse{} + tagInfoList = append(tagInfoList, responseTagList...) - tagResponseStr := response.GetHttpContentString() - err = json.Unmarshal([]byte(tagResponseStr), &resTagResources) + case "SUBNET", irs.SUBNET: + responseTagList, err := aliSubnetTagList(tagHandler.VpcClient, regionInfo, alibabaResourceType, resType, keyword) if err != nil { - cblogger.Error(err.Error()) - return tagInfo, nil + cblogger.Error(err) } - cblogger.Debug("resTagResources ", resTagResources) - cblogger.Debug("resTagResources.AliTagResources ", resTagResources.AliTagResources) - cblogger.Debug("resTagResources.AliTagResources.Resources ", resTagResources.AliTagResources.Resources) - - for _, aliTagResource := range resTagResources.AliTagResources.Resources { - cblogger.Debug("aliTagResource ", aliTagResource) - aTagInfo, err := ExtractTagResourceInfo(&aliTagResource) - if err != nil { - cblogger.Error(err.Error()) - continue - } + cblogger.Debug("aliEcsTag response : ", responseTagList) - aTagInfo.ResType = resType + tagInfoList = append(tagInfoList, responseTagList...) - cblogger.Debug("tagInfo ", aTagInfo) - tagInfo = &aTagInfo - } - case "cs": // cs : container service - clusters, err := aliDescribeClustersV1(tagHandler.CsClient, regionID) + case "CLUSTER", irs.CLUSTER: // cs : container service + responseTagList, err := aliClusterTagList(tagHandler.CsClient, regionInfo, resType, keyword) if err != nil { cblogger.Error(err) - LoggingError(hiscallInfo, err) - return nil, err } + cblogger.Debug("aliEcsTag response : ", responseTagList) + + tagInfoList = append(tagInfoList, responseTagList...) + + case "ALL", irs.ALL: + // 모든 자원 유형을 포함하는 슬라이스를 선언 + allResourceTypes := []irs.RSType{irs.VM, irs.KEY, irs.SG, irs.DISK, irs.MYIMAGE, irs.VPC, irs.SUBNET, irs.CLUSTER} + + // 각 자원 유형별로 태그 정보 조회 + for _, resourceType := range allResourceTypes { + switch resourceType { + case irs.VM, irs.KEY, irs.SG, irs.DISK: + responseTagList, err := aliEcsTagList(tagHandler.Client, regionInfo, alibabaResourceType, resourceType, keyword) + if err != nil { + cblogger.Errorf("Error retrieving tags for %s: %v", resourceType, err) + } else { + tagInfoList = append(tagInfoList, responseTagList...) + } + + case irs.MYIMAGE: + responseTagList, err := aliMyImageTagList(tagHandler.Client, regionInfo, keyword) + if err != nil { + cblogger.Errorf("Error retrieving tags for MYIMAGE: %v", err) + } else { + tagInfoList = append(tagInfoList, responseTagList...) + } + + case irs.VPC: + responseTagList, err := aliVpcTagList(tagHandler.VpcClient, regionInfo, alibabaResourceType, resourceType, keyword) + if err != nil { + cblogger.Errorf("Error retrieving tags for VPC: %v", err) + } else { + tagInfoList = append(tagInfoList, responseTagList...) + } + + case irs.SUBNET: + responseTagList, err := aliSubnetTagList(tagHandler.VpcClient, regionInfo, alibabaResourceType, resourceType, keyword) + if err != nil { + cblogger.Errorf("Error retrieving tags for SUBNET: %v", err) + } else { + tagInfoList = append(tagInfoList, responseTagList...) + } - //cblogger.Debug("clusters ", clusters) - // 모든 cluster를 돌면서 Tag 찾기 - for _, cluster := range clusters { - cblogger.Debug("inCluster ") - for _, aliTag := range cluster.Tags { - //cblogger.Debug("aliTag ", aliTag) - //cblogger.Debug("keyword ", keyword) - //cblogger.Debug("aliTag.Key ", *(aliTag.Key)) - if *(aliTag.Key) == keyword { - var aTagInfo irs.TagInfo - aTagInfo.ResIId = irs.IID{SystemId: *cluster.ClusterId} - aTagInfo.ResType = resType - - tagList := []irs.KeyValue{} - tagList = append(tagList, irs.KeyValue{Key: "TagKey", Value: *aliTag.Key}) - tagList = append(tagList, irs.KeyValue{Key: "TagValue", Value: *aliTag.Value}) - aTagInfo.TagList = tagList - //cblogger.Debug("append Tag ", &tagInfo) - tagInfo = &aTagInfo + case irs.CLUSTER: + // CLUSTER 태그 정보 조회 로직 + clusters, err := aliDescribeClustersV1(tagHandler.CsClient, regionID) + if err != nil { + cblogger.Errorf("Error retrieving clusters: %v", err) + } else { + for _, cluster := range clusters { + for _, aliTag := range cluster.Tags { + if *(aliTag.Key) == keyword { + aTagInfo := irs.TagInfo{ + ResIId: irs.IID{SystemId: *cluster.ClusterId}, + ResType: resourceType, + TagList: []irs.KeyValue{ + {Key: "TagKey", Value: *aliTag.Key}, + {Key: "TagValue", Value: *aliTag.Value}, + }, + } + tagInfoList = append(tagInfoList, &aTagInfo) + } + } + } } } } } - return tagInfo, nil + return tagInfoList, nil } /* @@ -690,7 +729,6 @@ func ExtractTagResourceInfo(tagResource *AliTagResource) (irs.TagInfo, error) { //cblogger.Debug("tag ", aliTag) //cblogger.Debug("TagKey ", aliTag.TagKey) //cblogger.Debug("TagValue ", aliTag.TagValue) - tagInfo.ResType = irs.RSType(tagResource.ResourceType) tagInfo.ResIId = irs.IID{SystemId: tagResource.ResourceId} From 0eb4771fe6dcfb1d8d950983b0dc920050d4d4d1 Mon Sep 17 00:00:00 2001 From: SungWoongz Date: Wed, 24 Jul 2024 15:16:14 +0900 Subject: [PATCH 40/56] language check --- .../cloud-driver/drivers/alibaba/resources/CommonHandler.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go index 2eeb8d2a4..9a13e89f9 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonHandler.go @@ -1273,7 +1273,6 @@ func aliEcsTagList(Client *ecs.Client, regionInfo idrv.RegionInfo, alibabaResour queryParams["ResourceType"] = alibabaResourceType //string(resType)//keypair if keyword != "" { queryParams["Tag.1.Key"] = keyword - cblogger.Info("쿼리", queryParams) } start := call.Start() @@ -1356,8 +1355,6 @@ func aliVpcTagList(VpcClient *vpc.Client, regionInfo idrv.RegionInfo, alibabaRes queryParams := map[string]string{} queryParams["RegionId"] = regionID queryParams["ResourceType"] = alibabaResourceType //string(resType)//keypair - // queryParams["Tag.1.Key"] = keyword - // cblogger.Info("쿼리", queryParams) start := call.Start() @@ -1405,8 +1402,6 @@ func aliSubnetTagList(VpcClient *vpc.Client, regionInfo idrv.RegionInfo, alibaba queryParams := map[string]string{} queryParams["RegionId"] = regionID queryParams["ResourceType"] = alibabaResourceType //string(resType)//keypair - // queryParams["Tag.1.Key"] = keyword - // cblogger.Info("쿼리", queryParams) start := call.Start() response, err := CallVpcRequest(resType, VpcClient, regionInfo, "DescribeVSwitches", queryParams) From 26cf6c4148f8eed341936fa91a78a33c7858d21d Mon Sep 17 00:00:00 2001 From: innodreamer Date: Wed, 24 Jul 2024 15:41:03 +0900 Subject: [PATCH 41/56] Unify NCP VPC VM Creating Status (Booting / Setting up -> Creating) --- .../drivers/ncp/resources/VMHandler.go | 16 +---- .../drivers/ncpvpc/resources/VMHandler.go | 71 ++++--------------- 2 files changed, 15 insertions(+), 72 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go index 24d716035..6fe71dade 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncp/resources/VMHandler.go @@ -62,7 +62,6 @@ func init() { func (vmHandler *NcpVMHandler) StartVM(vmReqInfo irs.VMReqInfo) (irs.VMInfo, error) { cblogger.Info("NCP Classic Cloud driver: called StartVM()!!") - InitLog() callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Zone, call.VM, vmReqInfo.IId.NameId, "StartVM()") @@ -481,7 +480,6 @@ func (vmHandler *NcpVMHandler) MappingServerInfo(NcpInstance *server.ServerInsta } else { // To get the VPC info. vpcHandler := NcpVPCHandler { - CredentialInfo: vmHandler.CredentialInfo, RegionInfo: vmHandler.RegionInfo, VMClient: vmHandler.VMClient, } @@ -502,7 +500,7 @@ func (vmHandler *NcpVMHandler) MappingServerInfo(NcpInstance *server.ServerInsta } } } - cblogger.Infof("NCP Instance Uptime : [%s]", *NcpInstance.Uptime) + // cblogger.Infof("NCP Instance Uptime : [%s]", *NcpInstance.Uptime) // Note : NCP VPC PlatformType : LNX32, LNX64, WND32, WND64, UBD64, UBS64 if strings.Contains(*NcpInstance.PlatformType.Code, "LNX") || strings.Contains(*NcpInstance.PlatformType.Code, "UB") { @@ -543,8 +541,6 @@ func (vmHandler *NcpVMHandler) GetVM(vmIID irs.IID) (irs.VMInfo, error) { func (vmHandler *NcpVMHandler) SuspendVM(vmIID irs.IID) (irs.VMStatus, error) { cblogger.Info("NCP Classic Cloud driver: called SuspendVM()!!") - cblogger.Infof("vmID : [%s]", vmIID.SystemId) - InitLog() callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Zone, call.VM, vmIID.NameId, "SuspendVM()") @@ -605,8 +601,6 @@ func (vmHandler *NcpVMHandler) SuspendVM(vmIID irs.IID) (irs.VMStatus, error) { func (vmHandler *NcpVMHandler) ResumeVM(vmIID irs.IID) (irs.VMStatus, error) { cblogger.Info("NCP Classic Cloud driver: called ResumeVM()!") - cblogger.Infof("vmID : " + vmIID.SystemId) - InitLog() callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Zone, call.VM, vmIID.NameId, "ResumeVM()") @@ -673,8 +667,6 @@ func (vmHandler *NcpVMHandler) ResumeVM(vmIID irs.IID) (irs.VMStatus, error) { func (vmHandler *NcpVMHandler) RebootVM(vmIID irs.IID) (irs.VMStatus, error) { cblogger.Info("NCP Classic Cloud driver: called RebootVM()!") - cblogger.Infof("vmID : [%s]", vmIID.SystemId) - InitLog() callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Zone, call.VM, vmIID.NameId, "RebootVM()") @@ -742,8 +734,6 @@ func (vmHandler *NcpVMHandler) RebootVM(vmIID irs.IID) (irs.VMStatus, error) { func (vmHandler *NcpVMHandler) TerminateVM(vmIID irs.IID) (irs.VMStatus, error) { cblogger.Info("NCP Classic Cloud driver: called TerminateVM()!") - cblogger.Infof("vmID : [%s]", vmIID.SystemId) - InitLog() callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Zone, call.VM, vmIID.NameId, "TerminateVM()") @@ -961,7 +951,6 @@ func ConvertVMStatusString(vmStatus string) (irs.VMStatus, error) { func (vmHandler *NcpVMHandler) GetVMStatus(vmIID irs.IID) (irs.VMStatus, error) { cblogger.Info("NCP Classic Cloud driver: called GetVMStatus()!") - InitLog() callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Zone, call.VM, vmIID.NameId, "GetVMStatus()") @@ -1000,13 +989,12 @@ func (vmHandler *NcpVMHandler) GetVMStatus(vmIID irs.IID) (irs.VMStatus, error) // cblogger.Info("Succeeded in Getting ServerInstanceList!!") vmStatus, errStatus := ConvertVMStatusString(*result.ServerInstanceList[0].ServerInstanceStatusName) - cblogger.Info("# Converted VM Status : " + vmStatus) + // cblogger.Info("# Converted VM Status : " + vmStatus) return vmStatus, errStatus } func (vmHandler *NcpVMHandler) ListVMStatus() ([]*irs.VMStatusInfo, error) { cblogger.Info("NCP Classic Cloud driver: called ListVMStatus()!") - InitLog() callLogInfo := GetCallLogScheme(vmHandler.RegionInfo.Zone, call.VM, "ListVMStatus()", "ListVMStatus()") diff --git a/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VMHandler.go index e5d0d6e8c..20c64a9ba 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncpvpc/resources/VMHandler.go @@ -323,63 +323,19 @@ func (vmHandler *NcpVpcVMHandler) GetVM(vmIID irs.IID) (irs.VMInfo, error) { return irs.VMInfo{}, newErr } - curStatus, statusErr := vmHandler.GetVMStatus(vmIID) - if statusErr != nil { - newErr := fmt.Errorf("Failed to Get the VM Status with the VM ID : [%s], [%v]", vmIID.SystemId, statusErr) - cblogger.Error(newErr.Error()) - LoggingError(callLogInfo, newErr) - return irs.VMInfo{}, newErr - } - cblogger.Infof("===> VM Status : [%s]", curStatus) - - // Since it's impossible to get VM info. during Creation, ... - switch string(curStatus) { - case "Creating": - cblogger.Infof("The VM status is '%s', so wait for the VM creation before inquiring the info.", string(curStatus)) - return irs.VMInfo{}, errors.New("The VM status is 'Creating', so wait for the VM creation before inquiring the info. : " + vmIID.SystemId) - default: - cblogger.Infof("===> The VM status is not 'Creating', you can get the VM info.") - } - - /* - newVMIID := irs.IID{SystemId: systemId} - - curStatus, statusErr := vmHandler.WaitToGetInfo(newVMIID) - if statusErr != nil { - cblogger.Error(statusErr.Error()) - return irs.VMInfo{}, nil - } - */ - instanceNumList := []*string{ncloud.String(vmIID.SystemId),} - instanceReq := vserver.GetServerInstanceListRequest{ - RegionCode: ncloud.String(vmHandler.RegionInfo.Region), // $$$ Caution!! - ServerInstanceNoList: instanceNumList, - } - start := call.Start() - result, err := vmHandler.VMClient.V2Api.GetServerInstanceList(&instanceReq) + ncpVMInfo, err := vmHandler.GetNcpVMInfo(vmIID.SystemId) if err != nil { - newErr := fmt.Errorf("Failed to Find VM Instance List from NCP VPC!! : [%v]", err) + newErr := fmt.Errorf("Failed to Get the VM Info : [%v]", err) cblogger.Error(newErr.Error()) - LoggingError(callLogInfo, newErr) return irs.VMInfo{}, newErr } - LoggingInfo(callLogInfo, start) - if len(result.ServerInstanceList) < 1 { - newErr := fmt.Errorf("Failed to Find the VM Info. The VM does Not Exist!!") - cblogger.Error(newErr.Error()) - LoggingError(callLogInfo, newErr) - return irs.VMInfo{}, newErr - } - - vmInfo, err := vmHandler.MappingServerInfo(result.ServerInstanceList[0]) + vmInfo, err := vmHandler.MappingServerInfo(ncpVMInfo) if err != nil { - newErr := fmt.Errorf("Failed to Map the VM Info!! : [%v]", err) - cblogger.Error(newErr.Error()) - LoggingError(callLogInfo, newErr) - return irs.VMInfo{}, newErr + LoggingError(callLogInfo, err) + return irs.VMInfo{}, err } - return vmInfo, nil + return vmInfo, nil } func (vmHandler *NcpVpcVMHandler) SuspendVM(vmIID irs.IID) (irs.VMStatus, error) { @@ -774,9 +730,9 @@ func ConvertVMStatusString(vmStatus string) (irs.VMStatus, error) { resultStatus = "Creating" } else if strings.EqualFold(vmStatus, "booting") { //Caution!! - resultStatus = "Booting" + resultStatus = "Creating" } else if strings.EqualFold(vmStatus, "setting up") { - resultStatus = "Setting_up" + resultStatus = "Creating" } else if strings.EqualFold(vmStatus, "running") { resultStatus = "Running" } else if strings.EqualFold(vmStatus, "shutting down") { @@ -838,7 +794,7 @@ func (vmHandler *NcpVpcVMHandler) GetVMStatus(vmIID irs.IID) (irs.VMStatus, erro } vmStatus, statusErr := ConvertVMStatusString(*result.ServerInstanceList[0].ServerInstanceStatusName) - cblogger.Infof("VM Status of [%s] : [%s]", vmIID.SystemId, vmStatus) + // cblogger.Infof("VM Status of [%s] : [%s]", vmIID.SystemId, vmStatus) return vmStatus, statusErr } @@ -1338,14 +1294,13 @@ func (vmHandler *NcpVpcVMHandler) WaitToGetInfo(vmIID irs.IID) (irs.VMStatus, er cblogger.Errorf("Failed to Get the VM Status of [%s]", vmIID.SystemId) cblogger.Error(statusErr.Error()) } else { - cblogger.Infof("Succeeded in Getting the Status of VM [%s] : [%s]", vmIID.SystemId, curStatus) + cblogger.Infof("===> VM Status : [%s]", curStatus) } - cblogger.Infof("===> VM Status : [%s]", curStatus) switch string(curStatus) { - case "Creating", "Booting", "Setting_up": + case "Creating": curRetryCnt++ - cblogger.Infof("The VM is 'Creating' and 'Booting', so wait for a second more before inquiring the VM info.") + cblogger.Infof("The VM is 'Creating', so wait for a second more before inquiring the VM info.") time.Sleep(time.Second * 5) if curRetryCnt > maxRetryCnt { cblogger.Errorf("Despite waiting for a long time(%d sec), the VM status is '%s', so it is forcibly finishied.", maxRetryCnt, curStatus) @@ -1375,7 +1330,7 @@ func (vmHandler *NcpVpcVMHandler) WaitToDelPublicIp(vmIID irs.IID) (irs.VMStatus } else { cblogger.Infof("Succeeded in Getting the VM Status of [%s]", vmIID.SystemId) } - cblogger.Infof("===> VM Status [%s] : ", curStatus) + cblogger.Infof("===> VM Status : [%s]", curStatus) switch string(curStatus) { case "Suspended", "Terminating": From bce9706ec654eacf8c6b5dd17883c8b406282142 Mon Sep 17 00:00:00 2001 From: SungWoongz Date: Thu, 25 Jul 2024 13:54:28 +0900 Subject: [PATCH 42/56] Remove Korean text from test resource file --- .../drivers/alibaba/main/Test_Resources.go | 705 ++++++------------ 1 file changed, 227 insertions(+), 478 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go index 322639701..956d7145d 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/main/Test_Resources.go @@ -31,116 +31,6 @@ func init() { cblog.SetLevel("debug") } -/* -// Test PublicIp -func handlePublicIP() { - cblogger.Debug("Start Publicip Resource Test") - - ResourceHandler, err := testconf.GetResourceHandler("Publicip") - if err != nil { - panic(err) - } - - handler := ResourceHandler.(irs.PublicIPHandler) - - config := testconf.ReadConfigFile() - //reqGetPublicIP := "13.124.140.207" - reqPublicIP := config.Ali.PublicIP - //reqPublicIP = "eipalloc-0231a3e16ec42e869" - cblogger.Info("reqPublicIP : ", reqPublicIP) - //handler.CreatePublicIP(publicIPReqInfo) - //handler.ListPublicIP() - //handler.GetPublicIP("13.124.140.207") - - for { - fmt.Println("") - fmt.Println("Publicip Resource Test") - fmt.Println("1. ListPublicIP()") - fmt.Println("2. GetPublicIP()") - fmt.Println("3. CreatePublicIP()") - fmt.Println("4. DeletePublicIP()") - fmt.Println("5. Exit") - - var commandNum int - var reqDelIP string - - inputCnt, err := fmt.Scan(&commandNum) - if err != nil { - panic(err) - } - - if inputCnt == 1 { - switch commandNum { - case 1: - fmt.Println("Start ListPublicIP() ...") - result, err := handler.ListPublicIP() - if err != nil { - cblogger.Error("PublicIP 목록 조회 실패 : ", err) - } else { - cblogger.Info("PublicIP 목록 조회 결과") - spew.Dump(result) - } - - fmt.Println("Finish ListPublicIP()") - - case 2: - fmt.Println("Start GetPublicIP() ...") - result, err := handler.GetPublicIP(reqPublicIP) - if err != nil { - cblogger.Error(reqPublicIP, " PublicIP 정보 조회 실패 : ", err) - } else { - cblogger.Infof("PublicIP[%s] 정보 조회 결과", reqPublicIP) - spew.Dump(result) - } - fmt.Println("Finish GetPublicIP()") - - case 3: - fmt.Println("Start CreatePublicIP() ...") - reqInfo := irs.PublicIPReqInfo{Name: "mcloud-barista-eip-test"} - result, err := handler.CreatePublicIP(reqInfo) - if err != nil { - cblogger.Error("PublicIP 생성 실패 : ", err) - } else { - cblogger.Info("PublicIP 생성 성공 ", result) - spew.Dump(result) - } - fmt.Println("Finish CreatePublicIP()") - - case 4: - fmt.Println("Start DeletePublicIP() ...") - fmt.Print("삭제할 PublicIP를 입력하세요 : ") - inputCnt, err := fmt.Scan(&reqDelIP) - if err != nil { - panic(err) - } - - if inputCnt == 1 { - cblogger.Info("삭제할 PublicIP : ", reqDelIP) - } else { - fmt.Println("삭제할 Public IP만 입력하세요.") - } - - result, err := handler.DeletePublicIP(reqDelIP) - if err != nil { - cblogger.Error(reqDelIP, " PublicIP 삭제 실패 : ", err) - } else { - if result { - cblogger.Infof("PublicIP[%s] 삭제 완료", reqDelIP) - } else { - cblogger.Errorf("PublicIP[%s] 삭제 실패", reqDelIP) - } - } - fmt.Println("Finish DeletePublicIP()") - - case 5: - fmt.Println("Exit") - return - } - } - } -} -*/ - // Test VMSpec func handleVMSpec() { cblogger.Debug("Start VMSpec Resource Test") @@ -155,7 +45,8 @@ func handleVMSpec() { //config := testconf.ReadConfigFile() //reqVMSpec := config.Ali.VMSpec //reqVMSpec := "ecs.g6.large" // GPU가 없음 - reqVMSpec := "ecs.vgn5i-m8.4xlarge" // GPU 1개 + // reqVMSpec := "ecs.vgn5i-m8.4xlarge" // GPU 1개 + reqVMSpec := "" // GPU 1개 //reqVMSpec := "ecs.gn6i-c24g1.24xlarge" // GPU 4개 //reqRegion := config.Ali.Region @@ -185,9 +76,9 @@ func handleVMSpec() { fmt.Println("Start ListVMSpec() ...") result, err := handler.ListVMSpec() if err != nil { - cblogger.Error("VMSpec 목록 조회 실패 : ", err) + cblogger.Error("Failed to retrieve VMSpec list : ", err) } else { - cblogger.Info("VMSpec 목록 조회 결과") + cblogger.Info("Result of VMSpec list retrieval") spew.Dump(result) } @@ -197,9 +88,9 @@ func handleVMSpec() { fmt.Println("Start GetVMSpec() ...") result, err := handler.GetVMSpec(reqVMSpec) if err != nil { - cblogger.Error(reqVMSpec, " VMSpec 정보 조회 실패 : ", err) + cblogger.Error(reqVMSpec, "Failed to retrieve VMSpec list : ", err) } else { - cblogger.Infof("VMSpec[%s] 정보 조회 결과", reqVMSpec) + cblogger.Infof("Result of VMSpec list retrieval", reqVMSpec) spew.Dump(result) } fmt.Println("Finish GetVMSpec()") @@ -208,9 +99,9 @@ func handleVMSpec() { fmt.Println("Start ListOrgVMSpec() ...") result, err := handler.ListOrgVMSpec() if err != nil { - cblogger.Error("VMSpec 목록 조회 실패 : ", err) + cblogger.Error("Failed to retrieve VMSpec list : ", err) } else { - cblogger.Info("VMSpec 목록 조회 결과") + cblogger.Info("Result of VMSpec list retrieval") cblogger.Info(result) //spew.Dump(result) } @@ -221,9 +112,9 @@ func handleVMSpec() { fmt.Println("Start GetOrgVMSpec() ...") result, err := handler.GetOrgVMSpec(reqVMSpec) if err != nil { - cblogger.Error(reqVMSpec, " VMSpec 정보 조회 실패 : ", err) + cblogger.Error(reqVMSpec, "Failed to retrieve VMSpec list : ", err) } else { - cblogger.Infof("VMSpec[%s] 정보 조회 결과", reqVMSpec) + cblogger.Infof("Result of VMSpec list retrieval", reqVMSpec) cblogger.Info(result) //spew.Dump(result) } @@ -237,96 +128,6 @@ func handleVMSpec() { } } -/* -// Test AMI -func handleImage() { - cblogger.Debug("Start ImageHandler Resource Test") - - ResourceHandler, err := testconf.GetResourceHandler("Image") - if err != nil { - panic(err) - } - //handler := ResourceHandler.(irs2.ImageHandler) - handler := ResourceHandler.(irs.ImageHandler) - - //imageReqInfo := irs2.ImageReqInfo{ - imageReqInfo := irs.ImageReqInfo{ - Id: "ami-047f7b46bd6dd5d84", - Name: "Test OS Image", - } - - for { - fmt.Println("ImageHandler Management") - fmt.Println("0. Quit") - fmt.Println("1. Image List") - fmt.Println("2. Image Create") - fmt.Println("3. Image Get") - fmt.Println("4. Image Delete") - - var commandNum int - inputCnt, err := fmt.Scan(&commandNum) - if err != nil { - panic(err) - } - - if inputCnt == 1 { - switch commandNum { - case 0: - return - - case 1: - result, err := handler.ListImage() - if err != nil { - cblogger.Infof(" Image 목록 조회 실패 : ", err) - } else { - cblogger.Info("Image 목록 조회 결과") - cblogger.Info(result) - //spew.Dump(result) - cblogger.Info("출력 결과 수 : ", len(result)) - - //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. - if result != nil { - imageReqInfo.Id = result[0].Id // 조회 및 삭제를 위해 생성된 ID로 변경 - } - } - - case 2: - cblogger.Infof("[%s] Image 생성 테스트", imageReqInfo.Name) - //vNetworkReqInfo := irs.VNetworkReqInfo{} - result, err := handler.CreateImage(imageReqInfo) - if err != nil { - cblogger.Infof(imageReqInfo.Id, " Image 생성 실패 : ", err) - } else { - cblogger.Infof("Image 생성 결과 : ", result) - imageReqInfo.Id = result.Id // 조회 및 삭제를 위해 생성된 ID로 변경 - spew.Dump(result) - } - - case 3: - cblogger.Infof("[%s] Image 조회 테스트", imageReqInfo.Id) - result, err := handler.GetImage(imageReqInfo.Id) - if err != nil { - cblogger.Infof("[%s] Image 조회 실패 : ", imageReqInfo.Id, err) - } else { - cblogger.Infof("[%s] Image 조회 결과 : [%s]", imageReqInfo.Id, result) - spew.Dump(result) - } - - case 4: - cblogger.Infof("[%s] Image 삭제 테스트", imageReqInfo.Id) - result, err := handler.DeleteImage(imageReqInfo.Id) - if err != nil { - cblogger.Infof("[%s] Image 삭제 실패 : ", imageReqInfo.Id, err) - } else { - cblogger.Infof("[%s] Image 삭제 결과 : [%s]", imageReqInfo.Id, result) - } - } - } - } -} - -*/ - func handleSecurity() { cblogger.Debug("Start Security Resource Test") @@ -368,25 +169,25 @@ func handleSecurity() { case 1: result, err := handler.ListSecurity() if err != nil { - cblogger.Infof(" Security 목록 조회 실패 : ", err) + cblogger.Infof("Result of Security list retrieval : ", err) } else { - cblogger.Info("Security 목록 조회 결과") - //cblogger.Info(result) + cblogger.Info("Result of Security list retrieval") + spew.Dump(result) if result != nil { - securityId = result[0].IId.SystemId // 조회 및 삭제를 위해 생성된 ID로 변경 + securityId = result[0].IId.SystemId // Changed to the ID created for retrieval and deletion } } case 2: - cblogger.Infof("[%s] Security 생성 테스트", securityName) + cblogger.Infof("[%s] Security creation test", securityName) tag1 := irs.KeyValue{Key: "", Value: ""} securityReqInfo := irs.SecurityReqInfo{ IId: irs.IID{NameId: securityName}, VpcIID: irs.IID{SystemId: vpcId}, TagList: []irs.KeyValue{tag1}, - SecurityRules: &[]irs.SecurityRuleInfo{ //보안 정책 설정 - //CIDR 테스트 + SecurityRules: &[]irs.SecurityRuleInfo{ // Security policy configuration + // CIDR Test /*{ FromPort: "20", ToPort: "22", @@ -445,7 +246,7 @@ func handleSecurity() { { //FromPort: "8443", //ToPort: "9999", - IPProtocol: "-1", // 모두 허용 (포트 정보 없음) + IPProtocol: "-1", // Allow all Direction: "inbound", },*/ { @@ -460,37 +261,36 @@ func handleSecurity() { result, err := handler.CreateSecurity(securityReqInfo) if err != nil { - cblogger.Infof(securityName, " Security 생성 실패 : ", err) + cblogger.Infof(securityName, "Security creation Failed : ", err) } else { - cblogger.Infof("[%s] Security 생성 결과 : [%v]", securityName, result) + cblogger.Infof("[%s] Result of Security creation : [%v]", securityName, result) securityId = result.IId.SystemId spew.Dump(result) } case 3: - cblogger.Infof("[%s] Security 조회 테스트", securityId) + cblogger.Infof("[%s] Security retrieval test", securityId) result, err := handler.GetSecurity(irs.IID{SystemId: securityId}) if err != nil { - cblogger.Infof(securityId, " Security 조회 실패 : ", err) + cblogger.Infof(securityId, "Failed to retrieve Security list : ", err) } else { - cblogger.Infof("[%s] Security 조회 결과 : [%v]", securityId, result) + cblogger.Infof("[%s] Result of Security retrieve : [%v]", securityId, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] Security 삭제 테스트", securityId) + cblogger.Infof("[%s] Security delete test", securityId) result, err := handler.DeleteSecurity(irs.IID{SystemId: securityId}) if err != nil { - cblogger.Infof(securityId, " Security 삭제 실패 : ", err) + cblogger.Infof(securityId, "Failed to deletion Security : ", err) } else { - cblogger.Infof("[%s] Security 삭제 결과 : [%s]", securityId, result) + cblogger.Infof("[%s] Result of Security delete : [%s]", securityId, result) } case 5: - cblogger.Infof("[%s] Rule 추가 테스트", securityId) + cblogger.Infof("[%s] Test of add Rule", securityId) securityRules := &[]irs.SecurityRuleInfo{ /*{ - //20-22 Prot로 등록 FromPort: "20", ToPort: "21", IPProtocol: "tcp", @@ -498,7 +298,6 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 FromPort: "20", ToPort: "21", IPProtocol: "tcp", @@ -506,46 +305,45 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - // 8080 Port로 등록 FromPort: "8080", - ToPort: "8080", //FromPort나 ToPort중 하나에 -1이 입력될 경우 -1이 입력된 경우 -1을 공백으로 처리 + ToPort: "8080", // If either FromPort or ToPort is set to -1, replace the -1 with a blank IPProtocol: "tcp", Direction: "inbound", CIDR: "0.0.0.0/0", },*/ - /*{ // 1323 Prot로 등록 - FromPort: "1323", //FromPort나 ToPort중 하나에 -1이 입력될 경우 -1이 입력된 경우 -1을 공백으로 처리 + /*{ // + FromPort: "1323", // If either FromPort or ToPort is set to -1, replace the -1 with a blank ToPort: "1323", IPProtocol: "tcp", Direction: "inbound", CIDR: "0.0.0.0/0", },*/ /*{ - // All Port로 등록 + // All Port FromPort: "", ToPort: "", - IPProtocol: "icmp", //icmp는 포트 정보가 없음 + IPProtocol: "icmp", // ICMP has no port information. Direction: "inbound", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Port FromPort: "20", ToPort: "22", IPProtocol: "tcp", Direction: "inbound", },*/ /*{ - // 80 Port로 등록 + // 80 Port FromPort: "80", ToPort: "80", IPProtocol: "tcp", Direction: "inbound", CIDR: "0.0.0.0/0", },*/ - /*{ // 모든 프로토콜 모든 포트로 등록 + /*{ // All port //FromPort: "", //ToPort: "", - IPProtocol: "all", // 모두 허용 (포트 정보 없음) + IPProtocol: "all", Direction: "inbound", CIDR: "0.0.0.0/0", },*/ @@ -563,7 +361,7 @@ func handleSecurity() { Direction: "outbound", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "22", ToPort: "22", IPProtocol: "tcp", @@ -571,7 +369,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "1000", ToPort: "1000", IPProtocol: "tcp", @@ -579,7 +377,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "1", ToPort: "65535", IPProtocol: "udp", @@ -587,7 +385,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "-1", ToPort: "-1", IPProtocol: "icmp", @@ -595,7 +393,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "-1", ToPort: "-1", IPProtocol: "all", @@ -603,7 +401,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "22", ToPort: "22", IPProtocol: "tcp", @@ -611,7 +409,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "1000", ToPort: "1000", IPProtocol: "tcp", @@ -619,7 +417,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "1", ToPort: "65535", IPProtocol: "udp", @@ -627,7 +425,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "22", ToPort: "22", IPProtocol: "tcp", @@ -635,7 +433,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Prot FromPort: "1000", ToPort: "1000", IPProtocol: "tcp", @@ -643,7 +441,7 @@ func handleSecurity() { CIDR: "4.5.6.7/32", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Port FromPort: "1", ToPort: "65535", IPProtocol: "udp", @@ -651,7 +449,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", }, { - //20-22 Prot로 등록 + //20-22 Prot FromPort: "-1", ToPort: "-1", IPProtocol: "icmp", @@ -659,7 +457,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ { - //20-22 Prot로 등록 + //20-22 Port FromPort: "22", ToPort: "22", IPProtocol: "tcp", @@ -667,7 +465,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", }, { - //20-22 Prot로 등록 + //20-22 Port FromPort: "1000", ToPort: "1000", IPProtocol: "tcp", @@ -675,7 +473,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", }, { - //20-22 Prot로 등록 + //20-22 Port FromPort: "1", ToPort: "65535", IPProtocol: "udp", @@ -683,7 +481,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", }, { - //20-22 Prot로 등록 + //20-22 Port FromPort: "-1", ToPort: "-1", IPProtocol: "icmp", @@ -694,16 +492,16 @@ func handleSecurity() { result, err := handler.AddRules(irs.IID{SystemId: securityId}, securityRules) if err != nil { - cblogger.Infof(securityId, " Rule 추가 실패 : ", err) + cblogger.Infof(securityId, "Failed to add Rule : ", err) } else { - cblogger.Infof("[%s] Rule 추가 결과 : [%v]", securityId, result) + cblogger.Infof("[%s] Result of add Rule : [%v]", securityId, result) spew.Dump(result) } case 6: - cblogger.Infof("[%s] Rule 삭제 테스트", securityId) + cblogger.Infof("[%s] Test of delete Rule", securityId) securityRules := &[]irs.SecurityRuleInfo{ /*{ - //20-22 Prot로 등록 + //20-22 Port FromPort: "20", ToPort: "21", IPProtocol: "tcp", @@ -711,7 +509,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Port FromPort: "20", ToPort: "21", IPProtocol: "tcp", @@ -726,7 +524,7 @@ func handleSecurity() { CIDR: "10.13.1.10/32", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Port FromPort: "20", ToPort: "22", IPProtocol: "tcp", @@ -734,7 +532,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - //20-22 Prot로 등록 + //20-22 Port FromPort: "20", ToPort: "21", IPProtocol: "udp", @@ -742,37 +540,37 @@ func handleSecurity() { CIDR: "0.0.0.0/0", },*/ /*{ - // 8080 Port로 등록 + // 8080 Port FromPort: "8080", - ToPort: "8080", //FromPort나 ToPort중 하나에 -1이 입력될 경우 -1이 입력된 경우 -1을 공백으로 처리 + ToPort: "8080", // If either FromPort or ToPort is set to -1, replace the -1 with a blank IPProtocol: "tcp", Direction: "inbound", CIDR: "0.0.0.0/0", },*/ - /*{ // 1323 Prot로 등록 - FromPort: "1323", //FromPort나 ToPort중 하나에 -1이 입력될 경우 -1이 입력된 경우 -1을 공백으로 처리 + /*{ // 1323 Port + FromPort: "1323", // If either FromPort or ToPort is set to -1, replace the -1 with a blank ToPort: "1323", IPProtocol: "tcp", Direction: "inbound", CIDR: "0.0.0.0/0", },*/ /*{ - // All Port로 등록 + // All Port FromPort: "", ToPort: "", - IPProtocol: "icmp", //icmp는 포트 정보가 없음 + IPProtocol: "icmp", // ICMP has no port information Direction: "inbound", CIDR: "0.0.0.0/0", },*/ - /*{ // 모든 프로토콜 모든 포트로 등록 + /*{ //FromPort: "", //ToPort: "", - IPProtocol: "all", // 모두 허용 (포트 정보 없음) + IPProtocol: "all", Direction: "inbound", CIDR: "0.0.0.0/0", },*/ { - //20-22 Prot로 등록 + //20-22 Port FromPort: "22", ToPort: "22", IPProtocol: "tcp", @@ -780,7 +578,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", }, { - //20-22 Prot로 등록 + //20-22 Port FromPort: "1000", ToPort: "1000", IPProtocol: "tcp", @@ -788,7 +586,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", }, { - //20-22 Prot로 등록 + //20-22 Port FromPort: "1", ToPort: "65535", IPProtocol: "udp", @@ -796,7 +594,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", }, { - //20-22 Prot로 등록 + //20-22 Port FromPort: "-1", ToPort: "-1", IPProtocol: "icmp", @@ -804,7 +602,7 @@ func handleSecurity() { CIDR: "0.0.0.0/0", }, /*{ - //20-22 Prot로 등록 + //20-22 Port FromPort: "-1", ToPort: "-1", IPProtocol: "all", @@ -815,9 +613,9 @@ func handleSecurity() { result, err := handler.RemoveRules(irs.IID{SystemId: securityId}, securityRules) if err != nil { - cblogger.Infof(securityId, " Rule 삭제 실패 : ", err) + cblogger.Infof(securityId, "Fail to delete Rule : ", err) } else { - cblogger.Infof("[%s] Rule 삭제 결과 : [%v]", securityId, result) + cblogger.Infof("[%s] Result of delete Rule : [%v]", securityId, result) } } } @@ -862,74 +660,52 @@ func handleKeyPair() { case 1: result, err := handler.ListKey() if err != nil { - cblogger.Infof(" 키 페어 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve Keypair list : ", err) } else { - cblogger.Info("키 페어 목록 조회 결과") + cblogger.Info("Result of retrieve Keypair list") //cblogger.Info(result) spew.Dump(result) if result != nil { - keyPairName = result[0].IId.SystemId // 조회 및 삭제를 위해 생성된 ID로 변경 + keyPairName = result[0].IId.SystemId // Changed to the ID created for retrieval and deletion } - cblogger.Info("키 페어 수 : ", len(result)) + cblogger.Info("Number of Keypair : ", len(result)) } case 2: - cblogger.Infof("[%s] 키 페어 생성 테스트", keyPairName) + cblogger.Infof("[%s] Test of creation Keypair", keyPairName) keyPairReqInfo := irs.KeyPairReqInfo{ IId: irs.IID{NameId: keyPairName}, TagList: []irs.KeyValue{tag1}, } result, err := handler.CreateKey(keyPairReqInfo) if err != nil { - cblogger.Infof(keyPairName, " 키 페어 생성 실패 : ", err) + cblogger.Infof(keyPairName, "Fail to create Keypair : ", err) } else { - cblogger.Infof("[%s] 키 페어 생성 결과 : [%s]", keyPairName, result) + cblogger.Infof("[%s] Result of create Keypair : [%s]", keyPairName, result) spew.Dump(result) } case 3: - cblogger.Infof("[%s] 키 페어 조회 테스트", keyPairName) + cblogger.Infof("[%s] Test of retrieve Keypair", keyPairName) result, err := handler.GetKey(irs.IID{SystemId: keyPairName}) if err != nil { - cblogger.Infof(keyPairName, " 키 페어 조회 실패 : ", err) + cblogger.Infof(keyPairName, " Fail to retrieve Keypair : ", err) } else { - cblogger.Infof("[%s] 키 페어 조회 결과 : [%s]", keyPairName, result) + cblogger.Infof("[%s] Result of Keypair : [%s]", keyPairName, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] 키 페어 삭제 테스트", keyPairName) + cblogger.Infof("[%s] Test of delete Keypair", keyPairName) result, err := handler.DeleteKey(irs.IID{SystemId: keyPairName}) if err != nil { - cblogger.Infof(keyPairName, " 키 페어 삭제 실패 : ", err) + cblogger.Infof(keyPairName, "Fail to delete Keypair : ", err) } else { - cblogger.Infof("[%s] 키 페어 삭제 결과 : [%s]", keyPairName, result) + cblogger.Infof("[%s] Result of delete Keypair : [%s]", keyPairName, result) } } } } } -/* -func TestMain() { - cblogger.Debug("Start ImageHandler Resource Test") - - ResourceHandler, err := testconf.GetResourceHandler("Image") - if err != nil { - panic(err) - } - handler := ResourceHandler.(irs.ImageHandler) - - result, err := handler.ListImage() - if err != nil { - cblogger.Infof(" Image 목록 조회 실패 : ", err) - } else { - cblogger.Info("Image 목록 조회 결과") - cblogger.Info(result) - cblogger.Info("출력 결과 수 : ", len(result)) - spew.Dump(result) - } -} -*/ - func handleVPC() { cblogger.Debug("Start VPC Resource Test") ResourceHandler, err := testconf.GetResourceHandler("VPC") @@ -943,8 +719,8 @@ func handleVPC() { IPv4_CIDR: "10.0.3.0/24", } - subnetReqVpcInfo := irs.IID{SystemId: "vpc-6wex2mrx1fovfecsl44mx"} - reqSubnetId := irs.IID{SystemId: "vsw-6we4h4n4wp9xdtakrno15"} + subnetReqVpcInfo := irs.IID{SystemId: ""} + reqSubnetId := irs.IID{SystemId: ""} cblogger.Debug(subnetReqInfo) cblogger.Debug(subnetReqVpcInfo) cblogger.Debug(reqSubnetId) @@ -968,13 +744,13 @@ func handleVPC() { }, }, // TagList: []irs.KeyValue{tag1}, - //Id: "subnet-044a2b57145e5afc5", - //Name: "CB-VNet-Subnet", // 웹 도구 등 외부에서 전달 받지 않고 드라이버 내부적으로 자동 구현때문에 사용하지 않음. + //Id: "", + //Name: "CB-VNet-Subnet", // Not used due to internal automatic implementation within the driver, without relying on external web tools //CidrBlock: "10.0.0.0/16", //CidrBlock: "192.168.0.0/16", } - reqVpcId := irs.IID{SystemId: "vpc-j6c64o9vym4zqqmvx3j3c"} + reqVpcId := irs.IID{SystemId: ""} for { fmt.Println("Handler Management") @@ -1000,69 +776,68 @@ func handleVPC() { case 1: result, err := handler.ListVPC() if err != nil { - cblogger.Infof(" VPC 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve VPC list : ", err) } else { - cblogger.Info("VPC 목록 조회 결과") - //cblogger.Info(result) - spew.Dump(result) + cblogger.Info("Result of retrieve VPC list") - // 내부적으로 1개만 존재함. - //조회및 삭제 테스트를 위해 리스트의 첫번째 서브넷 ID를 요청ID로 자동 갱신함. + spew.Dump(result) + // Only one exists internally + // Automatically updated the request ID to the first subnet ID in the list for retrieval and deletion tests if result != nil { - reqVpcId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 - subnetReqVpcInfo = reqVpcId //Subnet 추가/삭제 테스트용 + reqVpcId = result[0].IId // Changed to the ID created for retrieval and deletion. + subnetReqVpcInfo = reqVpcId // For testing subnet addition/deletion. } } case 2: - cblogger.Infof("[%s] VPC 생성 테스트", vpcReqInfo.IId.NameId) + cblogger.Infof("[%s] Test of create VPC", vpcReqInfo.IId.NameId) //vpcReqInfo := irs.VPCReqInfo{} result, err := handler.CreateVPC(vpcReqInfo) if err != nil { - cblogger.Infof(reqVpcId.NameId, " VPC 생성 실패 : ", err) + cblogger.Infof(reqVpcId.NameId, "Fail to create VPC : ", err) } else { - cblogger.Infof("VPC 생성 결과 : ", result) - reqVpcId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 + cblogger.Infof("Result of create VPC : ", result) + reqVpcId = result.IId spew.Dump(result) } case 3: - cblogger.Infof("[%s] VPC 조회 테스트", reqVpcId) + cblogger.Infof("[%s] Test of retrieve VPC", reqVpcId) result, err := handler.GetVPC(reqVpcId) if err != nil { - cblogger.Infof("[%s] VPC 조회 실패 : ", reqVpcId, err) + cblogger.Infof("[%s] Fail to retrieve VPC : ", reqVpcId, err) } else { - cblogger.Infof("[%s] VPC 조회 결과 : [%s]", reqVpcId, result) + cblogger.Infof("[%s] Result of retrieve VPC : [%s]", reqVpcId, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] VPC 삭제 테스트", reqVpcId) + cblogger.Infof("[%s] Test of delete VPC", reqVpcId) result, err := handler.DeleteVPC(reqVpcId) if err != nil { - cblogger.Infof("[%s] VPC 삭제 실패 : ", reqVpcId, err) + cblogger.Infof("[%s] Fail to delete VPC : ", reqVpcId, err) } else { - cblogger.Infof("[%s] VPC 삭제 결과 : [%s]", reqVpcId, result) + cblogger.Infof("[%s] Result of delete VPC : [%s]", reqVpcId, result) } case 5: - cblogger.Infof("[%s] Subnet 추가 테스트", vpcReqInfo.IId.NameId) + cblogger.Infof("[%s] Test of add Subnet", vpcReqInfo.IId.NameId) result, err := handler.AddSubnet(subnetReqVpcInfo, subnetReqInfo) if err != nil { - cblogger.Infof(reqSubnetId.NameId, " Subnet 추가 실패 : ", err) + cblogger.Infof(reqSubnetId.NameId, "Fail to add Subnet : ", err) } else { - cblogger.Infof("Subnet 추가 결과 : ", result) - reqSubnetId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 + cblogger.Infof("Result of ad Subnet : ", result) + reqSubnetId = result.IId spew.Dump(result) } case 6: - cblogger.Infof("[%s] Subnet 삭제 테스트", reqSubnetId.SystemId) + cblogger.Infof("[%s] Test of delete Subnet", reqSubnetId.SystemId) result, err := handler.RemoveSubnet(subnetReqVpcInfo, reqSubnetId) if err != nil { - cblogger.Infof("[%s] Subnet 삭제 실패 : ", reqSubnetId.SystemId, err) + cblogger.Infof("[%s] Fail to delete Subnet : ", reqSubnetId.SystemId, err) } else { - cblogger.Infof("[%s] Subnet 삭제 결과 : [%s]", reqSubnetId.SystemId, result) + cblogger.Infof("[%s] Result of delete Subnet : [%s]", reqSubnetId.SystemId, result) } } } @@ -1081,8 +856,8 @@ func handleImage() { //imageReqInfo := irs2.ImageReqInfo{ imageReqInfo := irs.ImageReqInfo{ - IId: irs.IID{NameId: "Test OS Image", SystemId: "ami-047f7b46bd6dd5d84"}, - //Id: "ami-047f7b46bd6dd5d84", + IId: irs.IID{NameId: "Test OS Image", SystemId: ""}, + //Id: "", //Name: "Test OS Image", } @@ -1108,50 +883,49 @@ func handleImage() { case 1: result, err := handler.ListImage() if err != nil { - cblogger.Infof(" Image 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve Image list : ", err) } else { - cblogger.Info("Image 목록 조회 결과") + cblogger.Info("Result of retrieve Image list") cblogger.Debug(result) - cblogger.Info("출력 결과 수 : ", len(result)) + cblogger.Info("Number of result : ", len(result)) if cblogger.Level.String() == "debug" { spew.Dump(result) } - //조회및 삭제 테스트를 위해 리스트의 첫번째 정보의 ID를 요청ID로 자동 갱신함. if result != nil { - imageReqInfo.IId = result[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 + imageReqInfo.IId = result[0].IId } } case 2: - cblogger.Infof("[%s] Image 생성 테스트", imageReqInfo.IId.NameId) + cblogger.Infof("[%s] Test of create Image", imageReqInfo.IId.NameId) result, err := handler.CreateImage(imageReqInfo) if err != nil { - cblogger.Infof(imageReqInfo.IId.NameId, " Image 생성 실패 : ", err) + cblogger.Infof(imageReqInfo.IId.NameId, "Fail to create Image : ", err) } else { - cblogger.Infof("Image 생성 결과 : ", result) + cblogger.Infof("Result of create Image : ", result) imageReqInfo.IId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 spew.Dump(result) } case 3: - cblogger.Infof("[%s] Image 조회 테스트", imageReqInfo.IId) + cblogger.Infof("[%s] Test of retrieve Image", imageReqInfo.IId) result, err := handler.GetImage(imageReqInfo.IId) if err != nil { - cblogger.Infof("[%s] Image 조회 실패 : ", imageReqInfo.IId.NameId, err) + cblogger.Infof("[%s] Fail to retrieve Image : ", imageReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] Image 조회 결과 : [%s]", imageReqInfo.IId.NameId, result) + cblogger.Infof("[%s] Result of retrieve Image : [%s]", imageReqInfo.IId.NameId, result) spew.Dump(result) } case 4: - cblogger.Infof("[%s] Image 삭제 테스트", imageReqInfo.IId.NameId) + cblogger.Infof("[%s] Test of delete Image", imageReqInfo.IId.NameId) result, err := handler.DeleteImage(imageReqInfo.IId) if err != nil { - cblogger.Infof("[%s] Image 삭제 실패 : ", imageReqInfo.IId.NameId, err) + cblogger.Infof("[%s] Fail to delete Image : ", imageReqInfo.IId.NameId, err) } else { - cblogger.Infof("[%s] Image 삭제 결과 : [%s]", imageReqInfo.IId.NameId, result) + cblogger.Infof("[%s] Result of delete Image : [%s]", imageReqInfo.IId.NameId, result) } } } @@ -1202,23 +976,20 @@ func handleVM() { vmReqInfo := irs.VMReqInfo{ // IId: irs.IID{NameId: ""}, - // ImageIID: irs.IID{SystemId: "aliyun_3_x64_20G_alibase_20210425.vhd"}, - ImageIID: irs.IID{SystemId: "aliyun_2_1903_x64_20G_dengbao_alibase_20240705.vhd"}, - //ImageIID: irs.IID{SystemId: "aliyun_2_1903_x64_20G_alibase_20200324.vhd"}, - //ImageIID: irs.IID{SystemId: "ubuntu_18_04_x64_20G_alibase_20210318.vhd"}, - // ImageIID: irs.IID{SystemId: "ubuntu_18_04_x64_20G_alibase_20210420.vhd"}, - VpcIID: irs.IID{SystemId: "vpc-j6cjneevz77bun1f0hodv"}, - //SubnetIID: irs.IID{SystemId: "vsw-0jlj155cbwhjumtipnm6d"}, + + ImageIID: irs.IID{SystemId: ""}, + VpcIID: irs.IID{SystemId: ""}, + //SubnetIID: irs.IID{SystemId: ""}, // SubnetIID: irs.IID{SystemId: ""}, //Tokyo Zone B - SubnetIID: irs.IID{SystemId: "vsw-j6c37ennotvnfcvuw23ku"}, //hongkong-c + SubnetIID: irs.IID{SystemId: ""}, //hongkong-c //SecurityGroupIIDs: []irs.IID{{SystemId: ""}, {SystemId: ""}}, - SecurityGroupIIDs: []irs.IID{{SystemId: "sg-j6cauy6gbpy26deg8iz9"}}, // 홍콩 리전 + SecurityGroupIIDs: []irs.IID{{SystemId: ""}}, // 홍콩 리전 //VMSpecName: "ecs.t5-lc2m1.nano", //VMSpecName: "ecs.g6.large", //cn-wulanchabu 리전 // VMSpecName: "ecs.t5-lc2m1.nano", //도쿄리전 VMSpecName: "ecs.t5-lc2m1.nano", //홍콩리전 // KeyPairIID: irs.IID{SystemId: ""}, - KeyPairIID: irs.IID{SystemId: "keypair-honkong-cqbnlg6iuvoi0eb89lq0"}, //홍콩리전 + KeyPairIID: irs.IID{SystemId: ""}, //홍콩리전 //VMUserId: "root", //root만 가능 //VMUserPasswd: "Cbuser!@#", //대문자 소문자 모두 사용되어야 함. 그리고 숫자나 특수 기호 중 하나가 포함되어야 함. @@ -1232,10 +1003,10 @@ func handleVM() { vmInfo, err := vmHandler.StartVM(vmReqInfo) if err != nil { //panic(err) - cblogger.Error("VM 생성 실패 - 실패 이유") + cblogger.Error("Fail to create VM") cblogger.Error(err) } else { - cblogger.Info("VM 생성 완료!!", vmInfo) + cblogger.Info("Result of create VM", vmInfo) spew.Dump(vmInfo) VmID = vmInfo.IId } @@ -1246,10 +1017,10 @@ func handleVM() { case 2: vmInfo, err := vmHandler.GetVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM 정보 조회 실패", VmID) + cblogger.Errorf("[%s] Fail to retrieve VM", VmID) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM 정보 조회 결과", VmID) + cblogger.Infof("[%s] Result of retrieve VM", VmID) cblogger.Info(vmInfo) spew.Dump(vmInfo) } @@ -1258,60 +1029,60 @@ func handleVM() { cblogger.Info("Start Suspend VM ...") result, err := vmHandler.SuspendVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM Suspend 실패 - [%s]", VmID, result) + cblogger.Errorf("[%s] VM Suspend fail - [%s]", VmID, result) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Suspend 성공 - [%s]", VmID, result) + cblogger.Infof("[%s] VM Suspend success - [%s]", VmID, result) } case 4: cblogger.Info("Start Resume VM ...") result, err := vmHandler.ResumeVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM Resume 실패 - [%s]", VmID, result) + cblogger.Errorf("[%s] VM Resume fail - [%s]", VmID, result) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Resume 성공 - [%s]", VmID, result) + cblogger.Infof("[%s] VM Resume success - [%s]", VmID, result) } case 5: cblogger.Info("Start Reboot VM ...") result, err := vmHandler.RebootVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM Reboot 실패 - [%s]", VmID, result) + cblogger.Errorf("[%s] VM Reboot fail - [%s]", VmID, result) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Reboot 성공 - [%s]", VmID, result) + cblogger.Infof("[%s] VM Reboot success - [%s]", VmID, result) } case 6: cblogger.Info("Start Terminate VM ...") result, err := vmHandler.TerminateVM(VmID) if err != nil { - cblogger.Errorf("[%s] VM Terminate 실패 - [%s]", VmID, result) + cblogger.Errorf("[%s] VM Terminate fail - [%s]", VmID, result) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Terminate 성공 - [%s]", VmID, result) + cblogger.Infof("[%s] VM Terminate success - [%s]", VmID, result) } case 7: cblogger.Info("Start Get VM Status...") vmStatus, err := vmHandler.GetVMStatus(VmID) if err != nil { - cblogger.Errorf("[%s] VM Get Status 실패", VmID) + cblogger.Errorf("[%s] VM Get Status fail", VmID) cblogger.Error(err) } else { - cblogger.Infof("[%s] VM Get Status 성공 : [%s]", VmID, vmStatus) + cblogger.Infof("[%s] VM Get Status success : [%s]", VmID, vmStatus) } case 8: cblogger.Info("Start ListVMStatus ...") vmStatusInfos, err := vmHandler.ListVMStatus() if err != nil { - cblogger.Error("ListVMStatus 실패") + cblogger.Error("ListVMStatus fail") cblogger.Error(err) } else { - cblogger.Info("ListVMStatus 성공") + cblogger.Info("ListVMStatus success") cblogger.Info(vmStatusInfos) spew.Dump(vmStatusInfos) } @@ -1320,11 +1091,11 @@ func handleVM() { cblogger.Info("Start ListVM ...") vmList, err := vmHandler.ListVM() if err != nil { - cblogger.Error("ListVM 실패") + cblogger.Error("ListVM fail") cblogger.Error(err) } else { - cblogger.Info("ListVM 성공") - cblogger.Info("=========== VM 목록 ================") + cblogger.Info("ListVM success") + cblogger.Info("=========== VM List ================") cblogger.Info(vmList) spew.Dump(vmList) if len(vmList) > 0 { @@ -1351,7 +1122,7 @@ func handleNLB() { nlbReqInfo := irs.NLBInfo{ // TCP IId: irs.IID{NameId: ""}, - VpcIID: irs.IID{SystemId: "j6cjneevz77bun1f0hodv"}, + VpcIID: irs.IID{SystemId: ""}, Type: "PUBLIC", Listener: irs.ListenerInfo{Protocol: "TCP", Port: "80"}, HealthChecker: irs.HealthCheckerInfo{Protocol: "TCP", Port: "80", Interval: 5, Timeout: 2, Threshold: 3}, @@ -1359,7 +1130,7 @@ func handleNLB() { Protocol: "TCP", Port: "80", - VMs: &[]irs.IID{{SystemId: "i-j6camjyolcjjhbs3ack7"}, {SystemId: "i-j6cgtrzdfo2tvrjzjw34"}}, + VMs: &[]irs.IID{{SystemId: ""}, {SystemId: ""}}, }, TagList: []irs.KeyValue{tag1}, // UDP @@ -1406,104 +1177,104 @@ func handleNLB() { case 1: result, err := handler.ListNLB() if err != nil { - cblogger.Infof(" NLB 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve NLB list : ", err) } else { - cblogger.Info("NLB 목록 조회 결과") - //cblogger.Info(result) + cblogger.Info("Result of retrieve NLB list") + spew.Dump(result) } case 2: - cblogger.Infof("[%s] NLB 생성 테스트", nlbReqInfo.IId.NameId) + cblogger.Infof("[%s] Test of create NLB", nlbReqInfo.IId.NameId) //vpcReqInfo := irs.VPCReqInfo{} result, err := handler.CreateNLB(nlbReqInfo) if err != nil { - cblogger.Infof(nlbReqInfo.IId.NameId, " NLB 생성 실패 : ", err) + cblogger.Infof(nlbReqInfo.IId.NameId, "Fail to create NLB : ", err) } else { - cblogger.Infof("NLB 생성 결과 : ", result) + cblogger.Infof("Result of create NLB : ", result) //reqNLBId = result.IId // 조회 및 삭제를 위해 생성된 ID로 변경 spew.Dump(result) } case 3: reqNLBId = irs.IID{SystemId: ""} - cblogger.Infof("[%s] NLB 조회 테스트", reqNLBId) + cblogger.Infof("[%s] Test of retrieve NLB", reqNLBId) result, err := handler.GetNLB(reqNLBId) if err != nil { - cblogger.Infof("[%s] NLB 조회 실패 : ", reqNLBId, err) + cblogger.Infof("[%s] Fail to retrieve NLB : ", reqNLBId, err) } else { - cblogger.Infof("[%s] NLB 조회 결과 : [%s]", reqNLBId, result) + cblogger.Infof("[%s] Result of retrieve NLB : [%s]", reqNLBId, result) spew.Dump(result) } case 4: reqNLBId.SystemId = "" - cblogger.Infof("[%s] NLB 삭제 테스트", reqNLBId) + cblogger.Infof("[%s] Test of delete NLB", reqNLBId) result, err := handler.DeleteNLB(reqNLBId) if err != nil { - cblogger.Infof("[%s] NLB 삭제 실패 : ", reqNLBId, err) + cblogger.Infof("[%s] Fail to delete NLB : ", reqNLBId, err) } else { - cblogger.Infof("[%s] NLB 삭제 결과 : [%s]", reqNLBId, result) + cblogger.Infof("[%s] Result of delete NLB : [%s]", reqNLBId, result) } case 5: - cblogger.Infof("[%s] VM 추가 테스트", reqNLBId) + cblogger.Infof("[%s] Test of add VM", reqNLBId) reqNLBId.SystemId = "" vmIID := irs.IID{SystemId: ""} result, err := handler.AddVMs(reqNLBId, &[]irs.IID{vmIID}) if err != nil { - cblogger.Infof("VM 추가 실패 : ", err) + cblogger.Infof("Fail to add VM : ", err) } else { - cblogger.Infof("VM 추가 결과 : ", result) + cblogger.Infof("Result of add VM : ", result) //reqSubnetId = result.SubnetInfoList[0].IId // 조회 및 삭제를 위해 생성된 ID로 변경 spew.Dump(result) } case 6: - cblogger.Infof("[%s] VM 삭제 테스트", reqNLBId.SystemId) + cblogger.Infof("[%s] Test of delete VM", reqNLBId.SystemId) reqNLBId.SystemId = "" vmIID := irs.IID{SystemId: ""} result, err := handler.RemoveVMs(reqNLBId, &[]irs.IID{vmIID}) if err != nil { - cblogger.Infof("VM 삭제 실패 : ", err) + cblogger.Infof("Fail to delete VM : ", err) } else { - cblogger.Infof("VM 삭제 결과 : [%s]", result) + cblogger.Infof("Result of delete VM : [%s]", result) } case 7: - cblogger.Infof("[%s] NLB VM Health 조회 테스트", reqNLBId) - cblogger.Infof("[%s] VM 추가 테스트", reqNLBId) + cblogger.Infof("[%s] Test of retrieve NLB VM", reqNLBId) + cblogger.Infof("[%s] Test of add VM", reqNLBId) reqNLBId.SystemId = "" result, err := handler.GetVMGroupHealthInfo(reqNLBId) if err != nil { - cblogger.Infof("[%s] NLB VM Health 조회 실패 : ", reqNLBId.SystemId, err) + cblogger.Infof("[%s] Fail to retrieve NLB VM : ", reqNLBId.SystemId, err) } else { - cblogger.Infof("[%s] NLB VM Health 조회 결과 : [%s]", reqNLBId.SystemId, result) + cblogger.Infof("[%s] Result of retrieve NLB VM Health : [%s]", reqNLBId.SystemId, result) spew.Dump(result) } case 8: - cblogger.Infof("[%s] NLB Listener 변경 테스트", reqNLBId) + cblogger.Infof("[%s] Test of change NLB Listener", reqNLBId) reqNLBId.SystemId = "" changeListener := irs.ListenerInfo{} changeListener.Protocol = "tcp" - changeListener.Port = "8080" // 포트만 변경 + changeListener.Port = "8080" result, err := handler.ChangeListener(reqNLBId, changeListener) if err != nil { - cblogger.Infof("[%s] NLB Listener 변경 실패 : ", reqNLBId.SystemId, err) + cblogger.Infof("[%s] Fail to change NLB Listener : ", reqNLBId.SystemId, err) } else { - cblogger.Infof("[%s] NLB Listener 변경 결과 : [%s]", reqNLBId.SystemId, result) + cblogger.Infof("[%s] Result of change NLB Listener : [%s]", reqNLBId.SystemId, result) spew.Dump(result) } case 9: - cblogger.Infof("[%s] NLB VM Group 변경 테스트", reqNLBId) + cblogger.Infof("[%s] Test of change NLB VM Group", reqNLBId) result, err := handler.ChangeVMGroupInfo(reqNLBId, irs.VMGroupInfo{ Protocol: "TCP", Port: "8080", }) if err != nil { - cblogger.Infof("[%s] NLB VM Group 변경 실패 : ", reqNLBId.SystemId, err) + cblogger.Infof("[%s] Fail to change NLB VM Group : ", reqNLBId.SystemId, err) } else { - cblogger.Infof("[%s] NLB VM Group 변경 결과 : [%s]", reqNLBId.SystemId, result) + cblogger.Infof("[%s] Result of change NLB VM Group : [%s]", reqNLBId.SystemId, result) spew.Dump(result) } case 10: @@ -1515,12 +1286,12 @@ func handleNLB() { Timeout: 30, Threshold: 9, } - cblogger.Infof("[%s] NLB Health Checker 변경 테스트", reqNLBId) + cblogger.Infof("[%s] Test of change NLB Health Checker", reqNLBId) result, err := handler.ChangeHealthCheckerInfo(reqNLBId, reqHealthCheckInfo) if err != nil { - cblogger.Infof("[%s] NLB Health Checker 변경 실패 : ", reqNLBId.SystemId, err) + cblogger.Infof("[%s] Fail to change NLB Health Checker : ", reqNLBId.SystemId, err) } else { - cblogger.Infof("[%s] NLB Health Checker 변경 결과 : [%s]", reqNLBId.SystemId, result) + cblogger.Infof("[%s] Resutl of change NLB Health Checker : [%s]", reqNLBId.SystemId, result) spew.Dump(result) } @@ -1561,36 +1332,36 @@ func handleRegionZone() { case 1: result, err := handler.ListRegionZone() if err != nil { - cblogger.Infof(" RegionZone 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve RegionZone list : ", err) } else { - cblogger.Info("RegionZone 목록 조회 결과") + cblogger.Info("Result of retrieve RegionZone list") spew.Dump(result) } case 2: result, err := handler.ListOrgRegion() if err != nil { - cblogger.Infof(" ListOrgRegion 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve ListOrgRegion list : ", err) } else { - cblogger.Info("ListOrgRegion 목록 조회 결과") + cblogger.Info("Result of retrieve ListOrgRegion list") spew.Dump(result) } case 3: result, err := handler.ListOrgZone() if err != nil { - cblogger.Infof(" ListOrgZone 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve ListOrgZone list : ", err) } else { - cblogger.Info("ListOrgZone 목록 조회 결과") + cblogger.Info("Result of retrieve ListOrgZone list") spew.Dump(result) } case 4: regionId := "ap-northeast-2" result, err := handler.GetRegionZone(regionId) if err != nil { - cblogger.Infof(" GetRegionZone 조회 실패 : ", regionId, err) + cblogger.Infof("Fail to retrieve GetRegionZone : ", regionId, err) } else { - cblogger.Info("GetRegionZone 조회 결과", regionId) + cblogger.Info("Result of retrieve GetRegionZone", regionId) spew.Dump(result) } @@ -1629,9 +1400,9 @@ func handlePriceInfo() { regionName := "cn-hongkong" result, err := handler.ListProductFamily(regionName) if err != nil { - cblogger.Infof(" ProductFamily 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve ProductFamily list : ", err) } else { - cblogger.Info("ProductFamily 목록 조회 결과") + cblogger.Info("Result of retrieve ProductFamily list") spew.Dump(result) } @@ -1641,10 +1412,10 @@ func handlePriceInfo() { result, err := handler.GetPriceInfo(productFamily, regionName, []irs.KeyValue{}) if err != nil { - cblogger.Info(" PriceInfo 조회 실패 : ", err) + cblogger.Info("Fail to retrieve PriceInfo : ", err) } else { - cblogger.Info("PriceInfo 조회 결과") - //spew.Dump(result) + cblogger.Info("Result of retrieve PriceInfo") + cblogger.Info(result) } } @@ -1680,7 +1451,7 @@ func handleTagInfo() { // resourceType := irs.RSType("VM") resourceType := irs.ALL - resourceIID := irs.IID{NameId: "", SystemId: "i-j6cd7rv72uwjyx8qy304"} + resourceIID := irs.IID{NameId: "", SystemId: ""} // resourceType := irs.RSType("CLUSTER") // resourceIID := irs.IID{NameId: "cs-issue-test", SystemId: ""} @@ -1692,9 +1463,9 @@ func handleTagInfo() { result, err := handler.ListTag(resourceType, resourceIID) if err != nil { - cblogger.Infof(" Tag 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve Tag list : ", err) } else { - cblogger.Info("Tag 목록 조회 결과") + cblogger.Info("Result of Tag list") spew.Dump(result) } @@ -1703,9 +1474,9 @@ func handleTagInfo() { tagName = "" result, err := handler.GetTag(resourceType, resourceIID, tagName) if err != nil { - cblogger.Info(" Tag 조회 실패 : ", err) + cblogger.Info("Fail to retrieve Tag : ", err) } else { - cblogger.Info("GetTag 조회 결과") + cblogger.Info("Result of retrieve GetTag") //spew.Dump(result) cblogger.Info(result) } @@ -1714,9 +1485,9 @@ func handleTagInfo() { result, err := handler.FindTag(resourceType, tagName) if err != nil { - cblogger.Info(" Tag 조회 실패 : ", err) + cblogger.Info("Fail to retrieve Tag : ", err) } else { - cblogger.Info("FindTag 조회 결과") + cblogger.Info("Result of retrieve FindTag") spew.Dump(result) // cblogger.Info(result) } @@ -1726,9 +1497,9 @@ func handleTagInfo() { newTag.Value = "addValueT1" result, err := handler.AddTag(resourceType, resourceIID, newTag) if err != nil { - cblogger.Info(" Tag 조회 실패 : ", err) + cblogger.Info("Fail to add Tag : ", err) } else { - cblogger.Info("AddTag 조회 결과") + cblogger.Info("Result of add Tag") //spew.Dump(result) cblogger.Info(result) } @@ -1736,9 +1507,9 @@ func handleTagInfo() { tagName := "addKeyT1" result, err := handler.RemoveTag(resourceType, resourceIID, tagName) if err != nil { - cblogger.Info(" Tag 조회 실패 : ", err) + cblogger.Info("Fail to remove Tag : ", err) } else { - cblogger.Info("RemoveTag 조회 결과") + cblogger.Info("Result of remove Tag") //spew.Dump(result) cblogger.Info(result) } @@ -1778,7 +1549,7 @@ func handleDisk() { panic(err) } // resourceType := irs.RSType("disk") - resourceIID := irs.IID{NameId: "", SystemId: "i-j6cd7rv72uwjyx8qy304"} + resourceIID := irs.IID{NameId: "", SystemId: ""} // resourceType := irs.RSType("CLUSTER") // resourceIID := irs.IID{NameId: "cs-issue-test", SystemId: ""} @@ -1790,9 +1561,9 @@ func handleDisk() { result, err := handler.ListDisk() if err != nil { - cblogger.Infof(" Tag 목록 조회 실패 : ", err) + cblogger.Infof("Fail to retrieve Disk list : ", err) } else { - cblogger.Info("Tag 목록 조회 결과") + cblogger.Info("Result of Disk list") spew.Dump(result) } @@ -1801,9 +1572,9 @@ func handleDisk() { // tagName = "" result, err := handler.GetDisk(resourceIID) if err != nil { - cblogger.Info(" Tag 조회 실패 : ", err) + cblogger.Info("Fail to get Disk : ", err) } else { - cblogger.Info("GetTag 조회 결과") + cblogger.Info("Result of get Disk") //spew.Dump(result) cblogger.Info(result) } @@ -1812,34 +1583,12 @@ func handleDisk() { // tagName = "" result, err := handler.CreateDisk(diskReqInfo) if err != nil { - cblogger.Info(" Tag 조회 실패 : ", err) + cblogger.Info("Fail to create Disk : ", err) } else { - cblogger.Info("FindTag 조회 결과") + cblogger.Info("Result of create Disk") //spew.Dump(result) cblogger.Info(result) } - // case 4: - // newTag := irs.KeyValue{} - // newTag.Key = "addKeyT1" - // newTag.Value = "addValueT1" - // result, err := handler.AddTag(resourceType, resourceIID, newTag) - // if err != nil { - // cblogger.Info(" Tag 조회 실패 : ", err) - // } else { - // cblogger.Info("AddTag 조회 결과") - // //spew.Dump(result) - // cblogger.Info(result) - // } - // case 5: - // tagName := "addKeyT1" - // result, err := handler.RemoveTag(resourceType, resourceIID, tagName) - // if err != nil { - // cblogger.Info(" Tag 조회 실패 : ", err) - // } else { - // cblogger.Info("RemoveTag 조회 결과") - // //spew.Dump(result) - // cblogger.Info(result) - // } } } } @@ -2066,7 +1815,7 @@ func main() { // handleDisk() // handleMyImage() // handleCluster() - //handlePublicIP() // PublicIP 생성 후 conf + //handlePublicIP() //handleVNic() //Lancard //handleRegionZone() @@ -2076,13 +1825,13 @@ func main() { //StartTime := "2020-05-07T01:35:00Z" StartTime := "2020-05-07T01:35Z" timeLen := len(StartTime) - cblogger.Infof("======> 생성시간 길이 [%s]", timeLen) + cblogger.Infof("======> create time length [%s]", timeLen) if timeLen > 7 { - cblogger.Infof("======> 생성시간 마지막 문자열 [%s]", StartTime[timeLen-1:]) + cblogger.Infof("======> create time last string [%s]", StartTime[timeLen-1:]) if StartTime[timeLen-1:] == "Z" { - cblogger.Infof("======> 문자열 변환 : [%s]", StartTime[:timeLen-1]) + cblogger.Infof("======> change string : [%s]", StartTime[:timeLen-1]) NewStartTime := StartTime[:timeLen-1] + ":00Z" - cblogger.Infof("======> 최종 문자열 변환 : [%s]", NewStartTime) + cblogger.Infof("======> return result string : [%s]", NewStartTime) } } From edeeb73415838a42cea90c1761723bae82e34656 Mon Sep 17 00:00:00 2001 From: SungWoongz Date: Thu, 25 Jul 2024 13:54:58 +0900 Subject: [PATCH 43/56] Simplify structs and update types --- .../alibaba/resources/CommonAlibabaFunc.go | 64 +++---- .../drivers/alibaba/resources/TagHandler.go | 167 ++++++++++-------- 2 files changed, 125 insertions(+), 106 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go index 94ed1317f..93ac6e28c 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/CommonAlibabaFunc.go @@ -286,26 +286,26 @@ func GetAlibabaApiVPCEndpoint(regionId string, productCode string) string { // Alibaba에서 사용되는 리소스별 api product type func GetAlibabaProductCode(resType irs.RSType) (string, error) { switch resType { - case irs.RSType("VM"), irs.VM: + case irs.VM: return "ecs", nil - case irs.RSType("VPC"), irs.VPC: + case irs.VPC: return "vpc", nil - case irs.RSType("SUBNET"), irs.SUBNET: + case irs.SUBNET: // return "ecs", nil return "vpc", nil - case irs.RSType("SG"), irs.SG: + case irs.SG: return "ecs", nil - case irs.RSType("KEY"), irs.KEY: + case irs.KEY: return "ecs", nil - case irs.RSType("NLB"), irs.NLB: + case irs.NLB: return "slb", nil - case irs.RSType("DISK"), irs.DISK: + case irs.DISK: return "ecs", nil - case irs.RSType("MYIMAGE"), irs.MYIMAGE: + case irs.MYIMAGE: return "ecs", nil - case irs.RSType("CLUSTER"), irs.CLUSTER: + case irs.CLUSTER: return "ack", nil - case irs.RSType("NODEGROUP"), irs.NODEGROUP: + case irs.NODEGROUP: return "ack", nil default: //return "", nil @@ -316,29 +316,29 @@ func GetAlibabaProductCode(resType irs.RSType) (string, error) { // cb-spider의 resourceType 을 alibaba의 resourceType으로 func GetAlibabaResourceType(resType irs.RSType) (string, error) { switch resType { - case irs.RSType("VM"), irs.VM: + case irs.VM: return "instance", nil - case irs.RSType("VPC"), irs.VPC: + case irs.VPC: return "vpc", nil - case irs.RSType("SUBNET"), irs.SUBNET: + case irs.SUBNET: // return "ecs", nil return "vpc", nil - case irs.RSType("SG"), irs.SG: + case irs.SG: return "securitygroup", nil - case irs.RSType("KEY"), irs.KEY: + case irs.KEY: return "keypair", nil - // case irs.RSType("NLB"): + // case irs.NLB: // return "slb", nil - case irs.RSType("DISK"), irs.DISK: + case irs.DISK: return "disk", nil - case irs.RSType("MYIMAGE"), irs.MYIMAGE: + case irs.MYIMAGE: // return "snapshot", nil return "ecs", nil - case irs.RSType("CLUSTER"), irs.CLUSTER: + case irs.CLUSTER: return "CLUSTER", nil - case irs.RSType("ALL"), irs.ALL: + case irs.ALL: return "", nil - // case irs.RSType("NODEGROUP"): + // case irs.NODEGROUP" // return "", nil default: //return "", nil @@ -356,27 +356,27 @@ func GetAlibabaResourceType(resType irs.RSType) (string, error) { // resource Type별로 바라보는 api가 다름. ( ecs, bss, ... ) func GetAliTargetApi(resType irs.RSType) (string, error) { switch resType { - case irs.RSType("VM"): + case irs.VM: return "ecs", nil - case irs.RSType("VPC"): + case irs.VPC: return "vpc", nil - case irs.RSType("SUBNET"): + case irs.SUBNET: return "vpc", nil - case irs.RSType("SG"): + case irs.SG: return "ecs", nil - case irs.RSType("KEY"): + case irs.KEY: return "ecs", nil - case irs.RSType("NLB"): + case irs.NLB: return "slb", nil - case irs.RSType("DISK"): + case irs.DISK: return "ecs", nil - case irs.RSType("MYIMAGE"): + case irs.MYIMAGE: return "ecs", nil - case irs.RSType("CLUSTER"): + case irs.CLUSTER: return "cs", nil - case irs.RSType("ALL"): + case irs.ALL: return "all", nil - // case irs.RSType("NODEGROUP"): + // case NODEGROUP"): // return "", nil default: //return "", nil diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go index eb85b4463..cf1678629 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/TagHandler.go @@ -77,87 +77,106 @@ type DescribeTagsResponse struct { PageNumber int `json:"PageNumber" xml:"PageNumber"` Tags ecs.TagsInDescribeTags `json:"Tags" xml:"Tags"` } +type VSwitchIds struct { + VSwitchId []string `json:"VSwitchId"` +} + +type SecondaryCidrBlocks struct { + SecondaryCidrBlock []string `json:"SecondaryCidrBlock"` +} + +type RouterTableIds struct { + RouterTableIds []string `json:"RouterTableIds"` +} + +type UserCidrs struct { + UserCidr []string `json:"UserCidr"` +} + +type NatGatewayIds struct { + NatGatewayIds []string `json:"NatGatewayIds"` +} + +type Vpc struct { + Status string `json:"Status"` + IsDefault bool `json:"IsDefault"` + CenStatus string `json:"CenStatus"` + Description string `json:"Description"` + ResourceGroupId string `json:"ResourceGroupId"` + VSwitchIds VSwitchIds `json:"VSwitchIds"` + SecondaryCidrBlocks SecondaryCidrBlocks `json:"SecondaryCidrBlocks"` + CidrBlock string `json:"CidrBlock"` + RouterTableIds RouterTableIds `json:"RouterTableIds"` + UserCidrs UserCidrs `json:"UserCidrs"` + NetworkAclNum int `json:"NetworkAclNum"` + AdvancedResource bool `json:"AdvancedResource"` + VRouterId string `json:"VRouterId"` + NatGatewayIds NatGatewayIds `json:"NatGatewayIds"` + VpcId string `json:"VpcId"` + OwnerId int64 `json:"OwnerId"` + CreationTime string `json:"CreationTime"` + VpcName string `json:"VpcName"` + EnabledIpv6 bool `json:"EnabledIpv6"` + RegionId string `json:"RegionId"` + Ipv6CidrBlock string `json:"Ipv6CidrBlock"` + Tags Tags `json:"Tags"` +} + +type Vpcs struct { + Vpc []Vpc `json:"Vpc"` +} + type DescribeVpcsResponse struct { TotalCount int `json:"TotalCount"` PageSize int `json:"PageSize"` RequestId string `json:"RequestId"` PageNumber int `json:"PageNumber"` - Vpcs struct { - Vpc []struct { - Status string `json:"Status"` - IsDefault bool `json:"IsDefault"` - CenStatus string `json:"CenStatus"` - Description string `json:"Description"` - ResourceGroupId string `json:"ResourceGroupId"` - VSwitchIds struct { - VSwitchId []string `json:"VSwitchId"` - } `json:"VSwitchIds"` - SecondaryCidrBlocks struct { - SecondaryCidrBlock []string `json:"SecondaryCidrBlock"` - } `json:"SecondaryCidrBlocks"` - CidrBlock string `json:"CidrBlock"` - RouterTableIds struct { - RouterTableIds []string `json:"RouterTableIds"` - } `json:"RouterTableIds"` - UserCidrs struct { - UserCidr []string `json:"UserCidr"` - } `json:"UserCidrs"` - NetworkAclNum int `json:"NetworkAclNum"` - AdvancedResource bool `json:"AdvancedResource"` - VRouterId string `json:"VRouterId"` - NatGatewayIds struct { - NatGatewayIds []string `json:"NatGatewayIds"` - } `json:"NatGatewayIds"` - VpcId string `json:"VpcId"` - OwnerId int64 `json:"OwnerId"` - CreationTime string `json:"CreationTime"` - VpcName string `json:"VpcName"` - EnabledIpv6 bool `json:"EnabledIpv6"` - RegionId string `json:"RegionId"` - Ipv6CidrBlock string `json:"Ipv6CidrBlock"` - Tags struct { - Tag []struct { - Value string `json:"Value"` - Key string `json:"Key"` - } `json:"Tag"` - } `json:"Tags"` - } `json:"Vpc"` - } `json:"Vpcs"` + Vpcs Vpcs `json:"Vpcs"` +} + +type Tag struct { + Value string `json:"Value"` + Key string `json:"Key"` +} + +type Tags struct { + Tag []Tag `json:"Tag"` +} + +type RouteTable struct { + RouteTableId string `json:"RouteTableId"` + RouteTableType string `json:"RouteTableType"` +} + +type VSwitch struct { + Status string `json:"Status"` + IsDefault bool `json:"IsDefault"` + Description string `json:"Description"` + ResourceGroupId string `json:"ResourceGroupId"` + ZoneId string `json:"ZoneId"` + NetworkAclId string `json:"NetworkAclId"` + AvailableIpAddressCount int `json:"AvailableIpAddressCount"` + VSwitchId string `json:"VSwitchId"` + CidrBlock string `json:"CidrBlock"` + RouteTable RouteTable `json:"RouteTable"` + VpcId string `json:"VpcId"` + OwnerId int64 `json:"OwnerId"` + CreationTime string `json:"CreationTime"` + VSwitchName string `json:"VSwitchName"` + Ipv6CidrBlock string `json:"Ipv6CidrBlock"` + Tags Tags `json:"Tags"` + ShareType string `json:"ShareType"` +} + +type VSwitches struct { + VSwitch []VSwitch `json:"VSwitch"` } type DescribeVSwitchesResponse struct { - TotalCount int `json:"TotalCount"` - PageSize int `json:"PageSize"` - RequestId string `json:"RequestId"` - PageNumber int `json:"PageNumber"` - VSwitches struct { - VSwitch []struct { - Status string `json:"Status"` - IsDefault bool `json:"IsDefault"` - Description string `json:"Description"` - ResourceGroupId string `json:"ResourceGroupId"` - ZoneId string `json:"ZoneId"` - NetworkAclId string `json:"NetworkAclId"` - AvailableIpAddressCount int `json:"AvailableIpAddressCount"` - VSwitchId string `json:"VSwitchId"` - CidrBlock string `json:"CidrBlock"` - RouteTable struct { - RouteTableId string `json:"RouteTableId"` - RouteTableType string `json:"RouteTableType"` - } `json:"RouteTable"` - VpcId string `json:"VpcId"` - OwnerId int64 `json:"OwnerId"` - CreationTime string `json:"CreationTime"` - VSwitchName string `json:"VSwitchName"` - Ipv6CidrBlock string `json:"Ipv6CidrBlock"` - Tags struct { - Tag []struct { - Value string `json:"Value"` - Key string `json:"Key"` - } `json:"Tag"` - } `json:"Tags"` - ShareType string `json:"ShareType"` - } `json:"VSwitch"` - } `json:"VSwitches"` + TotalCount int `json:"TotalCount"` + PageSize int `json:"PageSize"` + RequestId string `json:"RequestId"` + PageNumber int `json:"PageNumber"` + VSwitches VSwitches `json:"VSwitches"` } /* From a5bfb03ac22232c2d1259ae617892614ee25b7be Mon Sep 17 00:00:00 2001 From: dev4unet Date: Thu, 25 Jul 2024 08:07:44 +0000 Subject: [PATCH 44/56] Tested the Tag feature in Disk / MyImage --- .../drivers/aws/main/Test_Resources.go | 194 +++++++++++++++++- .../drivers/aws/resources/TagHandler.go | 33 ++- 2 files changed, 216 insertions(+), 11 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go index 64837edcc..d38de34fe 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/main/Test_Resources.go @@ -1806,12 +1806,15 @@ func handleTag() { var reqType irs.RSType = irs.KEY //reqIID := irs.IID{SystemId: "i-02ac1c4ff1d40815c"} reqIID := irs.IID{SystemId: "CB-KeyPairTest123123"} + reqIID = irs.IID{SystemId: "ami-03cda261fe3252863"} reqTag := irs.KeyValue{Key: "tag3", Value: "tag3 test"} reqKey := "tag3" reqKey = "CB-KeyPairTagTest" reqKey = "cb-nlb-test01123" - reqKey = "" + reqKey = "Name2" + reqKey = "cb-eks-cluster-tag-test" + reqKey = "tag3" reqType = irs.ALL for { @@ -1901,11 +1904,184 @@ func handleTag() { } } +// Test Disk +func handleDisk() { + cblogger.Debug("Start Disk Resource Test") + + ResourceHandler, err := getResourceHandler("Disk") + if err != nil { + panic(err) + } + handler := ResourceHandler.(irs.DiskHandler) + + reqIID := irs.IID{NameId: "tag-test", SystemId: "vol-0d0bdcd08f027c2a1"} + reqKey := "tag3" + reqInfo := irs.DiskInfo{ + IId: reqIID, + DiskType: "gp2", + DiskSize: "50", + + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: securityName+"123"}}, + TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}}, + } + + for { + fmt.Println("TagHandler Management") + fmt.Println("0. Quit") + fmt.Println("1. Disk List") + fmt.Println("2. Disk Create") + fmt.Println("3. Disk Get") + fmt.Println("4. Disk Delete") + + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + panic(err) + } + + if inputCnt == 1 { + switch commandNum { + case 0: + return + + case 1: + cblogger.Infof("Lookup disk list") + result, err := handler.ListDisk() + if err != nil { + cblogger.Info(" Disk List Lookup Failed : ", err) + } else { + cblogger.Info("Disk List Lookup Result") + cblogger.Debug(result) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) + //spew.Dump(result) + cblogger.Info("Number of output results : ", len(result)) + } + + case 2: + cblogger.Infof("[%s] Disk Create Test", reqIID.SystemId) + result, err := handler.CreateDisk(reqInfo) + if err != nil { + cblogger.Infof(reqIID.SystemId, " Disk Create Failed : ", err) + } else { + cblogger.Info("Disk Create Result : ", result) + spew.Dump(result) + } + + case 3: + cblogger.Infof("[%s] Disk Lookup Test - Key[%s]", reqIID.SystemId, reqKey) + result, err := handler.GetDisk(reqIID) + if err != nil { + cblogger.Infof("[%s] Disk Lookup Failed : [%v]", reqKey, err) + } else { + cblogger.Infof("[%s] Disk Lookup Result : [%s]", reqKey, result) + spew.Dump(result) + } + + case 4: + cblogger.Infof("[%s] Disk Delete Test - Key[%s]", reqIID.SystemId, reqKey) + result, err := handler.DeleteDisk(reqIID) + if err != nil { + cblogger.Infof("[%s] Disk Delete Failed : [%v]", reqKey, err) + } else { + cblogger.Infof("[%s] Disk Delete Result : [%v]", reqKey, result) + } + } + } + } +} + +// Test MyImage +func handleMyImage() { + cblogger.Debug("Start MyImage Resource Test") + + ResourceHandler, err := getResourceHandler("MyImage") + if err != nil { + panic(err) + } + handler := ResourceHandler.(irs.MyImageHandler) + + reqIID := irs.IID{NameId: "tag-test", SystemId: "ami-03cda261fe3252863"} + reqKey := "tag3" + reqInfo := irs.MyImageInfo{ + IId: reqIID, + SourceVM: irs.IID{SystemId: "i-0c65033e158e0fd99"}, + + //TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}, {Key: "Name", Value: securityName+"123"}}, + TagList: []irs.KeyValue{{Key: "Name1", Value: "Tag Name Value1"}, {Key: "Name2", Value: "Tag Name Value2"}}, + } + + for { + fmt.Println("TagHandler Management") + fmt.Println("0. Quit") + fmt.Println("1. MyImage List") + fmt.Println("2. MyImage Create") + fmt.Println("3. MyImage Get") + fmt.Println("4. MyImage Delete") + + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + panic(err) + } + + if inputCnt == 1 { + switch commandNum { + case 0: + return + + case 1: + cblogger.Infof("Lookup MyImage list") + result, err := handler.ListMyImage() + if err != nil { + cblogger.Info(" MyImage List Lookup Failed : ", err) + } else { + cblogger.Info("MyImage List Lookup Result") + cblogger.Debug(result) + cblogger.Infof("Log Level : [%s]", cblog.GetLevel()) + //spew.Dump(result) + cblogger.Info("Number of output results : ", len(result)) + } + + case 2: + cblogger.Infof("[%s] MyImage Create Test", reqIID.NameId) + result, err := handler.SnapshotVM(reqInfo) + if err != nil { + cblogger.Infof(reqIID.NameId, " MyImage Create Failed : ", err) + } else { + cblogger.Info("MyImage Create Result : ", result) + reqIID = result.IId + spew.Dump(result) + } + + case 3: + cblogger.Infof("[%s] MyImage Lookup Test - Key[%s]", reqIID.SystemId, reqKey) + result, err := handler.GetMyImage(reqIID) + if err != nil { + cblogger.Infof("[%s] MyImage Lookup Failed : [%v]", reqKey, err) + } else { + cblogger.Infof("[%s] MyImage Lookup Result : [%s]", reqKey, result) + spew.Dump(result) + } + + case 4: + cblogger.Infof("[%s] MyImage Delete Test - Key[%s]", reqIID.SystemId, reqKey) + result, err := handler.DeleteMyImage(reqIID) + if err != nil { + cblogger.Infof("[%s] MyImage Delete Failed : [%v]", reqKey, err) + } else { + cblogger.Infof("[%s] MyImage Delete Result : [%v]", reqKey, result) + } + } + } + } +} + // handlerType : resources폴더의 xxxHandler.go에서 Handler이전까지의 문자열 // (예) ImageHandler.go -> "Image" func getResourceHandler(handlerType string) (interface{}, error) { - var cloudDriver idrv.CloudDriver - cloudDriver = new(awsdrv.AwsDriver) + //var cloudDriver idrv.CloudDriver + //cloudDriver = new(awsdrv.AwsDriver) + cloudDriver := new(awsdrv.AwsDriver) config := readConfigFile() connectionInfo := idrv.ConnectionInfo{ @@ -1952,6 +2128,10 @@ func getResourceHandler(handlerType string) (interface{}, error) { resourceHandler, err = cloudConnection.CreatePriceInfoHandler() case "Tag": resourceHandler, err = cloudConnection.CreateTagHandler() + case "Disk": + resourceHandler, err = cloudConnection.CreateDiskHandler() + case "MyImage": + resourceHandler, err = cloudConnection.CreateMyImageHandler() } if err != nil { @@ -2077,9 +2257,11 @@ func readConfigFile() Config { } func main() { - // myimage / disk + handleMyImage() + // myimage //handleTag() // handlePublicIP() // PublicIP 생성 후 conf + // handleDisk() //handleKeyPair() //handleVPC() @@ -2089,8 +2271,8 @@ func main() { // handleVNic() //Lancard // handleVMSpec() //handleNLB() - handleCluster() + //handleCluster() //handleRegionZone() //handlePriceInfo() - //handleTag() + handleTag() } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go index 5c9151887..28b1beb85 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go @@ -141,15 +141,33 @@ func (tagHandler *AwsTagHandler) AddNLBTag(resIID irs.IID, tag irs.KeyValue) err return nil } +func (tagHandler *AwsTagHandler) getClusterArn(clusterName string) (string, error) { + input := &eks.DescribeClusterInput{ + Name: aws.String(clusterName), + } + + result, err := tagHandler.EKSClient.DescribeCluster(input) + if err != nil { + return "", fmt.Errorf("failed to describe EKS cluster: %w", err) + } + + return aws.StringValue(result.Cluster.Arn), nil +} + func (tagHandler *AwsTagHandler) AddClusterTag(resIID irs.IID, tag irs.KeyValue) error { + clusterArn, err := tagHandler.getClusterArn(resIID.SystemId) + if err != nil { + return err + } + input := &eks.TagResourceInput{ - ResourceArn: aws.String(resIID.SystemId), + ResourceArn: aws.String(clusterArn), Tags: map[string]*string{ tag.Key: aws.String(tag.Value), }, } - _, err := tagHandler.EKSClient.TagResource(input) + _, err = tagHandler.EKSClient.TagResource(input) if err != nil { return fmt.Errorf("failed to add tag to EKS cluster: %w", err) } @@ -413,7 +431,7 @@ func (tagHandler *AwsTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key } if len(tagList) == 0 { - return irs.KeyValue{}, nil + return irs.KeyValue{}, fmt.Errorf("%s tag key does not exist", key) } else { return tagList[0], nil } @@ -580,14 +598,19 @@ func (tagHandler *AwsTagHandler) RemoveNLBTag(resIID irs.IID, tagKey string) (bo } func (tagHandler *AwsTagHandler) RemoveClusterTag(resIID irs.IID, tagKey string) (bool, error) { + clusterArn, err := tagHandler.getClusterArn(resIID.SystemId) + if err != nil { + return false, err + } + input := &eks.UntagResourceInput{ - ResourceArn: aws.String(resIID.SystemId), + ResourceArn: aws.String(clusterArn), TagKeys: []*string{ aws.String(tagKey), }, } - _, err := tagHandler.EKSClient.UntagResource(input) + _, err = tagHandler.EKSClient.UntagResource(input) if err != nil { return false, fmt.Errorf("failed to remove tag from EKS cluster: %w", err) } From 640d95333e5c61bcbfe662625d818de06bf4bc71 Mon Sep 17 00:00:00 2001 From: handj622 Date: Thu, 25 Jul 2024 20:36:04 +0900 Subject: [PATCH 45/56] IBM: Implement TagHandler --- .../drivers/ibmcloud-vpc/IBMCloudDriver.go | 4 +- .../connect/Ibm_CloudConnection.go | 9 +- .../ibmcloud-vpc/main/Test_Resources.go | 130 ++++- .../ibmcloud-vpc/resources/TagHandler.go | 487 ++++++++++++++++++ 4 files changed, 623 insertions(+), 7 deletions(-) create mode 100644 cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/IBMCloudDriver.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/IBMCloudDriver.go index 32362e35a..391797c48 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/IBMCloudDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/IBMCloudDriver.go @@ -3,6 +3,8 @@ package ibmcloudvpc import ( "context" "errors" + "time" + "github.com/IBM/go-sdk-core/v5/core" "github.com/IBM/platform-services-go-sdk/globaltaggingv1" vpc0230 "github.com/IBM/vpc-go-sdk/0.23.0/vpcv1" @@ -12,7 +14,6 @@ import ( "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/utils/kubernetesserviceapiv1" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" icon "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/connect" - "time" ) type IbmCloudDriver struct{} @@ -38,6 +39,7 @@ func (IbmCloudDriver) GetDriverCapability() idrv.DriverCapabilityInfo { drvCapabilityInfo.NLBHandler = true drvCapabilityInfo.RegionZoneHandler = true drvCapabilityInfo.PriceInfoHandler = true + drvCapabilityInfo.TagHandler = true return drvCapabilityInfo } diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go index 205a11115..ea980ba44 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go @@ -175,5 +175,12 @@ func (cloudConn *IbmCloudConnection) CreatePriceInfoHandler() (irs.PriceInfoHand } func (cloudConn *IbmCloudConnection) CreateTagHandler() (irs.TagHandler, error) { - return nil, errors.New("Ibm Driver: not implemented") + cblogger.Info("Ibm Cloud Driver: called CreateTagHandler()!") + TagHandler := ibmrs.IbmTagHandler{ + CredentialInfo: cloudConn.CredentialInfo, + Region: cloudConn.Region, + VpcService: cloudConn.VpcService, + Ctx: cloudConn.Ctx, + } + return &TagHandler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/Test_Resources.go index ecf1aea16..1300455e4 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/Test_Resources.go @@ -4,6 +4,11 @@ import ( "bufio" "errors" "fmt" + "io/ioutil" + "os" + "strconv" + "strings" + cblog "github.com/cloud-barista/cb-log" ibm "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" @@ -11,10 +16,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" - "io/ioutil" - "os" - "strconv" - "strings" ) type Config struct { @@ -256,7 +257,8 @@ func showTestHandlerInfo() { cblogger.Info("10. RegionZoneHandler") cblogger.Info("11. PriceInfoHandler") cblogger.Info("12. ClusterHandler") - cblogger.Info("13. Exit") + cblogger.Info("13. TagHandler") + cblogger.Info("14. Exit") cblogger.Info("==========================================================") } @@ -307,8 +309,11 @@ func getResourceHandler(resourceType string, config Config) (interface{}, error) resourceHandler, err = ibmCon.CreatePriceInfoHandler() case "cluster": resourceHandler, err = ibmCon.CreateClusterHandler() + case "tag": + resourceHandler, err = ibmCon.CreateTagHandler() } + return resourceHandler, nil } func testImageHandlerListPrint() { @@ -1716,6 +1721,118 @@ Loop: } } +func testTagHandlerListPrint() { + cblogger.Info("Test TagHandler") + cblogger.Info("0. Menu") + cblogger.Info("1. ListTag()") + cblogger.Info("2. GetTag()") + cblogger.Info("3. FindTag()") + cblogger.Info("4. AddTag()") + cblogger.Info("5. RemoveTag()") + cblogger.Info("6. Exit") +} + +func testTagHandler(config Config) { + resourceHandler, err := getResourceHandler("tag", config) + if err != nil { + cblogger.Error(err) + return + } + tagHandler := resourceHandler.(irs.TagHandler) + + testTagHandlerListPrint() +Loop: + for { + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + cblogger.Error(err) + } + + if inputCnt == 1 { + switch commandNum { + case 0: + testTagHandlerListPrint() + case 1: + cblogger.Info("Start ListTag() ...") + if listTag, err := tagHandler.ListTag("VPC", irs.IID{NameId: "vpc-01-coadt9u8iqburq79175g",SystemId: "r006-6b9522df-6a21-47f3-8755-d8c217868367"}); err != nil { + cblogger.Error(err) + } else { + spew.Dump(listTag) + } + cblogger.Info("Finish listTag()") + case 2: + cblogger.Info("Start GetTag() ...") + var getTagName string + fmt.Print("Enter Get TagName: ") + if _, err := fmt.Scanln(&getTagName); err != nil { + cblogger.Error(err) + } + + if listTag, err := tagHandler.GetTag("VPC", irs.IID{NameId: "vpc-01-coadt9u8iqburq79175g",SystemId: "r006-6b9522df-6a21-47f3-8755-d8c217868367"}, getTagName); err != nil { + cblogger.Error(err) + } else { + spew.Dump(listTag) + } + cblogger.Info("Finish GetTag()") + case 3: + cblogger.Info("Start FindTag() ...") + var keyword string + fmt.Print("Enter keyword: ") + if _, err := fmt.Scanln(&keyword); err != nil { + cblogger.Error(err) + } + if listTag, err := tagHandler.FindTag("VPC", keyword); err != nil { + cblogger.Error(err) + } else { + spew.Dump(listTag) + } + cblogger.Info("Finish FindTag()") + case 4: + cblogger.Info("Start AddTag() ...") + var addKey string + var addValue string + + fmt.Print("Enter Add Key: ") + if _, err := fmt.Scanln(&addKey); err != nil { + cblogger.Error(err) + } + + fmt.Print("Enter Add Value: ") + if _, err := fmt.Scanln(&addValue); err != nil { + cblogger.Error(err) + } + + if listTag, err := tagHandler.AddTag("SecurityGroup", irs.IID{NameId: "sg01-coadudu8iqburq79176g",SystemId: "r006-a2fab93c-7964-4585-8900-9e13fbec4048"},irs.KeyValue{Key: addKey,Value: addValue}); err != nil { + cblogger.Error(err) + } else { + spew.Dump(listTag) + } + cblogger.Info("Finish AddTag()") + case 5: + cblogger.Info("Start RemoveTag() ...") + var removeKey string + fmt.Print("Enter Remove Key: ") + if _, err := fmt.Scanln(&removeKey); err != nil { + cblogger.Error(err) + } + + removeTagStatus, err := tagHandler.RemoveTag("VPC", irs.IID{NameId: "vpc-01-coadt9u8iqburq79175g",SystemId: "r006-6b9522df-6a21-47f3-8755-d8c217868367"}, removeKey) + if err != nil { + cblogger.Error(err) + } else { + spew.Dump(removeTagStatus) + } + cblogger.Info("Finish RemoveTag()") + case 6: + cblogger.Info("Exit") + break Loop + } + } + } +} + + func main() { showTestHandlerInfo() config := readConfigFile() @@ -1768,6 +1885,9 @@ Loop: testClusterHandler(config) showTestHandlerInfo() case 13: + testTagHandler(config) + showTestHandlerInfo() + case 14: cblogger.Info("Exit Test ResourceHandler Program") break Loop } diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go new file mode 100644 index 000000000..22a8d4efc --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go @@ -0,0 +1,487 @@ +package resources + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + + "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/platform-services-go-sdk/globalsearchv2" + globalTaggingService "github.com/IBM/platform-services-go-sdk/globaltaggingv1" + "github.com/IBM/vpc-go-sdk/vpcv1" + idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" + irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" +) + +type IbmTagHandler struct { + Region idrv.RegionInfo + CredentialInfo idrv.CredentialInfo + VpcService *vpcv1.VpcV1 + Ctx context.Context +} + +type TagList struct { + TotalCount *int64 `json:"total_count"` + Offset *int64 `json:"offset"` + Limit *int64 `json:"limit"` + Items []Tag `json:"items"` +} + +type Tag struct { + Name *string `json:"name" validate:"required"` +} + +type TagResults struct { + Results []TagResultsItem `json:"results"` +} + +type TagResultsItem struct { + ResourceID *string `json:"resource_id" validate:"required"` + IsError *bool `json:"is_error"` +} + +type Item struct { + CRN string + Name string + Tags []interface{} + Type string +} + +// ListTag implements resources.TagHandler. +func (tagHandler *IbmTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]irs.KeyValue, error) { + // IBM Cloud API Key 설정 + authenticator := &core.IamAuthenticator{ + ApiKey: tagHandler.CredentialInfo.ApiKey, + } + + // GlobalTaggingV1 인스턴스 생성 + service, err := globalTaggingService.NewGlobalTaggingV1(&globalTaggingService.GlobalTaggingV1Options{ + Authenticator: authenticator, + }) + if err != nil { + log.Fatalf("Failed to create service: %v", err) + } + + // ListTagsOptions 생성 + listTagsOptions := service.NewListTagsOptions() + listTagsOptions.SetTagType("user") + // listTagsOptions.SetAttachedOnly(false) // 리소스에 연결된 태그만 가져오기 + listTagsOptions.SetProviders([]string{"ghost"}) + listTagsOptions.SetOrderByName("asc") + + // ListTags 호출 + tagList, response, err := service.ListTags(listTagsOptions) + if err != nil { + panic(err) + } + + // 응답 디버깅 + if response != nil { + fmt.Printf("Response status code: %d\n", response.StatusCode) + } + + // 태그 데이터 파싱 + var tags []irs.KeyValue + + for _, tag := range tagList.Items { + tags = append(tags, irs.KeyValue{ + Key: *tag.Name, + Value: "", + }) + } + + // 디버그 출력 + b, _ := json.MarshalIndent(tagList, "", " ") + fmt.Println(string(b)) + + return tags, nil +} + +// GetTag implements resources.TagHandler. +func (tagHandler *IbmTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (irs.KeyValue, error) { + // IBM Cloud API Key 설정 + authenticator := &core.IamAuthenticator{ + ApiKey: tagHandler.CredentialInfo.ApiKey, + } + + // Create Global Search service instance + options := &globalsearchv2.GlobalSearchV2Options{ + Authenticator: authenticator, + } + service, err := globalsearchv2.NewGlobalSearchV2(options) + if err != nil { + fmt.Println("Error creating Global Search service:", err) + return irs.KeyValue{}, err + } + + // Define search options + searchOptions := service.NewSearchOptions() + // 검색 쿼리를 설정합니다. + resourceType := resType + resourceName := resIID.NameId + query := fmt.Sprintf("type:%s AND name:%s", resourceType, resourceName) + // searchQuery := resIID.SystemId + searchOptions.SetQuery(query) + searchOptions.SetFields([]string{"name","type","crn","tags"}) + searchOptions.SetLimit(100) + + // Call the service + scanResult, response, err := service.Search(searchOptions) + if err != nil { + fmt.Printf("Error searching for resources: %s\n", err.Error()) + if response != nil { + fmt.Printf("Response status code: %d\n", response.StatusCode) + fmt.Printf("Response headers: %v\n", response.Headers) + fmt.Printf("Response result: %v\n", response.Result) + } + return irs.KeyValue{}, err + } + + var result irs.KeyValue + // 결과 필터링 + for _, item := range scanResult.Items { + tags, ok := item.GetProperty("tags").([]interface{}) + if !ok { + fmt.Println("Error: Tags are not in expected format") + continue + } + for _, tag := range tags { + tagStr, ok := tag.(string) + if !ok { + fmt.Println("Error: Tag is not a string") + continue + } + + parts := strings.SplitN(tagStr, ":", 2) + if parts[0] == key { + result = irs.KeyValue{Key: parts[0], Value: parts[1]} + break + } + } + } + + // 응답 디버깅 + if response != nil { + fmt.Printf("Response status code: %d\n", response.StatusCode) + } + + return result, err +} + + +// FindTag implements resources.TagHandler. +func (tagHandler *IbmTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + // IBM Cloud API Key 설정 + authenticator := &core.IamAuthenticator{ + ApiKey: tagHandler.CredentialInfo.ApiKey, + } + + // Create Global Search service instance + options := &globalsearchv2.GlobalSearchV2Options{ + Authenticator: authenticator, + } + service, err := globalsearchv2.NewGlobalSearchV2(options) + if err != nil { + fmt.Println("Error creating Global Search service:", err) + return nil, err + } + + // Define search options + searchOptions := service.NewSearchOptions() + // 검색 쿼리를 설정합니다. + resourceType := resType + query := fmt.Sprintf("type:%s", resourceType) + // searchQuery := resIID.SystemId + searchOptions.SetQuery(query) + searchOptions.SetFields([]string{"name","resource_id","type","crn","tags"}) + searchOptions.SetLimit(100) + + // Call the service + scanResult, response, err := service.Search(searchOptions) + if err != nil { + fmt.Printf("Error searching for resources: %s\n", err.Error()) + if response != nil { + fmt.Printf("Response status code: %d\n", response.StatusCode) + fmt.Printf("Response headers: %v\n", response.Headers) + fmt.Printf("Response result: %v\n", response.Result) + } + return nil, err + } + + var result []*irs.TagInfo + // 결과 필터링 + for _, item := range scanResult.Items { + matchedTag := []irs.KeyValue{} + tags, ok := item.GetProperty("tags").([]interface{}) + if !ok { + fmt.Println("Error: Tags are not in expected format") + continue + } + for _, tag := range tags { + tagStr, ok := tag.(string) + if !ok { + fmt.Println("Error: Tag is not a string") + continue + } + parts := strings.SplitN(tagStr, ":", 2) + Key := parts[0] + Value := parts[1] + + if Key == keyword || Value == keyword { + matchedTag = append(matchedTag, irs.KeyValue{Key: Key, Value: Value}) + } + } + if len(matchedTag) > 0 { + item.SetProperty("tags", matchedTag) + result = append(result, &irs.TagInfo{ + ResType: resType, + ResIId: irs.IID{NameId: (item.GetProperty("name")).(string),SystemId: (item.GetProperty("resource_id").(string))}, + TagList: matchedTag, + KeyValueList: matchedTag, + }) + } + } + + + + b, _ := json.MarshalIndent(result, "", " ") + fmt.Println(string(b)) + + // 응답 디버깅 + if response != nil { + fmt.Printf("Response status code: %d\n", response.StatusCode) + } + + return result, err +} + +// AddTag implements resources.TagHandler. +func (tagHandler *IbmTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + switch resType { + case "VPC": + vpc, err := GetRawVPC(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *vpc.CRN) + case "Subnet": + vpc, err := GetRawVPC(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + + subnet, err := getVPCRawSubnet(vpc, resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *subnet.CRN) + case "SecurityGroup": + securityGroup, err := getRawSecurityGroup(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + fmt.Println(securityGroup) + AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *securityGroup.CRN) + case "VMKeyPair": + vmKeyPair, err := getRawKey(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *vmKeyPair.CRN) + case "VM": + vm, err := getRawInstance(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *vm.CRN) + case "Disk": + disk, err := getRawVolume(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *disk.CRN) + // case "MyImage": + // imageHandler := &IbmMyImageHandler{} + // rawMyimage, err := imageHandler.GetMyImage(resIID) + // if err != nil { + // getErr := errors.New(fmt.Sprintf("Failed to Get NLB. err = %s", err.Error())) + // cblogger.Error(getErr.Error()) + // } + // AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *rawMyimage.CRN) + case "NLB": + nlbHandler := &IbmNLBHandler{} // or however you initialize IbmNLBHandler + rawNLB, err := nlbHandler.getRawNLBByName(resIID.NameId) + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to Get NLB. err = %s", err.Error())) + cblogger.Error(getErr.Error()) + } + AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *rawNLB.CRN) + } + + return irs.KeyValue{Key: tag.Key, Value: tag.Value}, nil +} + +// RemoveTag implements resources.TagHandler. +func (tagHandler *IbmTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, tagName string) (bool, error) { + switch resType { + case "VPC": + vpc, err := GetRawVPC(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *vpc.CRN) + case "Subnet": + vpc, err := GetRawVPC(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + + subnet, err := getVPCRawSubnet(vpc, resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *subnet.CRN) + case "SecurityGroup": + securityGroup, err := getRawSecurityGroup(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *securityGroup.CRN) + case "VMKeyPair": + vmKeyPair, err := getRawKey(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *vmKeyPair.CRN) + case "VM": + vm, err := getRawInstance(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *vm.CRN) + case "Disk": + disk, err := getRawVolume(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil{ + log.Fatalf("Failed to Attach Tag: %v", err) + } + DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *disk.CRN) + // case "MyImage": + // myImageHandler := &IbmMyImageHandler{} + // rawMyimage, err := myImageHandler.GetMyImage(resIID) + // if err != nil { + // getErr := errors.New(fmt.Sprintf("Failed to Get NLB. err = %s", err.Error())) + // cblogger.Error(getErr.Error()) + // } + // DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *rawMyimage.CRN) + case "NLB": + nlbHandler := &IbmNLBHandler{} // or however you initialize IbmNLBHandler + rawNLB, err := nlbHandler.getRawNLBByName(resIID.NameId) + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to Get NLB. err = %s", err.Error())) + cblogger.Error(getErr.Error()) + } + DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *rawNLB.CRN) + } + + return true, nil +} + +func AttachTag (apikey string, resIID irs.IID, tag irs.KeyValue, CRN string){ + // IBM Cloud API Key 설정 + authenticator := &core.IamAuthenticator{ + ApiKey: apikey, + } + + // GlobalTaggingV1 인스턴스 생성 + service, err := globalTaggingService.NewGlobalTaggingV1(&globalTaggingService.GlobalTaggingV1Options{ + Authenticator: authenticator, + }) + if err != nil { + fmt.Errorf("failed to create service: %w", err) + } + + resourceModel := globalTaggingService.Resource{ + ResourceID: &CRN, + } + + attachTagOptions := service.NewAttachTagOptions( + []globalTaggingService.Resource{resourceModel}, + ) + + tagName := "" + if tag.Value == "" { + tagName = tag.Key + } else { + tagName = tag.Key + ":" + tag.Value + } + + attachTagOptions.SetTagNames([]string{tagName}) + attachTagOptions.SetTagType("user") + + tagResults, response, err := service.AttachTag(attachTagOptions) + if err != nil { + } + + // 응답 디버깅 + if response != nil { + fmt.Printf("Response status code: %d\n", response.StatusCode) + } + + b, _ := json.MarshalIndent(tagResults, "", " ") + fmt.Println(string(b)) +} + +func DetachTag (apikey string, resIID irs.IID, tagName string, CRN string){ + // IBM Cloud API Key 설정 + authenticator := &core.IamAuthenticator{ + ApiKey: apikey, + } + + // GlobalTaggingV1 인스턴스 생성 + service, err := globalTaggingService.NewGlobalTaggingV1(&globalTaggingService.GlobalTaggingV1Options{ + Authenticator: authenticator, + }) + if err != nil { + fmt.Errorf("failed to delete service: %w", err) + } + + // Detach 진행 + resourceCRN := CRN + resourceModel := globalTaggingService.Resource{ + ResourceID: &resourceCRN, + } + + detachTagOptions := service.NewDetachTagOptions( + []globalTaggingService.Resource{resourceModel}, + ) + + detachTagOptions.SetTagNames([]string{tagName}) + detachTagOptions.SetTagType("user") + + detachTagResults, response, err := service.DetachTag(detachTagOptions) + if err != nil { + log.Fatalf("Failed to delete service: %v", err) + } + b, _ := json.MarshalIndent(detachTagResults, "", " ") + fmt.Println(string(b)) + + // Delete 진행 + deleteTagOptions := service.NewDeleteTagOptions(tagName) + deleteTagOptions.SetTagType("user") + + deleteTagResults, response, err := service.DeleteTag(deleteTagOptions) + if err != nil { + log.Fatalf("Failed to delete service: %v", err) + } + + b, _ = json.MarshalIndent(deleteTagResults, "", " ") + fmt.Println(string(b)) + + // 응답 디버깅 + if response != nil { + fmt.Printf("Response status code: %d\n", response.StatusCode) + } +} \ No newline at end of file From 28b40c8b44c56a85c21f2a6c93f1eb316355bdf2 Mon Sep 17 00:00:00 2001 From: ish Date: Thu, 25 Jul 2024 20:38:27 +0900 Subject: [PATCH 46/56] IBM: Fix TagHandler --- .../drivers/ibmcloud-vpc/IBMCloudDriver.go | 10 + .../connect/Ibm_CloudConnection.go | 4 + .../ibmcloud-vpc/main/Test_Resources.go | 226 ++++- .../ibmcloud-vpc/resources/TagHandler.go | 775 +++++++++--------- .../ibmcloud-vpc/resources/VPCHandler.go | 44 + 5 files changed, 650 insertions(+), 409 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/IBMCloudDriver.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/IBMCloudDriver.go index 391797c48..aabd510d9 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/IBMCloudDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/IBMCloudDriver.go @@ -3,6 +3,7 @@ package ibmcloudvpc import ( "context" "errors" + "github.com/IBM/platform-services-go-sdk/globalsearchv2" "time" "github.com/IBM/go-sdk-core/v5/core" @@ -111,6 +112,14 @@ func (driver *IbmCloudDriver) ConnectCloud(connectionInfo idrv.ConnectionInfo) ( if err != nil { return nil, err } + searchService, err := globalsearchv2.NewGlobalSearchV2(&globalsearchv2.GlobalSearchV2Options{ + Authenticator: &core.IamAuthenticator{ + ApiKey: connectionInfo.CredentialInfo.ApiKey, + }, + }) + if err != nil { + return nil, err + } iConn := connect.IbmCloudConnection{ CredentialInfo: connectionInfo.CredentialInfo, @@ -119,6 +128,7 @@ func (driver *IbmCloudDriver) ConnectCloud(connectionInfo idrv.ConnectionInfo) ( VpcService0230: vpcService0230, ClusterService: clusterService, TaggingService: taggingService, + SearchService: searchService, Ctx: ctx, } return &iConn, nil diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go index ea980ba44..959c8b8af 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go @@ -3,6 +3,7 @@ package connect import ( "context" "errors" + "github.com/IBM/platform-services-go-sdk/globalsearchv2" "github.com/IBM/platform-services-go-sdk/globaltaggingv1" vpcv0230 "github.com/IBM/vpc-go-sdk/0.23.0/vpcv1" @@ -28,6 +29,7 @@ type IbmCloudConnection struct { VpcService *vpcv1.VpcV1 ClusterService *kubernetesserviceapiv1.KubernetesServiceApiV1 TaggingService *globaltaggingv1.GlobalTaggingV1 + SearchService *globalsearchv2.GlobalSearchV2 VpcService0230 *vpcv0230.VpcV1 Ctx context.Context } @@ -181,6 +183,8 @@ func (cloudConn *IbmCloudConnection) CreateTagHandler() (irs.TagHandler, error) Region: cloudConn.Region, VpcService: cloudConn.VpcService, Ctx: cloudConn.Ctx, + TaggingService: cloudConn.TaggingService, + SearchService: cloudConn.SearchService, } return &TagHandler, nil } diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/Test_Resources.go index 1300455e4..05e4e1cb6 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/Test_Resources.go @@ -313,7 +313,6 @@ func getResourceHandler(resourceType string, config Config) (interface{}, error) resourceHandler, err = ibmCon.CreateTagHandler() } - return resourceHandler, nil } func testImageHandlerListPrint() { @@ -1723,15 +1722,46 @@ Loop: func testTagHandlerListPrint() { cblogger.Info("Test TagHandler") - cblogger.Info("0. Menu") - cblogger.Info("1. ListTag()") - cblogger.Info("2. GetTag()") - cblogger.Info("3. FindTag()") - cblogger.Info("4. AddTag()") - cblogger.Info("5. RemoveTag()") + cblogger.Info("0. Print Menu") + cblogger.Info("1. AddTag()") + cblogger.Info("2. ListTag()") + cblogger.Info("3. GetTag()") + cblogger.Info("4. RemoveTag()") + cblogger.Info("5. FindTag()") cblogger.Info("6. Exit") } +func inputToRSType(input string) irs.RSType { + switch input { + case "all": + return irs.ALL + case "image": + return irs.IMAGE + case "vpc": + return irs.VPC + case "subnet": + return irs.SUBNET + case "sg": + return irs.SG + case "keypair": + return irs.KEY + case "vm": + return irs.VM + case "nlb": + return irs.NLB + case "disk": + return irs.DISK + case "myimage": + return irs.MYIMAGE + case "cluster": + return irs.CLUSTER + case "nodegroup": + return irs.NODEGROUP + default: + return "" + } +} + func testTagHandler(config Config) { resourceHandler, err := getResourceHandler("tag", config) if err != nil { @@ -1754,76 +1784,187 @@ Loop: case 0: testTagHandlerListPrint() case 1: - cblogger.Info("Start ListTag() ...") - if listTag, err := tagHandler.ListTag("VPC", irs.IID{NameId: "vpc-01-coadt9u8iqburq79175g",SystemId: "r006-6b9522df-6a21-47f3-8755-d8c217868367"}); err != nil { + cblogger.Info("Start AddTag() ...") + fmt.Println("=== Enter resource type ===") + in := bufio.NewReader(os.Stdin) + resTypeStr, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + resTypeStr = strings.TrimSpace(resTypeStr) + result := irs.RSTypeString(irs.RSType(resTypeStr)) + if strings.Contains(result, "not supported") { + cblogger.Error(result) + break Loop + } + resType := inputToRSType(resTypeStr) + + fmt.Println("=== Enter name of the resource ===") + in = bufio.NewReader(os.Stdin) + resName, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + resName = strings.TrimSpace(resName) + + fmt.Println("=== Enter tag's key ===") + in = bufio.NewReader(os.Stdin) + tagKey, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + tagKey = strings.TrimSpace(tagKey) + + fmt.Println("=== Enter tag's value ===") + in = bufio.NewReader(os.Stdin) + tagValue, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + tagValue = strings.TrimSpace(tagValue) + + if tagKeyValue, err := tagHandler.AddTag(resType, irs.IID{NameId: resName}, irs.KeyValue{Key: tagKey, Value: tagValue}); err != nil { cblogger.Error(err) } else { - spew.Dump(listTag) + spew.Dump(tagKeyValue) } - cblogger.Info("Finish listTag()") + cblogger.Info("Finish AddTag()") case 2: - cblogger.Info("Start GetTag() ...") - var getTagName string - fmt.Print("Enter Get TagName: ") - if _, err := fmt.Scanln(&getTagName); err != nil { + cblogger.Info("Start ListTag() ...") + fmt.Println("=== Enter resource type ===") + in := bufio.NewReader(os.Stdin) + resTypeStr, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + resTypeStr = strings.TrimSpace(resTypeStr) + result := irs.RSTypeString(irs.RSType(resTypeStr)) + if strings.Contains(result, "not supported") { + cblogger.Error(result) + break Loop + } + resType := inputToRSType(resTypeStr) + + fmt.Println("=== Enter name of the resource ===") + in = bufio.NewReader(os.Stdin) + resName, err := in.ReadString('\n') + if err != nil { cblogger.Error(err) } + resName = strings.TrimSpace(resName) - if listTag, err := tagHandler.GetTag("VPC", irs.IID{NameId: "vpc-01-coadt9u8iqburq79175g",SystemId: "r006-6b9522df-6a21-47f3-8755-d8c217868367"}, getTagName); err != nil { + if tagList, err := tagHandler.ListTag(resType, irs.IID{NameId: resName}); err != nil { cblogger.Error(err) } else { - spew.Dump(listTag) + spew.Dump(tagList) } - cblogger.Info("Finish GetTag()") + cblogger.Info("Finish ListTag()") case 3: - cblogger.Info("Start FindTag() ...") - var keyword string - fmt.Print("Enter keyword: ") - if _, err := fmt.Scanln(&keyword); err != nil { + cblogger.Info("Start GetTag() ...") + fmt.Println("=== Enter resource type ===") + in := bufio.NewReader(os.Stdin) + resTypeStr, err := in.ReadString('\n') + if err != nil { cblogger.Error(err) } - if listTag, err := tagHandler.FindTag("VPC", keyword); err != nil { + resTypeStr = strings.TrimSpace(resTypeStr) + result := irs.RSTypeString(irs.RSType(resTypeStr)) + if strings.Contains(result, "not supported") { + cblogger.Error(result) + break Loop + } + resType := inputToRSType(resTypeStr) + + fmt.Println("=== Enter name of the resource ===") + in = bufio.NewReader(os.Stdin) + resName, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + resName = strings.TrimSpace(resName) + + fmt.Println("=== Enter tag's key ===") + in = bufio.NewReader(os.Stdin) + tagKey, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + tagKey = strings.TrimSpace(tagKey) + + if tagKeyValue, err := tagHandler.GetTag(resType, irs.IID{NameId: resName}, tagKey); err != nil { cblogger.Error(err) } else { - spew.Dump(listTag) + spew.Dump(tagKeyValue) } - cblogger.Info("Finish FindTag()") + cblogger.Info("Finish GetTag()") case 4: - cblogger.Info("Start AddTag() ...") - var addKey string - var addValue string + cblogger.Info("Start RemoveTag() ...") + fmt.Println("=== Enter resource type ===") + in := bufio.NewReader(os.Stdin) + resTypeStr, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + resTypeStr = strings.TrimSpace(resTypeStr) + result := irs.RSTypeString(irs.RSType(resTypeStr)) + if strings.Contains(result, "not supported") { + cblogger.Error(result) + break Loop + } + resType := inputToRSType(resTypeStr) - fmt.Print("Enter Add Key: ") - if _, err := fmt.Scanln(&addKey); err != nil { + fmt.Println("=== Enter name of the resource ===") + in = bufio.NewReader(os.Stdin) + resName, err := in.ReadString('\n') + if err != nil { cblogger.Error(err) } + resName = strings.TrimSpace(resName) - fmt.Print("Enter Add Value: ") - if _, err := fmt.Scanln(&addValue); err != nil { + fmt.Println("=== Enter tag's key ===") + in = bufio.NewReader(os.Stdin) + tagKey, err := in.ReadString('\n') + if err != nil { cblogger.Error(err) } + tagKey = strings.TrimSpace(tagKey) - if listTag, err := tagHandler.AddTag("SecurityGroup", irs.IID{NameId: "sg01-coadudu8iqburq79176g",SystemId: "r006-a2fab93c-7964-4585-8900-9e13fbec4048"},irs.KeyValue{Key: addKey,Value: addValue}); err != nil { + if tagKeyValue, err := tagHandler.RemoveTag(resType, irs.IID{NameId: resName}, tagKey); err != nil { cblogger.Error(err) } else { - spew.Dump(listTag) + spew.Dump(tagKeyValue) } - cblogger.Info("Finish AddTag()") + cblogger.Info("Finish RemoveTag()") case 5: - cblogger.Info("Start RemoveTag() ...") - var removeKey string - fmt.Print("Enter Remove Key: ") - if _, err := fmt.Scanln(&removeKey); err != nil { + cblogger.Info("Start FindTag() ...") + fmt.Println("=== Enter resource type ===") + in := bufio.NewReader(os.Stdin) + resTypeStr, err := in.ReadString('\n') + if err != nil { cblogger.Error(err) } + resTypeStr = strings.TrimSpace(resTypeStr) + result := irs.RSTypeString(irs.RSType(resTypeStr)) + if strings.Contains(result, "not supported") { + cblogger.Error(result) + break Loop + } + resType := inputToRSType(resTypeStr) - removeTagStatus, err := tagHandler.RemoveTag("VPC", irs.IID{NameId: "vpc-01-coadt9u8iqburq79175g",SystemId: "r006-6b9522df-6a21-47f3-8755-d8c217868367"}, removeKey) + fmt.Println("=== Enter keyword ===") + in = bufio.NewReader(os.Stdin) + keyword, err := in.ReadString('\n') if err != nil { cblogger.Error(err) + } + keyword = strings.TrimSpace(keyword) + + if tagKeyValue, err := tagHandler.FindTag(resType, keyword); err != nil { + cblogger.Error(err) } else { - spew.Dump(removeTagStatus) + spew.Dump(tagKeyValue) } - cblogger.Info("Finish RemoveTag()") + cblogger.Info("Finish FindTag()") case 6: cblogger.Info("Exit") break Loop @@ -1832,7 +1973,6 @@ Loop: } } - func main() { showTestHandlerInfo() config := readConfigFile() diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go index 22a8d4efc..c74ca800c 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go @@ -2,25 +2,25 @@ package resources import ( "context" - "encoding/json" "errors" "fmt" - "log" + call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" "strings" - "github.com/IBM/go-sdk-core/v5/core" "github.com/IBM/platform-services-go-sdk/globalsearchv2" - globalTaggingService "github.com/IBM/platform-services-go-sdk/globaltaggingv1" + "github.com/IBM/platform-services-go-sdk/globaltaggingv1" "github.com/IBM/vpc-go-sdk/vpcv1" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" ) type IbmTagHandler struct { - Region idrv.RegionInfo - CredentialInfo idrv.CredentialInfo - VpcService *vpcv1.VpcV1 - Ctx context.Context + Region idrv.RegionInfo + CredentialInfo idrv.CredentialInfo + VpcService *vpcv1.VpcV1 + Ctx context.Context + TaggingService *globaltaggingv1.GlobalTaggingV1 + SearchService *globalsearchv2.GlobalSearchV2 } type TagList struct { @@ -44,444 +44,487 @@ type TagResultsItem struct { } type Item struct { - CRN string - Name string - Tags []interface{} - Type string + CRN string + Name string + Tags []interface{} + Type string } -// ListTag implements resources.TagHandler. -func (tagHandler *IbmTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]irs.KeyValue, error) { - // IBM Cloud API Key 설정 - authenticator := &core.IamAuthenticator{ - ApiKey: tagHandler.CredentialInfo.ApiKey, - } - - // GlobalTaggingV1 인스턴스 생성 - service, err := globalTaggingService.NewGlobalTaggingV1(&globalTaggingService.GlobalTaggingV1Options{ - Authenticator: authenticator, - }) - if err != nil { - log.Fatalf("Failed to create service: %v", err) - } - - // ListTagsOptions 생성 - listTagsOptions := service.NewListTagsOptions() - listTagsOptions.SetTagType("user") - // listTagsOptions.SetAttachedOnly(false) // 리소스에 연결된 태그만 가져오기 - listTagsOptions.SetProviders([]string{"ghost"}) - listTagsOptions.SetOrderByName("asc") - - // ListTags 호출 - tagList, response, err := service.ListTags(listTagsOptions) - if err != nil { - panic(err) - } - - // 응답 디버깅 - if response != nil { - fmt.Printf("Response status code: %d\n", response.StatusCode) - } - - // 태그 데이터 파싱 - var tags []irs.KeyValue - - for _, tag := range tagList.Items { - tags = append(tags, irs.KeyValue{ - Key: *tag.Name, - Value: "", - }) - } - - // 디버그 출력 - b, _ := json.MarshalIndent(tagList, "", " ") - fmt.Println(string(b)) - - return tags, nil +func rsTypeToIBMType(resType irs.RSType) string { + switch resType { + case irs.ALL: + return "*" + case irs.IMAGE: + return "image" + case irs.VPC: + return "vpc" + case irs.SUBNET: + return "subnet" + case irs.SG: + return "security-group" + case irs.KEY: + return "key" + case irs.VM: + return "instance" + case irs.NLB: + return "load-balancer" + case irs.DISK: + return "volume" + case irs.MYIMAGE: + return "snapshot" + case irs.CLUSTER: + return "" + case irs.NODEGROUP: + return "instance-group" + default: + return "" + } } -// GetTag implements resources.TagHandler. -func (tagHandler *IbmTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (irs.KeyValue, error) { - // IBM Cloud API Key 설정 - authenticator := &core.IamAuthenticator{ - ApiKey: tagHandler.CredentialInfo.ApiKey, +func ibmTypeToRSType(ibmType string) (irs.RSType, error) { + fmt.Println(ibmType) + + switch ibmType { + case "image": + return irs.IMAGE, nil + case "vpc": + return irs.VPC, nil + case "subnet": + return irs.SUBNET, nil + case "security-group": + return irs.SG, nil + case "key": + return irs.KEY, nil + case "instance": + return irs.VM, nil + case "load-balancer": + return irs.NLB, nil + case "volume": + return irs.DISK, nil + case "snapshot": + return irs.MYIMAGE, nil + //case "???": + // return irs.CLUSTER + case "instance-group": + return irs.NODEGROUP, nil + default: + return "", errors.New(fmt.Sprintf("unsupport type %s", ibmType)) } +} - // Create Global Search service instance - options := &globalsearchv2.GlobalSearchV2Options{ - Authenticator: authenticator, +func getTagFromResource(searchService *globalsearchv2.GlobalSearchV2, + resType irs.RSType, resIID irs.IID, key string) (irs.KeyValue, error) { + searchOptions := searchService.NewSearchOptions() + + var query string + + ibmType := rsTypeToIBMType(resType) + if ibmType == "" { + return irs.KeyValue{}, errors.New("invalid resource type") } - service, err := globalsearchv2.NewGlobalSearchV2(options) - if err != nil { - fmt.Println("Error creating Global Search service:", err) - return irs.KeyValue{}, err + + if resIID.NameId != "" { + query = fmt.Sprintf("type:%s AND name:%s", ibmType, resIID.NameId) + } else { + query = fmt.Sprintf("type:%s AND id:%s", ibmType, resIID.SystemId) } - // Define search options - searchOptions := service.NewSearchOptions() - // 검색 쿼리를 설정합니다. - resourceType := resType - resourceName := resIID.NameId - query := fmt.Sprintf("type:%s AND name:%s", resourceType, resourceName) - // searchQuery := resIID.SystemId searchOptions.SetQuery(query) - searchOptions.SetFields([]string{"name","type","crn","tags"}) + searchOptions.SearchCursor = nil + searchOptions.SetFields([]string{"name", "type", "crn", "tags"}) searchOptions.SetLimit(100) - // Call the service - scanResult, response, err := service.Search(searchOptions) - if err != nil { - fmt.Printf("Error searching for resources: %s\n", err.Error()) - if response != nil { - fmt.Printf("Response status code: %d\n", response.StatusCode) - fmt.Printf("Response headers: %v\n", response.Headers) - fmt.Printf("Response result: %v\n", response.Result) + for { + scanResult, _, err := searchService.Search(searchOptions) + if err != nil { + return irs.KeyValue{}, err + } + + if len(scanResult.Items) == 0 { + break } - return irs.KeyValue{}, err - } - var result irs.KeyValue - // 결과 필터링 - for _, item := range scanResult.Items { - tags, ok := item.GetProperty("tags").([]interface{}) - if !ok { - fmt.Println("Error: Tags are not in expected format") - continue - } - for _, tag := range tags { - tagStr, ok := tag.(string) - if !ok { - fmt.Println("Error: Tag is not a string") - continue - } - - parts := strings.SplitN(tagStr, ":", 2) - if parts[0] == key { - result = irs.KeyValue{Key: parts[0], Value: parts[1]} - break - } - } - } - - // 응답 디버깅 - if response != nil { - fmt.Printf("Response status code: %d\n", response.StatusCode) + searchOptions.SearchCursor = scanResult.SearchCursor + + for _, item := range scanResult.Items { + tags, ok := item.GetProperty("tags").([]interface{}) + if !ok { + cblogger.Error("Tags are not in expected format") + continue + } + for _, tag := range tags { + tagStr, ok := tag.(string) + if !ok { + cblogger.Errorf("Tag is not a string (%v)", tag) + continue + } + + parts := strings.SplitN(tagStr, ":", 2) + if parts[0] == key { + return irs.KeyValue{Key: parts[0], Value: parts[1]}, nil + } + } + } } - return result, err + return irs.KeyValue{}, errors.New("tag not found") } - -// FindTag implements resources.TagHandler. -func (tagHandler *IbmTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { - // IBM Cloud API Key 설정 - authenticator := &core.IamAuthenticator{ - ApiKey: tagHandler.CredentialInfo.ApiKey, +func attachOrDetachTag(tagService *globaltaggingv1.GlobalTaggingV1, tag irs.KeyValue, CRN string, action string) error { + resourceModel := globaltaggingv1.Resource{ + ResourceID: &CRN, } - // Create Global Search service instance - options := &globalsearchv2.GlobalSearchV2Options{ - Authenticator: authenticator, - } - service, err := globalsearchv2.NewGlobalSearchV2(options) - if err != nil { - fmt.Println("Error creating Global Search service:", err) - return nil, err + var tagName string + if tag.Value == "" { + tagName = tag.Key + } else { + tagName = tag.Key + ":" + tag.Value } - // Define search options - searchOptions := service.NewSearchOptions() - // 검색 쿼리를 설정합니다. - resourceType := resType - query := fmt.Sprintf("type:%s", resourceType) - // searchQuery := resIID.SystemId - searchOptions.SetQuery(query) - searchOptions.SetFields([]string{"name","resource_id","type","crn","tags"}) - searchOptions.SetLimit(100) + switch action { + case "add": + attachTagOptions := tagService.NewAttachTagOptions( + []globaltaggingv1.Resource{resourceModel}, + ) - // Call the service - scanResult, response, err := service.Search(searchOptions) - if err != nil { - fmt.Printf("Error searching for resources: %s\n", err.Error()) - if response != nil { - fmt.Printf("Response status code: %d\n", response.StatusCode) - fmt.Printf("Response headers: %v\n", response.Headers) - fmt.Printf("Response result: %v\n", response.Result) - } - return nil, err - } + attachTagOptions.SetTagNames([]string{tagName}) + attachTagOptions.SetTagType("user") - var result []*irs.TagInfo - // 결과 필터링 - for _, item := range scanResult.Items { - matchedTag := []irs.KeyValue{} - tags, ok := item.GetProperty("tags").([]interface{}) - if !ok { - fmt.Println("Error: Tags are not in expected format") - continue - } - for _, tag := range tags { - tagStr, ok := tag.(string) - if !ok { - fmt.Println("Error: Tag is not a string") - continue - } - parts := strings.SplitN(tagStr, ":", 2) - Key := parts[0] - Value := parts[1] - - if Key == keyword || Value == keyword { - matchedTag = append(matchedTag, irs.KeyValue{Key: Key, Value: Value}) - } - } - if len(matchedTag) > 0 { - item.SetProperty("tags", matchedTag) - result = append(result, &irs.TagInfo{ - ResType: resType, - ResIId: irs.IID{NameId: (item.GetProperty("name")).(string),SystemId: (item.GetProperty("resource_id").(string))}, - TagList: matchedTag, - KeyValueList: matchedTag, - }) + _, _, err := tagService.AttachTag(attachTagOptions) + if err != nil { + return err } - } + case "remove": + detachTagOptions := tagService.NewDetachTagOptions( + []globaltaggingv1.Resource{resourceModel}, + ) + detachTagOptions.SetTagNames([]string{tagName}) + detachTagOptions.SetTagType("user") + _, _, err := tagService.DetachTag(detachTagOptions) + if err != nil { + return err + } - b, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(b)) + deleteTagAllOptions := tagService.NewDeleteTagAllOptions() + deleteTagAllOptions.SetTagType("user") - // 응답 디버깅 - if response != nil { - fmt.Printf("Response status code: %d\n", response.StatusCode) + _, _, err = tagService.DeleteTagAll(deleteTagAllOptions) + if err != nil { + return err + } } - return result, err + return nil } -// AddTag implements resources.TagHandler. -func (tagHandler *IbmTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { +func handleTagAddOrRemove(tagHandler *IbmTagHandler, resType irs.RSType, resIID irs.IID, + tag irs.KeyValue, action string) error { + var err2 error + + if action == "remove" { + tag, err2 = getTagFromResource(tagHandler.SearchService, resType, resIID, tag.Key) + if err2 != nil { + return err2 + } + } + + ibmType := rsTypeToIBMType(resType) + if ibmType == "" { + return errors.New("invalid resource type") + } else if ibmType == "all" { + return errors.New("all is not supported for getting tag from the resource") + } + switch resType { - case "VPC": + case irs.VPC: vpc, err := GetRawVPC(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + if err != nil { + err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + break } - AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *vpc.CRN) - case "Subnet": - vpc, err := GetRawVPC(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *vpc.CRN, action) + case irs.SUBNET: + subnet, err := getRawSubnet(resIID, tagHandler.VpcService, tagHandler.Ctx) + if err != nil { + err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + break } - subnet, err := getVPCRawSubnet(vpc, resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) - } - AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *subnet.CRN) - case "SecurityGroup": + err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *subnet.CRN, action) + case irs.SG: securityGroup, err := getRawSecurityGroup(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + if err != nil { + err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + break } - fmt.Println(securityGroup) - AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *securityGroup.CRN) - case "VMKeyPair": + + err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *securityGroup.CRN, action) + case irs.KEY: vmKeyPair, err := getRawKey(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + if err != nil { + err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + break } - AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *vmKeyPair.CRN) - case "VM": + + err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *vmKeyPair.CRN, action) + case irs.VM: vm, err := getRawInstance(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + if err != nil { + err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + break } - AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *vm.CRN) - case "Disk": + + err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *vm.CRN, action) + case irs.DISK: disk, err := getRawVolume(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + if err != nil { + err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + break } - AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *disk.CRN) - // case "MyImage": + + err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *disk.CRN, action) + // case irs.MYIMAGE: // imageHandler := &IbmMyImageHandler{} // rawMyimage, err := imageHandler.GetMyImage(resIID) - // if err != nil { - // getErr := errors.New(fmt.Sprintf("Failed to Get NLB. err = %s", err.Error())) - // cblogger.Error(getErr.Error()) - // } - // AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *rawMyimage.CRN) - case "NLB": + // if err != nil { + // err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + // break + // } + // err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *rawMyimage.CRN, action + case irs.NLB: nlbHandler := &IbmNLBHandler{} // or however you initialize IbmNLBHandler - rawNLB, err := nlbHandler.getRawNLBByName(resIID.NameId) - if err != nil { - getErr := errors.New(fmt.Sprintf("Failed to Get NLB. err = %s", err.Error())) - cblogger.Error(getErr.Error()) - } - AttachTag(tagHandler.CredentialInfo.ApiKey, resIID, tag, *rawNLB.CRN) + rawNLB, err := nlbHandler.getRawNLBByName(resIID.NameId) + if err != nil { + err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + break + } + + err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *rawNLB.CRN, action) + default: + return errors.New("invalid resource type") } - return irs.KeyValue{Key: tag.Key, Value: tag.Value}, nil + return err2 } -// RemoveTag implements resources.TagHandler. -func (tagHandler *IbmTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, tagName string) (bool, error) { - switch resType { - case "VPC": - vpc, err := GetRawVPC(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) - } - DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *vpc.CRN) - case "Subnet": - vpc, err := GetRawVPC(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) - } +func (tagHandler *IbmTagHandler) AddTag(resType irs.RSType, resIID irs.IID, tag irs.KeyValue) (irs.KeyValue, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.NameId, "AddTag()") + start := call.Start() - subnet, err := getVPCRawSubnet(vpc, resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) - } - DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *subnet.CRN) - case "SecurityGroup": - securityGroup, err := getRawSecurityGroup(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) - } - DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *securityGroup.CRN) - case "VMKeyPair": - vmKeyPair, err := getRawKey(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + tagFound, _ := getTagFromResource(tagHandler.SearchService, resType, resIID, tag.Key) + if tagFound.Key == tag.Key { + return tagFound, errors.New("tag with provided key is already exists") + } + + err := handleTagAddOrRemove(tagHandler, resType, resIID, tag, "add") + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to add a tag. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return irs.KeyValue{}, getErr + } + + LoggingInfo(hiscallInfo, start) + + return irs.KeyValue{Key: tag.Key, Value: tag.Value}, nil +} + +func (tagHandler *IbmTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([]irs.KeyValue, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.NameId, "ListTag()") + start := call.Start() + + ibmType := rsTypeToIBMType(resType) + if ibmType == "" { + return []irs.KeyValue{}, errors.New("invalid resource type") + } + + searchOptions := tagHandler.SearchService.NewSearchOptions() + + var query string + + if resIID.NameId != "" { + query = fmt.Sprintf("type:%s AND name:%s", resType, resIID.NameId) + } else { + query = fmt.Sprintf("type:%s AND id:%s", resType, resIID.SystemId) + } + + searchOptions.SetQuery(query) + searchOptions.SearchCursor = nil + searchOptions.SetFields([]string{"name", "type", "crn", "tags"}) + searchOptions.SetLimit(100) + + var tagList []irs.KeyValue + + for { + scanResult, _, err := tagHandler.SearchService.Search(searchOptions) + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to list tag. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return tagList, err } - DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *vmKeyPair.CRN) - case "VM": - vm, err := getRawInstance(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + + if len(scanResult.Items) == 0 { + break } - DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *vm.CRN) - case "Disk": - disk, err := getRawVolume(resIID, tagHandler.VpcService, tagHandler.Ctx) - if err != nil{ - log.Fatalf("Failed to Attach Tag: %v", err) + + searchOptions.SearchCursor = scanResult.SearchCursor + + for _, item := range scanResult.Items { + tags, ok := item.GetProperty("tags").([]interface{}) + if !ok { + cblogger.Error("Tags are not in expected format") + continue + } + for _, tag := range tags { + tagStr, ok := tag.(string) + if !ok { + cblogger.Error("Tag is not a string") + continue + } + + parts := strings.SplitN(tagStr, ":", 2) + tagList = append(tagList, irs.KeyValue{ + Key: parts[0], + Value: parts[1], + }) + } } - DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *disk.CRN) - // case "MyImage": - // myImageHandler := &IbmMyImageHandler{} - // rawMyimage, err := myImageHandler.GetMyImage(resIID) - // if err != nil { - // getErr := errors.New(fmt.Sprintf("Failed to Get NLB. err = %s", err.Error())) - // cblogger.Error(getErr.Error()) - // } - // DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *rawMyimage.CRN) - case "NLB": - nlbHandler := &IbmNLBHandler{} // or however you initialize IbmNLBHandler - rawNLB, err := nlbHandler.getRawNLBByName(resIID.NameId) - if err != nil { - getErr := errors.New(fmt.Sprintf("Failed to Get NLB. err = %s", err.Error())) - cblogger.Error(getErr.Error()) - } - DetachTag(tagHandler.CredentialInfo.ApiKey, resIID, tagName, *rawNLB.CRN) } - return true, nil -} + LoggingInfo(hiscallInfo, start) -func AttachTag (apikey string, resIID irs.IID, tag irs.KeyValue, CRN string){ - // IBM Cloud API Key 설정 - authenticator := &core.IamAuthenticator{ - ApiKey: apikey, - } - - // GlobalTaggingV1 인스턴스 생성 - service, err := globalTaggingService.NewGlobalTaggingV1(&globalTaggingService.GlobalTaggingV1Options{ - Authenticator: authenticator, - }) - if err != nil { - fmt.Errorf("failed to create service: %w", err) - } - - resourceModel := globalTaggingService.Resource{ - ResourceID: &CRN, - } + return tagList, nil +} - attachTagOptions := service.NewAttachTagOptions( - []globalTaggingService.Resource{resourceModel}, - ) +func (tagHandler *IbmTagHandler) GetTag(resType irs.RSType, resIID irs.IID, key string) (irs.KeyValue, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.NameId, "GetTag()") + start := call.Start() - tagName := "" - if tag.Value == "" { - tagName = tag.Key - } else { - tagName = tag.Key + ":" + tag.Value + tag, err := getTagFromResource(tagHandler.SearchService, resType, resIID, key) + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to get tag. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return irs.KeyValue{}, err } - attachTagOptions.SetTagNames([]string{tagName}) - attachTagOptions.SetTagType("user") + LoggingInfo(hiscallInfo, start) - tagResults, response, err := service.AttachTag(attachTagOptions) + return tag, nil +} + +func (tagHandler *IbmTagHandler) RemoveTag(resType irs.RSType, resIID irs.IID, key string) (bool, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.NameId, "RemoveTag()") + start := call.Start() + + err := handleTagAddOrRemove(tagHandler, resType, resIID, irs.KeyValue{Key: key}, "remove") if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to remove a tag. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return false, getErr } - // 응답 디버깅 - if response != nil { - fmt.Printf("Response status code: %d\n", response.StatusCode) - } + LoggingInfo(hiscallInfo, start) - b, _ := json.MarshalIndent(tagResults, "", " ") - fmt.Println(string(b)) + return true, nil } -func DetachTag (apikey string, resIID irs.IID, tagName string, CRN string){ - // IBM Cloud API Key 설정 - authenticator := &core.IamAuthenticator{ - ApiKey: apikey, - } +func (tagHandler *IbmTagHandler) FindTag(resType irs.RSType, keyword string) ([]*irs.TagInfo, error) { + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, keyword, "FindTag()") + start := call.Start() - // GlobalTaggingV1 인스턴스 생성 - service, err := globalTaggingService.NewGlobalTaggingV1(&globalTaggingService.GlobalTaggingV1Options{ - Authenticator: authenticator, - }) - if err != nil { - fmt.Errorf("failed to delete service: %w", err) - } + ibmType := rsTypeToIBMType(resType) + if ibmType == "" { + return []*irs.TagInfo{}, errors.New("invalid resource type") + } - // Detach 진행 - resourceCRN := CRN - resourceModel := globalTaggingService.Resource{ - ResourceID: &resourceCRN, - } + searchOptions := tagHandler.SearchService.NewSearchOptions() - detachTagOptions := service.NewDetachTagOptions( - []globalTaggingService.Resource{resourceModel}, - ) + query := fmt.Sprintf("type:%s", ibmType) + searchOptions.SetQuery(query) + searchOptions.SearchCursor = nil + searchOptions.SetFields([]string{"name", "resource_id", "type", "crn", "tags"}) + searchOptions.SetLimit(100) - detachTagOptions.SetTagNames([]string{tagName}) - detachTagOptions.SetTagType("user") + var tagInfo []*irs.TagInfo - detachTagResults, response, err := service.DetachTag(detachTagOptions) + for { + scanResult, _, err := tagHandler.SearchService.Search(searchOptions) if err != nil { - log.Fatalf("Failed to delete service: %v", err) + getErr := errors.New(fmt.Sprintf("Failed to list tag. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return tagInfo, err } - b, _ := json.MarshalIndent(detachTagResults, "", " ") - fmt.Println(string(b)) - // Delete 진행 - deleteTagOptions := service.NewDeleteTagOptions(tagName) - deleteTagOptions.SetTagType("user") + if len(scanResult.Items) == 0 { + break + } - deleteTagResults, response, err := service.DeleteTag(deleteTagOptions) - if err != nil { - log.Fatalf("Failed to delete service: %v", err) + searchOptions.SearchCursor = scanResult.SearchCursor + + for _, item := range scanResult.Items { + var tagFound bool + + tags, ok := item.GetProperty("tags").([]interface{}) + if !ok { + cblogger.Error("Tags are not in expected format") + continue + } + for _, tag := range tags { + tagStr, ok := tag.(string) + if !ok { + cblogger.Errorf("Tag is not a string (%v)", tag) + continue + } + if strings.Contains(tagStr, keyword) { + tagFound = true + break + } + } + + if tagFound { + var tagKeyValue []irs.KeyValue + for _, tag := range tags { + tagStr, ok := tag.(string) + if !ok { + cblogger.Errorf("Tag is not a string (%v)", tag) + continue + } + parts := strings.SplitN(tagStr, ":", 2) + tagKeyValue = append(tagKeyValue, irs.KeyValue{ + Key: parts[0], + Value: parts[1], + }) + } + + rType, ok := item.GetProperty("type").(string) + if !ok { + cblogger.Error("type is not a string") + continue + } + rsType, err := ibmTypeToRSType(rType) + if err != nil { + cblogger.Error(err) + continue + } + + tagInfo = append(tagInfo, &irs.TagInfo{ + ResType: rsType, + ResIId: irs.IID{NameId: (item.GetProperty("name")).(string), SystemId: (item.GetProperty("resource_id").(string))}, + TagList: tagKeyValue, + KeyValueList: []irs.KeyValue{}, // reserved for optional usage + }) + } } + } - b, _ = json.MarshalIndent(deleteTagResults, "", " ") - fmt.Println(string(b)) + LoggingInfo(hiscallInfo, start) - // 응답 디버깅 - if response != nil { - fmt.Printf("Response status code: %d\n", response.StatusCode) - } -} \ No newline at end of file + return tagInfo, nil +} diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/VPCHandler.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/VPCHandler.go index fe1311182..acc39a360 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/VPCHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/VPCHandler.go @@ -615,6 +615,50 @@ func getVPCRawSubnets(vpc vpcv1.VPC, vpcService *vpcv1.VpcV1, ctx context.Contex return newSubnetInfos, nil } +func getRawSubnet(subnetIID irs.IID, vpcService *vpcv1.VpcV1, ctx context.Context) (vpcv1.Subnet, error) { + options := &vpcv1.ListSubnetsOptions{} + subnets, _, err := vpcService.ListSubnetsWithContext(ctx, options) + if err != nil { + return vpcv1.Subnet{}, err + } + var subnetFoundByName bool + var foundedSubnetByName vpcv1.Subnet + for { + if *subnets.TotalCount > 0 { + for _, subnet := range subnets.Subnets { + if subnetIID.SystemId != "" && *subnet.ID == subnetIID.SystemId { + return subnet, nil + } + if subnetIID.NameId == *subnet.Name { + if subnetFoundByName { + return vpcv1.Subnet{}, errors.New("found multiple subnets") + } + subnetFoundByName = true + foundedSubnetByName = subnet + } + } + } + nextstr, _ := getSubnetNextHref(subnets.Next) + if nextstr != "" { + options := &vpcv1.ListSubnetsOptions{ + Start: core.StringPtr(nextstr), + } + subnets, _, err = vpcService.ListSubnetsWithContext(ctx, options) + if err != nil { + break + } + } else { + break + } + } + + if subnetFoundByName { + return foundedSubnetByName, nil + } + + return vpcv1.Subnet{}, err +} + func getVPCRawSubnet(vpc vpcv1.VPC, subnetIID irs.IID, vpcService *vpcv1.VpcV1, ctx context.Context) (vpcv1.Subnet, error) { options := &vpcv1.ListSubnetsOptions{} subnets, _, err := vpcService.ListSubnetsWithContext(ctx, options) From c01cbb85bcdda5d4ce4cb84b946218817052ebab Mon Sep 17 00:00:00 2001 From: ish Date: Fri, 26 Jul 2024 18:41:25 +0900 Subject: [PATCH 47/56] IBM: Fix cluster creating issue --- .../drivers/ibmcloud-vpc/resources/ClusterHandler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go index 74323b623..9aff95abb 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go @@ -162,7 +162,7 @@ func (ic *IbmClusterHandler) CreateCluster(clusterReqInfo irs.ClusterInfo) (irs. if getRawVpcErr != nil { cblogger.Error(getRawVpcErr) LoggingError(hiscallInfo, getRawVpcErr) - ic.DeleteCluster(clusterReqInfo.IId) + _, _ = ic.DeleteCluster(clusterReqInfo.IId) return irs.ClusterInfo{}, errors.New(fmt.Sprintf("Failed to Create Cluster. err = %s", getRawVpcErr)) } @@ -1385,7 +1385,6 @@ func (ic *IbmClusterHandler) getWorkerPoolFromNodeGroupInfo(nodeGroupInfo irs.No ID: core.StringPtr(ic.Region.Zone), SubnetID: core.StringPtr(subnetId), }}, - OperatingSystem: core.StringPtr("UBUNTU_18_64"), } } From 5dd4322c967363f85d5b6d47fe7fd8f94110069d Mon Sep 17 00:00:00 2001 From: ish Date: Fri, 26 Jul 2024 18:41:46 +0900 Subject: [PATCH 48/56] IBM: Show cluster prices --- .../drivers/ibmcloud-vpc/resources/PriceInfoHandler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/PriceInfoHandler.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/PriceInfoHandler.go index 4af88ce0b..5183e7209 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/PriceInfoHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/PriceInfoHandler.go @@ -326,8 +326,8 @@ func (priceInfoHandler *IbmPriceInfoHandler) ListProductFamily(regionName string mutex.Lock() for _, resource := range rsInfoTemp.Resources { - // Only accept name starts with 'is.' - if strings.HasPrefix(resource.Name, "is.") { + // Only accept name starts with 'is.' or find kubernetes + if strings.HasPrefix(resource.Name, "is.") || resource.Name == "containers-kubernetes" { for _, geo := range resource.GeoTags { if geo == regionName { kinds = append(kinds, resource.Name) From 5bd23c29632a47060dbcec98fe2ca6624f381286 Mon Sep 17 00:00:00 2001 From: ish Date: Fri, 26 Jul 2024 19:39:46 +0900 Subject: [PATCH 49/56] IBM: Fix security group setup issue while creating cluster --- .../ibmcloud-vpc/resources/ClusterHandler.go | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go index 9aff95abb..7856d89d3 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go @@ -1413,15 +1413,29 @@ func (ic *IbmClusterHandler) initSecurityGroup(clusterReqInfo irs.ClusterInfo, c if getSgErr != nil { initSuccess = false ic.manageStatusTag(clusterCrn, SecurityGroupStatus, FAILED) - ic.DeleteCluster(clusterReqInfo.IId) + _, _ = ic.DeleteCluster(clusterReqInfo.IId) break } - _, sgUpdateErr := sgHandler.AddRules(defaultSgInfo.IId, sgInfo.SecurityRules) + var updateRules []irs.SecurityRuleInfo + for _, newRule := range *sgInfo.SecurityRules { + existCheck := false + for _, baseRule := range *defaultSgInfo.SecurityRules { + if equalsRule(newRule, baseRule) { + existCheck = true + break + } + } + if existCheck { + continue + } + updateRules = append(updateRules, newRule) + } + _, sgUpdateErr := sgHandler.AddRules(defaultSgInfo.IId, &updateRules) if sgUpdateErr != nil { initSuccess = false ic.manageStatusTag(clusterCrn, SecurityGroupStatus, FAILED) - ic.DeleteCluster(clusterReqInfo.IId) + _, _ = ic.DeleteCluster(clusterReqInfo.IId) break } } From 64f68aa42344cab2414467256f08e804d9ac940e Mon Sep 17 00:00:00 2001 From: ish Date: Fri, 26 Jul 2024 19:43:26 +0900 Subject: [PATCH 50/56] IBM: config: Update k8s version --- .../drivers/ibmcloud-vpc/main/conf/config.yaml.sample | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/conf/config.yaml.sample b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/conf/config.yaml.sample index 39c2f4865..e7bb05600 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/conf/config.yaml.sample +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/main/conf/config.yaml.sample @@ -72,7 +72,7 @@ ibmvpc: cluster: IID: nameId: mycluster - version: "1.24.9" + version: "1.30.2" network: vpcIID: nameId: "cb-sp-container" @@ -113,5 +113,5 @@ ibmvpc: desiredNodeSize: 2 minNodeSize: 2 maxNodeSize: 3 - upgradeVersion: "1.25.5" + upgradeVersion: "1.30.2" From 3d38547ec9b255f55806d32266ae8a09aeb397d3 Mon Sep 17 00:00:00 2001 From: ish Date: Fri, 26 Jul 2024 21:06:55 +0900 Subject: [PATCH 51/56] IBM: Add cluster support to TagHandler --- .../connect/Ibm_CloudConnection.go | 1 + .../ibmcloud-vpc/resources/ClusterHandler.go | 36 +++++++++++ .../ibmcloud-vpc/resources/TagHandler.go | 60 ++++++++++++++++--- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go index 959c8b8af..5263d2dbf 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/connect/Ibm_CloudConnection.go @@ -182,6 +182,7 @@ func (cloudConn *IbmCloudConnection) CreateTagHandler() (irs.TagHandler, error) CredentialInfo: cloudConn.CredentialInfo, Region: cloudConn.Region, VpcService: cloudConn.VpcService, + ClusterService: cloudConn.ClusterService, Ctx: cloudConn.Ctx, TaggingService: cloudConn.TaggingService, SearchService: cloudConn.SearchService, diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go index 7856d89d3..074299530 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/ClusterHandler.go @@ -317,6 +317,42 @@ func (ic *IbmClusterHandler) ListCluster() ([]*irs.ClusterInfo, error) { return ret, nil } +func (ic *IbmClusterHandler) getRawCluster(clusterIID irs.IID) (kubernetesserviceapiv1.GetClusterDetailResponse, error) { + rawCluster := kubernetesserviceapiv1.GetClusterDetailResponse{} + + if clusterIID.NameId == "" && clusterIID.SystemId == "" { + return rawCluster, errors.New("Failed to Get Cluster. err = invalid IID") + } + + resourceGroupId, getResourceGroupIdErr := ic.getDefaultResourceGroupId() + if getResourceGroupIdErr != nil { + return rawCluster, errors.New(fmt.Sprintf("Failed to Get Cluster. err = %s", getResourceGroupIdErr)) + } + + var cluster string + if clusterIID.SystemId != "" { + cluster = clusterIID.SystemId + } else { + cluster = clusterIID.NameId + } + rawClusters, _, getClustersErr := ic.ClusterService.VpcGetClusterWithContext(ic.Ctx, &kubernetesserviceapiv1.VpcGetClusterOptions{ + Cluster: core.StringPtr(cluster), + XAuthResourceGroup: core.StringPtr(resourceGroupId), + ShowResources: core.StringPtr("true"), + }) + if getClustersErr != nil { + return rawCluster, errors.New(fmt.Sprintf("Failed to Get Cluster. err = %s", getClustersErr)) + } + + for _, rCluster := range *rawClusters { + if rCluster.Id == clusterIID.SystemId || rCluster.Name == clusterIID.NameId { + return rCluster, nil + } + } + + return rawCluster, errors.New("Failed to Get Cluster. err = cluster not found") +} + func (ic *IbmClusterHandler) GetCluster(clusterIID irs.IID) (irs.ClusterInfo, error) { hiscallInfo := GetCallLogScheme(ic.Region, call.CLUSTER, clusterIID.NameId, "GetCluster()") start := call.Start() diff --git a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go index c74ca800c..90316d23d 100644 --- a/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/resources/TagHandler.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" call "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/call-log" + "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/drivers/ibmcloud-vpc/utils/kubernetesserviceapiv1" "strings" "github.com/IBM/platform-services-go-sdk/globalsearchv2" @@ -18,6 +19,7 @@ type IbmTagHandler struct { Region idrv.RegionInfo CredentialInfo idrv.CredentialInfo VpcService *vpcv1.VpcV1 + ClusterService *kubernetesserviceapiv1.KubernetesServiceApiV1 Ctx context.Context TaggingService *globaltaggingv1.GlobalTaggingV1 SearchService *globalsearchv2.GlobalSearchV2 @@ -73,7 +75,7 @@ func rsTypeToIBMType(resType irs.RSType) string { case irs.MYIMAGE: return "snapshot" case irs.CLUSTER: - return "" + return "k8-cluster" case irs.NODEGROUP: return "instance-group" default: @@ -82,8 +84,6 @@ func rsTypeToIBMType(resType irs.RSType) string { } func ibmTypeToRSType(ibmType string) (irs.RSType, error) { - fmt.Println(ibmType) - switch ibmType { case "image": return irs.IMAGE, nil @@ -103,8 +103,8 @@ func ibmTypeToRSType(ibmType string) (irs.RSType, error) { return irs.DISK, nil case "snapshot": return irs.MYIMAGE, nil - //case "???": - // return irs.CLUSTER + case "k8-cluster": + return irs.CLUSTER, nil case "instance-group": return irs.NODEGROUP, nil default: @@ -303,6 +303,22 @@ func handleTagAddOrRemove(tagHandler *IbmTagHandler, resType irs.RSType, resIID } err2 = attachOrDetachTag(tagHandler.TaggingService, tag, *rawNLB.CRN, action) + case irs.CLUSTER: + clusterHandler := &IbmClusterHandler{ + CredentialInfo: tagHandler.CredentialInfo, + Region: tagHandler.Region, + Ctx: tagHandler.Ctx, + VpcService: tagHandler.VpcService, + ClusterService: tagHandler.ClusterService, + TaggingService: tagHandler.TaggingService, + } + rawCluster, err := clusterHandler.getRawCluster(resIID) + if err != nil { + err2 = errors.New(fmt.Sprintf("Failed to add tag. err = %s", err)) + break + } + + err2 = attachOrDetachTag(tagHandler.TaggingService, tag, rawCluster.Crn, action) default: return errors.New("invalid resource type") } @@ -346,9 +362,9 @@ func (tagHandler *IbmTagHandler) ListTag(resType irs.RSType, resIID irs.IID) ([] var query string if resIID.NameId != "" { - query = fmt.Sprintf("type:%s AND name:%s", resType, resIID.NameId) + query = fmt.Sprintf("type:%s AND name:%s", ibmType, resIID.NameId) } else { - query = fmt.Sprintf("type:%s AND id:%s", resType, resIID.SystemId) + query = fmt.Sprintf("type:%s AND id:%s", ibmType, resIID.SystemId) } searchOptions.SetQuery(query) @@ -514,9 +530,37 @@ func (tagHandler *IbmTagHandler) FindTag(resType irs.RSType, keyword string) ([] continue } + name, ok := item.GetProperty("name").(string) + if !ok { + cblogger.Error("name is not a string") + continue + } + resourceId, ok := item.GetProperty("resource_id").(string) + if !ok { + cblogger.Error("resource_id is not a string") + continue + } + + if rsType == irs.CLUSTER { + clusterHandler := &IbmClusterHandler{ + CredentialInfo: tagHandler.CredentialInfo, + Region: tagHandler.Region, + Ctx: tagHandler.Ctx, + VpcService: tagHandler.VpcService, + ClusterService: tagHandler.ClusterService, + TaggingService: tagHandler.TaggingService, + } + rawCluster, err := clusterHandler.getRawCluster(irs.IID{NameId: name}) + if err != nil { + cblogger.Error(err) + continue + } + resourceId = rawCluster.Id + } + tagInfo = append(tagInfo, &irs.TagInfo{ ResType: rsType, - ResIId: irs.IID{NameId: (item.GetProperty("name")).(string), SystemId: (item.GetProperty("resource_id").(string))}, + ResIId: irs.IID{NameId: name, SystemId: resourceId}, TagList: tagKeyValue, KeyValueList: []irs.KeyValue{}, // reserved for optional usage }) From 0e3d12a51e5e421fc6e94b199e9023f469e3c1a3 Mon Sep 17 00:00:00 2001 From: powerkimhub Date: Sat, 27 Jul 2024 14:36:29 +0900 Subject: [PATCH 52/56] List/Get VMStatus need to wait for https://github.com/cloud-barista/cb-spider/pull/1244#issuecomment-2253741979 --- api-runtime/common-runtime/VMManager.go | 73 +++++++++++++++++++------ 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/api-runtime/common-runtime/VMManager.go b/api-runtime/common-runtime/VMManager.go index f056655c1..bedd13173 100644 --- a/api-runtime/common-runtime/VMManager.go +++ b/api-runtime/common-runtime/VMManager.go @@ -1364,7 +1364,7 @@ func ListVMStatus(connectionName string, rsType string) ([]*cres.VMStatusInfo, e return infoList, nil } - // (2) get VMStatusInfo List with iidInoList + // (2) get VMStatusInfo List with iidInfoList infoList2 := []*cres.VMStatusInfo{} for _, iidInfo := range iidInfoList { @@ -1373,17 +1373,36 @@ func ListVMStatus(connectionName string, rsType string) ([]*cres.VMStatusInfo, e */ // 2. get CSP:VMStatus(SystemId) - statusInfo, err := handler.GetVMStatus(getDriverIID(cres.IID{NameId: iidInfo.NameId, SystemId: iidInfo.SystemId})) - if err != nil { - //vmSPLock.RUnlock(connectionName, iidInfo.IId.NameId) - if checkNotFoundError(err) { - cblog.Info(err) - continue + var statusInfo cres.VMStatus + driverIID := getDriverIID(cres.IID{NameId: iidInfo.NameId, SystemId: iidInfo.SystemId}) + + // need to wait for https://github.com/cloud-barista/cb-spider/pull/1244#issuecomment-2253741979 + waiter := NewWaiter(3, 60) // 3 seconds sleep, 60 seconds timeout + + for { + statusInfo, err = handler.GetVMStatus(driverIID) + if statusInfo == cres.NotExist { + err = fmt.Errorf("Not Found %s", driverIID.SystemId) + } + if err != nil { + if checkNotFoundError(err) { + statusInfo = cres.NotExist + break + } + cblog.Error(err) + return nil, err + } + + if statusInfo == cres.Creating || statusInfo == cres.Running || statusInfo == cres.Suspending || statusInfo == cres.Suspended || + statusInfo == cres.Resuming || statusInfo == cres.Rebooting || statusInfo == cres.Terminating || statusInfo == cres.Terminated || + statusInfo == cres.NotExist || statusInfo == cres.Failed { + break + } + + if !waiter.Wait() { + return nil, fmt.Errorf("Unable to provide current VM status for VM '%s'. Timeout after %v seconds", iidInfo.NameId, waiter.Timeout) } - cblog.Error(err) - return nil, err } - //vmSPLock.RUnlock(connectionName, iidInfo.IId.NameId) infoList2 = append(infoList2, &cres.VMStatusInfo{IId: getUserIID(cres.IID{NameId: iidInfo.NameId, SystemId: iidInfo.SystemId}), VmStatus: statusInfo}) } @@ -1433,14 +1452,34 @@ func GetVMStatus(connectionName string, rsType string, nameID string) (cres.VMSt return "", err } - // (2) get CSP:VMStatus(SystemId) - info, err := handler.GetVMStatus(getDriverIID(cres.IID{NameId: iidInfo.NameId, SystemId: iidInfo.SystemId})) - if err != nil { - cblog.Error(err) - return "", err - } + driverIID := getDriverIID(cres.IID{NameId: iidInfo.NameId, SystemId: iidInfo.SystemId}) - return info, nil + // need to wait for https://github.com/cloud-barista/cb-spider/pull/1244#issuecomment-2253741979 + waiter := NewWaiter(3, 60) // 3 seconds sleep, 60 seconds timeout + + for { + info, err := handler.GetVMStatus(driverIID) + if info == cres.NotExist { + err = fmt.Errorf("Not Found %s", driverIID.SystemId) + } + if err != nil { + if checkNotFoundError(err) { + return "", err + } + cblog.Error(err) + return "", err + } + + if info == cres.Creating || info == cres.Running || info == cres.Suspending || info == cres.Suspended || + info == cres.Resuming || info == cres.Rebooting || info == cres.Terminating || info == cres.Terminated || + info == cres.NotExist || info == cres.Failed { + return info, nil + } + + if !waiter.Wait() { + return "", fmt.Errorf("Unable to provide current VM status for VM '%s'. Timeout after %v seconds", nameID, waiter.Timeout) + } + } } // (1) get IID(NameId) From de0369288dfadc35633d3162617bd77152f6f41b Mon Sep 17 00:00:00 2001 From: powerkimhub Date: Sat, 27 Jul 2024 14:37:10 +0900 Subject: [PATCH 53/56] Update the version of Tencent SDK --- go.mod | 2 +- go.sum | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 633b59e51..1826cc94a 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( github.com/labstack/echo/v4 v4.9.0 github.com/tencentcloud/tencentcloud-sdk-go-intl-en v3.0.531+incompatible github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cbs v1.0.492 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.964 golang.org/x/mod v0.18.0 k8s.io/api v0.22.5 k8s.io/apimachinery v0.22.5 @@ -97,7 +98,6 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.964 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect diff --git a/go.sum b/go.sum index d08ee0de5..256b4bfe5 100644 --- a/go.sum +++ b/go.sum @@ -709,7 +709,6 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.415 h1:ylKa86l github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.415/go.mod h1:XswWFMZ7ekTOWvZlUXr7I21pKyLuRAyZGgR5fMDTCyM= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.415/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.492/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.493 h1:iROpofiufr+AiHdF5Vl9FlD1MNo11T5qe+s4q0pLTVo= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.493/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.964 h1:ET3EulYQvWrdD5FNwOP+196w5Vbniy/uRGucM5ILExQ= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.964/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= From 5fc8fd8fa3e0e543bbd31d8c56318bca52594dfd Mon Sep 17 00:00:00 2001 From: powerkimhub Date: Sat, 27 Jul 2024 15:27:53 +0900 Subject: [PATCH 54/56] Change log messages with english --- .../cloud-driver/drivers/alibaba/resources/VMHandler.go | 2 +- .../cloud-driver/drivers/aws/resources/VMHandler.go | 2 +- .../cloud-driver/drivers/gcp/resources/VMHandler.go | 2 +- .../cloud-driver/drivers/tencent/resources/VMHandler.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VMHandler.go index 8a7a6b24c..55d47b4c3 100644 --- a/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/alibaba/resources/VMHandler.go @@ -1122,7 +1122,7 @@ func (vmHandler *AlibabaVMHandler) ConvertVMStatusString(vmStatus string) (irs.V cblogger.Errorf("Cannot find mapping information matching vmStatus [%s].", vmStatus) return irs.VMStatus("Failed"), errors.New("Cannot find CB VM status information matching " + vmStatus) } - cblogger.Infof("VM 상태 치환 : [%s] ==> [%s]", vmStatus, resultStatus) + cblogger.Infof("Replace VMStatus : [%s] ==> [%s]", vmStatus, resultStatus) return irs.VMStatus(resultStatus), nil } diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go index 152ec5752..f681276d3 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/VMHandler.go @@ -1495,7 +1495,7 @@ func ConvertVMStatusString(vmStatus string) (irs.VMStatus, error) { cblogger.Errorf("No mapping information found matching vmStatus [%s", vmStatus) return irs.VMStatus("Failed"), errors.New("Cannot find status information that matches " + vmStatus) } - cblogger.Infof("VM 상태 치환 : [%s] ==> [%s]", vmStatus, resultStatus) + cblogger.Infof("VReplace VMStatus : [%s] ==> [%s]", vmStatus, resultStatus) return irs.VMStatus(resultStatus), nil } diff --git a/cloud-control-manager/cloud-driver/drivers/gcp/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/gcp/resources/VMHandler.go index 690fb4afd..daa44f9db 100644 --- a/cloud-control-manager/cloud-driver/drivers/gcp/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/gcp/resources/VMHandler.go @@ -878,7 +878,7 @@ func ConvertVMStatusString(vmStatus string) (irs.VMStatus, error) { cblogger.Errorf("Couldn't find mapping information matching vmStatus [%s].", vmStatus) return irs.VMStatus("Failed"), errors.New("Couldn't find CB VM status information matching vmStatus " + vmStatus) } - cblogger.Infof("VM 상태 치환 : [%s] ==> [%s]", vmStatus, resultStatus) + cblogger.Infof("Replace VMStatus : [%s] ==> [%s]", vmStatus, resultStatus) return irs.VMStatus(resultStatus), nil } diff --git a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go index 57ab1765e..0a1ef59bd 100644 --- a/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/tencent/resources/VMHandler.go @@ -980,7 +980,7 @@ func ConvertVMStatusString(vmStatus string) (irs.VMStatus, error) { cblogger.Debugf("Mapping information matching vmStatus [%s] not found.", vmStatus) return irs.VMStatus("Failed"), errors.New("Cannot find CB VM status information matching " + vmStatus) } - cblogger.Infof("VM 상태 치환 : [%s] ==> [%s]", vmStatus, resultStatus) + cblogger.Infof("Replace VMStatus : [%s] ==> [%s]", vmStatus, resultStatus) return irs.VMStatus(resultStatus), nil } From 45e091f277215e50677176c3724462d80d7e8d70 Mon Sep 17 00:00:00 2001 From: powerkimhub Date: Sun, 28 Jul 2024 20:33:20 +0900 Subject: [PATCH 55/56] Tunning the left menu page --- .../rest-runtime/admin-web/html/left_menu.html | 16 +++++++++++++--- .../rest-runtime/admin-web/html/main.html | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/api-runtime/rest-runtime/admin-web/html/left_menu.html b/api-runtime/rest-runtime/admin-web/html/left_menu.html index 58e7f62fc..02e047714 100644 --- a/api-runtime/rest-runtime/admin-web/html/left_menu.html +++ b/api-runtime/rest-runtime/admin-web/html/left_menu.html @@ -4,7 +4,7 @@