diff --git a/cloud-control-manager/cloud-driver/call-log/calllogger.go b/cloud-control-manager/cloud-driver/call-log/calllogger.go index e19c6b54c..3d4bba8f9 100644 --- a/cloud-control-manager/cloud-driver/call-log/calllogger.go +++ b/cloud-control-manager/cloud-driver/call-log/calllogger.go @@ -32,25 +32,26 @@ type RES_TYPE string const ( //=========== CloudOS (ref: cb-spider/cloud-driver-libs/cloudos.yaml) - AWS CLOUD_OS = "AWS" - AZURE CLOUD_OS = "AZURE" - GCP CLOUD_OS = "GCP" - ALIBABA CLOUD_OS = "ALIBABA" - TENCENT CLOUD_OS = "TENCENT" - IBM CLOUD_OS = "IBM" - OPENSTACK CLOUD_OS = "OPENSTACK" - CLOUDIT CLOUD_OS = "CLOUDIT" - NCP CLOUD_OS = "NCP" - NCPVPC CLOUD_OS = "NCPVPC" - NHNCLOUD CLOUD_OS = "NHNCLOUD" - KTCLOUD CLOUD_OS = "KTCLOUD" - KTCLOUDVPC CLOUD_OS = "KTCLOUDVPC" - DOCKER CLOUD_OS = "DOCKER" - MOCK CLOUD_OS = "MOCK" - CLOUDTWIN CLOUD_OS = "CLOUDTWIN" + AWS CLOUD_OS = "AWS" + AZURE CLOUD_OS = "AZURE" + GCP CLOUD_OS = "GCP" + ALIBABA CLOUD_OS = "ALIBABA" + TENCENT CLOUD_OS = "TENCENT" + IBM CLOUD_OS = "IBM" + OPENSTACK CLOUD_OS = "OPENSTACK" + CLOUDIT CLOUD_OS = "CLOUDIT" + NCP CLOUD_OS = "NCP" + NCPVPC CLOUD_OS = "NCPVPC" + NHNCLOUD CLOUD_OS = "NHNCLOUD" + KTCLOUD CLOUD_OS = "KTCLOUD" + KTCLOUDVPC CLOUD_OS = "KTCLOUDVPC" + DOCKER CLOUD_OS = "DOCKER" + MOCK CLOUD_OS = "MOCK" + CLOUDTWIN CLOUD_OS = "CLOUDTWIN" //=========== ResourceType REGIONZONE RES_TYPE = "REGIONZONE" + PRICEINFO RES_TYPE = "PRICEINFO" VMIMAGE RES_TYPE = "VMIMAGE" VMSPEC RES_TYPE = "VMSPEC" VPCSUBNET RES_TYPE = "VPC/SUBNET" @@ -62,7 +63,7 @@ const ( NLB RES_TYPE = "NETWORKLOADBALANCER" //=========== PMKS: Provider-Managed K8S - CLUSTER RES_TYPE = "CLUSTER" + CLUSTER RES_TYPE = "CLUSTER" ) type CALLLogger struct { @@ -194,7 +195,7 @@ func getFormatter(loggerName string) *calllogformatter.Formatter { return callFormatter } -//========================= +// ========================= type CLOUDLOGSCHEMA struct { CloudOS CLOUD_OS // ex) AWS | AZURE | ALIBABA | GCP | OPENSTACK | CLOUDTWIN | CLOUDIT | DOCKER | NCP | MOCK | IBM RegionZone string // ex) us-east1/us-east1-c diff --git a/cloud-control-manager/cloud-driver/drivers/azure/AzureDriver.go b/cloud-control-manager/cloud-driver/drivers/azure/AzureDriver.go index fce5ab27a..a79352f01 100644 --- a/cloud-control-manager/cloud-driver/drivers/azure/AzureDriver.go +++ b/cloud-control-manager/cloud-driver/drivers/azure/AzureDriver.go @@ -54,6 +54,7 @@ func (AzureDriver) GetDriverCapability() idrv.DriverCapabilityInfo { drvCapabilityInfo.DiskHandler = true drvCapabilityInfo.MyImageHandler = true drvCapabilityInfo.RegionZoneHandler = true + drvCapabilityInfo.PriceInfoHandler = true drvCapabilityInfo.ClusterHandler = true return drvCapabilityInfo diff --git a/cloud-control-manager/cloud-driver/drivers/azure/connect/Azure_CloudConnection.go b/cloud-control-manager/cloud-driver/drivers/azure/connect/Azure_CloudConnection.go index 3270b3667..4407d65e1 100644 --- a/cloud-control-manager/cloud-driver/drivers/azure/connect/Azure_CloudConnection.go +++ b/cloud-control-manager/cloud-driver/drivers/azure/connect/Azure_CloudConnection.go @@ -188,6 +188,17 @@ func (cloudConn *AzureCloudConnection) CreateRegionZoneHandler() (irs.RegionZone return ®ionZoneHandler, nil } +func (cloudConn *AzureCloudConnection) CreatePriceInfoHandler() (irs.PriceInfoHandler, error) { + cblogger.Info("Azure Cloud Driver: called CreatePriceInfoHandler()!") + priceInfoHandler := azrs.AzurePriceInfoHandler{ + CredentialInfo: cloudConn.CredentialInfo, + Region: cloudConn.Region, + Ctx: cloudConn.Ctx, + ResourceSkusClient: cloudConn.ResourceSkusClient, + } + return &priceInfoHandler, nil +} + func (cloudConn *AzureCloudConnection) IsConnected() (bool, error) { return true, nil } @@ -219,7 +230,3 @@ func (cloudConn *AzureCloudConnection) CreateClusterHandler() (irs.ClusterHandle func (cloudConn *AzureCloudConnection) CreateAnyCallHandler() (irs.AnyCallHandler, error) { return nil, errors.New("Azure Driver: not implemented") } - -func (*AzureCloudConnection) CreatePriceInfoHandler() (irs.PriceInfoHandler, error) { - return nil, errors.New("Alibaba Driver: not implemented") -} diff --git a/cloud-control-manager/cloud-driver/drivers/azure/main/Test_Resources.go b/cloud-control-manager/cloud-driver/drivers/azure/main/Test_Resources.go index 9b698d941..c9d2f4bf6 100644 --- a/cloud-control-manager/cloud-driver/drivers/azure/main/Test_Resources.go +++ b/cloud-control-manager/cloud-driver/drivers/azure/main/Test_Resources.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "errors" "fmt" cblog "github.com/cloud-barista/cb-log" @@ -12,6 +13,7 @@ import ( "gopkg.in/yaml.v3" "io/ioutil" "os" + "strings" "time" ) @@ -160,8 +162,9 @@ func showTestHandlerInfo() { cblogger.Info("8. DiskHandler") cblogger.Info("9. MyImageHandler") cblogger.Info("10. RegionZoneHandler") - cblogger.Info("11. ClusterHandler") - cblogger.Info("12. Exit") + cblogger.Info("11. PriceInfoHandler") + cblogger.Info("12. ClusterHandler") + cblogger.Info("13. Exit") cblogger.Info("==========================================================") } @@ -205,6 +208,8 @@ func getResourceHandler(resourceType string, config Config) (interface{}, error) resourceHandler, err = cloudConnection.CreateMyImageHandler() case "regionzone": resourceHandler, err = cloudConnection.CreateRegionZoneHandler() + case "price": + resourceHandler, err = cloudConnection.CreatePriceInfoHandler() case "cluster": resourceHandler, err = cloudConnection.CreateClusterHandler() } @@ -1306,6 +1311,109 @@ Loop: } } +func testPriceInfoHandlerListPrint() { + cblogger.Info("Test PriceInfoHandler") + cblogger.Info("0. Print Menu") + cblogger.Info("1. ListProductFamily()") + cblogger.Info("2. GetPriceInfo()") + cblogger.Info("3. Exit") +} + +func testPriceInfoHandler(config Config) { + resourceHandler, err := getResourceHandler("price", config) + if err != nil { + cblogger.Error(err) + return + } + priceInfoHandler := resourceHandler.(irs.PriceInfoHandler) + + testPriceInfoHandlerListPrint() +Loop: + for { + var commandNum int + inputCnt, err := fmt.Scan(&commandNum) + if err != nil { + cblogger.Error(err) + } + + if inputCnt == 1 { + switch commandNum { + case 0: + testPriceInfoHandlerListPrint() + case 1: + cblogger.Info("Start ListProductFamily() ...") + var region string + fmt.Print("Enter Region Name: ") + if _, err := fmt.Scanln(®ion); err != nil { + cblogger.Error(err) + } + if listProductFamily, err := priceInfoHandler.ListProductFamily(region); err != nil { + cblogger.Error(err) + } else { + spew.Dump(listProductFamily) + } + cblogger.Info("Finish ListProductFamily()") + case 2: + cblogger.Info("Start GetPriceInfo() ...") + fmt.Println("=== Enter Product Familiy ===") + in := bufio.NewReader(os.Stdin) + productFamiliy, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + + productFamiliy = strings.TrimSpace(productFamiliy) + var region string + fmt.Print("Enter Region Name: ") + if _, err := fmt.Scanln(®ion); err != nil { + cblogger.Error(err) + } + + var addFilterList string + var filterList []irs.KeyValue + for { + fmt.Print("Add filter list? (y/N): ") + _, err := fmt.Scanln(&addFilterList) + if err != nil || strings.ToLower(addFilterList) == "n" { + break + } + + fmt.Println("=== Enter key to filter ===") + in = bufio.NewReader(os.Stdin) + key, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + key = strings.TrimSpace(key) + + fmt.Println("=== Enter value to filter ===") + in = bufio.NewReader(os.Stdin) + value, err := in.ReadString('\n') + if err != nil { + cblogger.Error(err) + } + value = strings.TrimSpace(value) + + filterList = append(filterList, irs.KeyValue{ + Key: key, + Value: value, + }) + } + + if priceInfo, err := priceInfoHandler.GetPriceInfo(productFamiliy, region, filterList); err != nil { + cblogger.Error(err) + } else { + spew.Dump(priceInfo) + } + cblogger.Info("Finish GetPriceInfo()") + case 3: + cblogger.Info("Exit") + break Loop + } + } + } +} + func testClusterHandlerListPrint() { cblogger.Info("Test ClusterHandler") cblogger.Info("0. Print Menu") @@ -1869,9 +1977,12 @@ Loop: testRegionZoneHandler(config) showTestHandlerInfo() case 11: - testClusterHandler(config) + testPriceInfoHandler(config) showTestHandlerInfo() case 12: + testClusterHandler(config) + showTestHandlerInfo() + case 13: cblogger.Info("Exit Test ResourceHandler Program") break Loop } diff --git a/cloud-control-manager/cloud-driver/drivers/azure/resources/CommonAzureFunc.go b/cloud-control-manager/cloud-driver/drivers/azure/resources/CommonAzureFunc.go index f572b00f7..976084d26 100644 --- a/cloud-control-manager/cloud-driver/drivers/azure/resources/CommonAzureFunc.go +++ b/cloud-control-manager/cloud-driver/drivers/azure/resources/CommonAzureFunc.go @@ -6,6 +6,7 @@ import ( irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" "math/rand" "net" + "sort" "strconv" "strings" "sync" @@ -301,3 +302,20 @@ func overlapCheckCidr(cidr1 string, cidr2 string) (bool, error) { check2 := cidr2IPnet.Contains(cidr1IP) return !check1 && !check2, nil } + +func removeDuplicateStr(array []string) []string { + if len(array) < 1 { + return array + } + + sort.Strings(array) + prev := 1 + for curr := 1; curr < len(array); curr++ { + if array[curr-1] != array[curr] { + array[prev] = array[curr] + prev++ + } + } + + return array[:prev] +} diff --git a/cloud-control-manager/cloud-driver/drivers/azure/resources/PriceInfoHandler.go b/cloud-control-manager/cloud-driver/drivers/azure/resources/PriceInfoHandler.go new file mode 100644 index 000000000..f7cbae36e --- /dev/null +++ b/cloud-control-manager/cloud-driver/drivers/azure/resources/PriceInfoHandler.go @@ -0,0 +1,268 @@ +package resources + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-03-01/compute" + 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" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +type AzurePriceInfoHandler struct { + CredentialInfo idrv.CredentialInfo + Region idrv.RegionInfo + Ctx context.Context + ResourceSkusClient *compute.ResourceSkusClient +} + +const AzurePriceApiEndpoint = "https://prices.azure.com/api/retail/prices" + +type Item struct { + CurrencyCode string `json:"currencyCode"` + TierMinimumUnits float64 `json:"tierMinimumUnits"` + RetailPrice float64 `json:"retailPrice"` + UnitPrice float64 `json:"unitPrice"` + ArmRegionName string `json:"armRegionName"` + Location string `json:"location"` + EffectiveStartDate time.Time `json:"effectiveStartDate"` + MeterID string `json:"meterId"` + MeterName string `json:"meterName"` + ProductID string `json:"productId"` + SkuID string `json:"skuId"` + ProductName string `json:"productName"` + SkuName string `json:"skuName"` + ServiceName string `json:"serviceName"` + ServiceID string `json:"serviceId"` + ServiceFamily string `json:"serviceFamily"` + UnitOfMeasure string `json:"unitOfMeasure"` + Type string `json:"type"` + IsPrimaryMeterRegion bool `json:"isPrimaryMeterRegion"` + ArmSkuName string `json:"armSkuName"` + ReservationTerm string `json:"reservationTerm,omitempty"` + EffectiveEndDate time.Time `json:"effectiveEndDate,omitempty"` +} + +type PriceInfo struct { + BillingCurrency string `json:"BillingCurrency"` + CustomerEntityID string `json:"CustomerEntityId"` + CustomerEntityType string `json:"CustomerEntityType"` + Items []Item `json:"items"` + NextPageLink string `json:"NextPageLink"` + Count int `json:"Count"` +} + +func getAzurePriceInfo(filterOption string) ([]byte, error) { + URL := AzurePriceApiEndpoint + "?$filter=" + url.QueryEscape(filterOption) + + fmt.Println(URL) + + ctx := context.Background() + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, URL, nil) + if err != nil { + return nil, err + } + + req = req.WithContext(ctx) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer func() { + _ = resp.Body.Close() + }() + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return responseBody, nil +} + +func (priceInfoHandler *AzurePriceInfoHandler) ListProductFamily(regionName string) ([]string, error) { + hiscallInfo := GetCallLogScheme(priceInfoHandler.Region, call.PRICEINFO, "PriceInfo", "ListProductFamily()") + start := call.Start() + + result, err := getAzurePriceInfo("armRegionName eq '" + regionName + "'") + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to List ProductFamily. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return nil, getErr + } + + LoggingInfo(hiscallInfo, start) + + var priceInfo PriceInfo + err = json.Unmarshal(result, &priceInfo) + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to List ProductFamily. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return nil, getErr + } + + var serviceFamilyList []string + for _, item := range priceInfo.Items { + serviceFamilyList = append(serviceFamilyList, item.ServiceFamily) + } + serviceFamilyList = removeDuplicateStr(serviceFamilyList) + + return serviceFamilyList, nil +} + +func (priceInfoHandler *AzurePriceInfoHandler) GetPriceInfo(productFamily string, regionName string, filterList []irs.KeyValue) (string, error) { + hiscallInfo := GetCallLogScheme(priceInfoHandler.Region, call.PRICEINFO, "PriceInfo", "ListProductFamily()") + start := call.Start() + + filterOption := "serviceFamily eq '" + productFamily + "' and armRegionName eq '" + regionName + "'" + for _, filter := range filterList { + filterOption += " and " + filter.Key + " eq '" + filter.Value + "'" + } + + result, err := getAzurePriceInfo(filterOption) + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to List ProductFamily. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return "", getErr + } + + LoggingInfo(hiscallInfo, start) + + var priceInfo PriceInfo + err = json.Unmarshal(result, &priceInfo) + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to List ProductFamily. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return "", getErr + } + + var resultResourceSkusClient compute.ResourceSkusResultPage + + if strings.ToLower(productFamily) == "compute" { + resultResourceSkusClient, err = priceInfoHandler.ResourceSkusClient.List(priceInfoHandler.Ctx, "location eq '"+regionName+"'") + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to List ProductFamily. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return "", getErr + } + } + + organized := make(map[string][]Item) + for _, item := range priceInfo.Items { + organized[item.ProductID] = append(organized[item.ProductID], item) + } + + var priceList []irs.Price + for _, value := range organized { + if len(value) == 0 { + continue + } + + vCPUs := "NA" + memoryGB := "NA" + storageGB := "NA" + operatingSystem := "NA" + + if strings.ToLower(productFamily) == "compute" { + for _, val := range resultResourceSkusClient.Values() { + if value[0].SkuName == *val.Name { + for _, capability := range *val.Capabilities { + if *capability.Name == "OSVhdSizeMB" { + sizeMB, _ := strconv.Atoi(*capability.Value) + sizeGB := float64(sizeMB) / 1024 + storageGB = strconv.FormatFloat(sizeGB, 'f', -1, 64) + " GiB" + } else if *capability.Name == "vCPUs" { + vCPUs = *capability.Value + } else if *capability.Name == "MemoryGB" { + memoryGB = *capability.Value + " GiB" + } + } + } + } + } + + productNameToLower := strings.ToLower(value[0].ProductName) + if strings.Contains(productNameToLower, "windows") { + operatingSystem = "Windows" + } else if strings.Contains(productNameToLower, "linux") { + operatingSystem = "Linux" + } + + var pricingPolicies []irs.PricingPolicies + for _, item := range value { + pricingPolicies = append(pricingPolicies, irs.PricingPolicies{ + PricingId: item.SkuID, + PricingPolicy: item.Type, + Unit: item.UnitOfMeasure, + Currency: item.CurrencyCode, + Price: strconv.FormatFloat(item.RetailPrice, 'f', -1, 64), + Description: "NA", + PricingPolicyInfo: nil, + }) + } + + priceList = append(priceList, irs.Price{ + ProductInfo: irs.ProductInfo{ + ProductId: value[0].ProductID, + RegionName: value[0].ArmRegionName, + InstanceType: value[0].ArmSkuName, + Vcpu: vCPUs, + Memory: memoryGB, + Storage: storageGB, + Gpu: "NA", + GpuMemory: "NA", + OperatingSystem: operatingSystem, + PreInstalledSw: "", + VolumeType: "NA", + StorageMedia: "NA", + MaxVolumeSize: "", + MaxIOPSVolume: "", + MaxThroughputVolume: "", + Description: value[0].ProductName, + CSPProductInfo: value[0], + }, + PriceInfo: irs.PriceInfo{ + PricingPolicies: pricingPolicies, + CSPPriceInfo: value, + }, + }) + } + + cloudPriceData := irs.CloudPriceData{ + Meta: irs.Meta{ + Version: "v0.1", + Description: "Multi-Cloud Price Info", + }, + CloudPriceList: []irs.CloudPrice{ + { + CloudName: "Azure", + PriceList: priceList, + }, + }, + } + + data, err := json.Marshal(cloudPriceData) + if err != nil { + getErr := errors.New(fmt.Sprintf("Failed to List ProductFamily. err = %s", err)) + cblogger.Error(getErr.Error()) + LoggingError(hiscallInfo, getErr) + return "", getErr + } + + return string(data), nil +} diff --git a/cloud-control-manager/cloud-driver/drivers/azure/resources/RegionZoneHandler.go b/cloud-control-manager/cloud-driver/drivers/azure/resources/RegionZoneHandler.go index 67616ca67..66a39e76c 100644 --- a/cloud-control-manager/cloud-driver/drivers/azure/resources/RegionZoneHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/azure/resources/RegionZoneHandler.go @@ -12,7 +12,6 @@ import ( 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" "reflect" - "sort" "strings" "sync" ) @@ -26,23 +25,6 @@ type AzureRegionZoneHandler struct { ResourceSkusClient *compute.ResourceSkusClient } -func removeDuplicateStr(array []string) []string { - if len(array) < 1 { - return array - } - - sort.Strings(array) - prev := 1 - for curr := 1; curr < len(array); curr++ { - if array[curr-1] != array[curr] { - array[prev] = array[curr] - prev++ - } - } - - return array[:prev] -} - func (regionZoneHandler *AzureRegionZoneHandler) ListRegionZone() ([]*irs.RegionZoneInfo, error) { hiscallInfo := GetCallLogScheme(regionZoneHandler.Region, call.REGIONZONE, "RegionZone", "ListRegionZone()") start := call.Start()