diff --git a/cloud-control-manager/cloud-driver/drivers/aws/AwsDriver.go b/cloud-control-manager/cloud-driver/drivers/aws/AwsDriver.go index 8c2bdd22a..186d881ee 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/AwsDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/AwsDriver.go @@ -60,6 +60,7 @@ func (AwsDriver) GetDriverCapability() idrv.DriverCapabilityInfo { drvCapabilityInfo.NLBHandler = true drvCapabilityInfo.RegionZoneHandler = true drvCapabilityInfo.PriceInfoHandler = true + drvCapabilityInfo.TagHandler = true return drvCapabilityInfo } @@ -254,6 +255,8 @@ func (driver *AwsDriver) ConnectCloud(connectionInfo idrv.ConnectionInfo) (icon. // Connection for AnyCall AnyCallClient: vmClient, + + TagClient: vmClient, } return &iConn, nil // return type: (icon.CloudConnection, error) 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 55e366f6a..bf39d9e64 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/connect/AwsCloudConnection.go @@ -11,8 +11,6 @@ package connect import ( - "errors" - cblog "github.com/cloud-barista/cb-log" idrv "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces" @@ -63,6 +61,7 @@ type AwsCloudConnection struct { AutoScalingClient *autoscaling.AutoScaling AnyCallClient *ec2.EC2 + TagClient *ec2.EC2 } var cblogger *logrus.Logger @@ -111,6 +110,11 @@ func (cloudConn *AwsCloudConnection) CreateSecurityHandler() (irs.SecurityHandle return &handler, nil } +func (cloudConn *AwsCloudConnection) CreateTagHandler() (irs.TagHandler, error) { + handler := ars.AwsTagHandler{cloudConn.Region, cloudConn.VMClient} + return &handler, nil +} + /* func (cloudConn *AwsCloudConnection) CreateVNicHandler() (irs.VNicHandler, error) { cblogger.Info("Start") @@ -182,7 +186,3 @@ func (cloudConn *AwsCloudConnection) CreatePriceInfoHandler() (irs.PriceInfoHand handler := ars.AwsPriceInfoHandler{cloudConn.Region, cloudConn.PriceInfoClient} return &handler, nil } - -func (cloudConn *AwsCloudConnection) CreateTagHandler() (irs.TagHandler, error) { - return nil, errors.New("AWS Driver: not implemented") -} 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 434620938..47dd39da7 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 @@ -1777,6 +1777,111 @@ func handlePriceInfo() { } } +// Test Tag +func handleTag() { + cblogger.Debug("Start TagHandler Resource Test") + + ResourceHandler, err := getResourceHandler("Tag") + if err != nil { + panic(err) + } + handler := ResourceHandler.(irs.TagHandler) + + var reqType irs.RSType = irs.VM + reqIID := irs.IID{SystemId: "i-02ac1c4ff1d40815c"} + reqTag := irs.KeyValue{Key: "tag3", Value: "태그3"} + reqKey := "tag3" + 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: + 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: + 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: + 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: + 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: + 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)) + } + } + } + } +} + // handlerType : resources폴더의 xxxHandler.go에서 Handler이전까지의 문자열 // (예) ImageHandler.go -> "Image" func getResourceHandler(handlerType string) (interface{}, error) { @@ -1826,6 +1931,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 { @@ -1933,7 +2040,7 @@ func readConfigFile() Config { //cblogger.Infof("최종 환경 설정파일 경로 : [%s]", rootPath+"/config/config.yaml") //data, err := ioutil.ReadFile(rootPath + "/config/config.yaml") //data, err = ioutil.ReadFile("/Users/mzc01-swy/projects/feature_aws_filter_swy_240130/cloud-control-manager/cloud-driver/drivers/aws/main/Sample/config/config.yaml") - + data, err := ioutil.ReadFile(confPath) if err != nil { panic(err) @@ -1951,8 +2058,7 @@ func readConfigFile() Config { } func main() { - cblogger.Info("AWS Resource Test") - // handleVPC() + //handleVPC() // handleKeyPair() // handlePublicIP() // PublicIP 생성 후 conf // handleSecurity() @@ -1963,5 +2069,6 @@ func main() { // handleNLB() // handleCluster() //handleRegionZone() - handlePriceInfo() + //handlePriceInfo() + 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 new file mode 100644 index 000000000..a5abf02bc --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/TagHandler.go @@ -0,0 +1,382 @@ +// 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 Resouces interfaces of Cloud Driver. +// +// by devunet@mz.co.kr + +// https://github.com/cloud-barista/cb-spider/wiki/Tag-and-Cloud-Driver-API +package resources + +import ( + //"errors" + //"reflect" + //"strconv" + + "errors" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + + //"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" +) + +type AwsTagHandler struct { + Region idrv.RegionInfo + Client *ec2.EC2 +} + +// Map of RSType to AWS resource types +var rsTypeToAwsResourceTypeMap = map[irs.RSType]string{ + irs.IMAGE: "image", + irs.VPC: "vpc", + irs.SUBNET: "subnet", + irs.SG: "security-group", + irs.KEY: "key-pair", + irs.VM: "instance", + irs.NLB: "network-load-balancer", + irs.DISK: "volume", + irs.MYIMAGE: "image", + irs.CLUSTER: "cluster", + irs.NODEGROUP: "nodegroup", +} + +// Map of AWS resource types to RSType for response handling +var awsResourceTypeToRSTypeMap = map[string]irs.RSType{ + "image": irs.IMAGE, + "vpc": irs.VPC, + "subnet": irs.SUBNET, + "security-group": irs.SG, + "key-pair": irs.KEY, + "instance": irs.VM, + "network-load-balancer": irs.NLB, + "volume": irs.DISK, + //"image": irs.MYIMAGE, + "cluster": irs.CLUSTER, + "nodegroup": irs.NODEGROUP, +} + +func (tagHandler *AwsTagHandler) 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{}, errors.New(msg) + } + + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.SystemId, "CreateTags()") + start := call.Start() + + // 리소스에 신규 태그 추가 + result, errtag := tagHandler.Client.CreateTags(&ec2.CreateTagsInput{ + Resources: []*string{&resIID.SystemId}, + Tags: []*ec2.Tag{ + { + Key: aws.String(tag.Key), + Value: aws.String(tag.Value), + }, + }, + }) + + if errtag != nil { + cblogger.Errorf("Failed to add [%s] Tag for [%s]", tag.Key, resIID.SystemId) + cblogger.Error(errtag) + return irs.KeyValue{}, errtag + } + LoggingInfo(hiscallInfo, start) + + if cblogger.Level.String() == "debug" { + cblogger.Info(result) + } + + return tag, nil +} + +func (tagHandler *AwsTagHandler) 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, errors.New(msg) + } + + input := &ec2.DescribeTagsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("resource-id"), + Values: []*string{ + aws.String(resIID.SystemId), + }, + }, + }, + } + if cblogger.Level.String() == "debug" { + cblogger.Debug(input) + } + + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.SystemId, "DescribeTags()") + start := call.Start() + + result, errtag := tagHandler.Client.DescribeTags(input) + if errtag != nil { + cblogger.Errorf("Failed to look up tags for [%s]", resIID.NameId) + cblogger.Error(errtag) + LoggingError(hiscallInfo, errtag) + return nil, errtag + } + LoggingInfo(hiscallInfo, start) + + if cblogger.Level.String() == "debug" { + cblogger.Info(result) + } + + var retTagList []irs.KeyValue + for _, tag := range result.Tags { + retTagList = append(retTagList, irs.KeyValue{ + Key: aws.StringValue(tag.Key), + Value: aws.StringValue(tag.Value), + }) + } + + return retTagList, nil +} + +// describetags +func (tagHandler *AwsTagHandler) 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{}, errors.New(msg) + } + + input := &ec2.DescribeTagsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("resource-id"), + Values: []*string{ + aws.String(resIID.SystemId), + }, + }, + { + Name: aws.String("key"), + Values: []*string{ + aws.String(key), + }, + }, + }, + } + + if cblogger.Level.String() == "debug" { + cblogger.Debug(input) + } + + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.SystemId, "DescribeTags()") + start := call.Start() + + result, errtag := tagHandler.Client.DescribeTags(input) + if errtag != nil { + cblogger.Errorf("Failed to lookup the [%s] tag key of an [%s] object", key, resIID.NameId) + cblogger.Error(errtag) + LoggingError(hiscallInfo, errtag) + return irs.KeyValue{}, errtag + } + LoggingInfo(hiscallInfo, start) + + if cblogger.Level.String() == "debug" { + cblogger.Info("---------------------") + cblogger.Info(result) + cblogger.Info("---------------------") + } + + if len(result.Tags) == 0 { + msg := "tag with key " + key + " not found" + cblogger.Error(msg) + return irs.KeyValue{}, errors.New(msg) + } + + var retTag irs.KeyValue + for _, tag := range result.Tags { + if aws.StringValue(tag.Key) == key { + retTag.Key = aws.StringValue(tag.Key) + retTag.Value = aws.StringValue(tag.Value) + break + } + } + + return retTag, nil +} + +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) + + if resIID.SystemId == "" || key == "" { + msg := "resIID.SystemId or key is not provided" + cblogger.Error(msg) + return false, errors.New(msg) + } + + input := &ec2.DeleteTagsInput{ + Resources: []*string{ + aws.String(resIID.SystemId), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String(key), + }, + }, + } + + if cblogger.Level.String() == "debug" { + cblogger.Debug(input) + } + + hiscallInfo := GetCallLogScheme(tagHandler.Region, call.TAG, resIID.SystemId, "DeleteTags()") + start := call.Start() + + result, errtag := tagHandler.Client.DeleteTags(input) + if errtag != nil { + cblogger.Errorf("Failed to delete [%s] tag key of an [%s] object", key, resIID.NameId) + cblogger.Error(errtag) + LoggingError(hiscallInfo, errtag) + return false, errtag + } + LoggingInfo(hiscallInfo, start) + + if cblogger.Level.String() == "debug" { + cblogger.Info(result) + } + + return true, nil +} + +// 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 (tagHandler *AwsTagHandler) FindTag(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) + } + + keyResult, err := tagHandler.Client.DescribeTags(keyInput) + if err != nil { + return nil, fmt.Errorf("failed to describe tags by key: %w", err) + } + 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) + } + + valueResult, err := tagHandler.Client.DescribeTags(valueInput) + if err != nil { + return nil, fmt.Errorf("failed to describe tags by value: %w", err) + } + processTags(valueResult) + } else { + // Search all tags if keyword is empty or "*" + input := &ec2.DescribeTagsInput{ + Filters: filters, + } + + result, err := tagHandler.Client.DescribeTags(input) + if err != nil { + return nil, fmt.Errorf("failed to describe tags: %w", err) + } + processTags(result) + } + + var tagInfos []*irs.TagInfo + for _, tagInfo := range tagInfoMap { + tagInfos = append(tagInfos, tagInfo) + } + + return tagInfos, nil +}