From d7f8bf2d3dad32315b93448edf17384eef235ffe Mon Sep 17 00:00:00 2001 From: innodreamer Date: Fri, 7 Feb 2025 22:46:11 +0900 Subject: [PATCH 1/5] Update README.md --- cloud-control-manager/cloud-driver/drivers/ktcloud/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ktcloud/README.md b/cloud-control-manager/cloud-driver/drivers/ktcloud/README.md index d5e9c39df..64bf84de1 100644 --- a/cloud-control-manager/cloud-driver/drivers/ktcloud/README.md +++ b/cloud-control-manager/cloud-driver/drivers/ktcloud/README.md @@ -80,8 +80,8 @@ $GOPATH/src/github.com/cloud-barista/ktcloud/ktcloud/main/


​ O VM 생성을 위한 VMImage ID, VMSpec ID 결정 관련 - - 해당 zone에서 지원하는 VM Image(KT Cloud의 Template) 목록중 사용하고자 하는 운영체제(OS)에 대한 Image ID 값을 찾은 뒤, VM Spec 목록에서 추가 정보로 제공하는 'SupportingImage(Template)ID'에서 그 Image ID와 같은 VM Spec을 찾아 해당 Image ID를 지원하는 VMSpec ID를 사용해야함. - - 위와 같이 해당 VMImage를 지원하는 VMSpec ID를 사용해야하는데, 그렇지 않은 경우 KT Cloud에서는 error message로 "general error"를 return함. + - 해당 zone에서 지원하는 VM Image(KT Cloud의 Template) 목록중 사용하고자 하는 운영체제(OS)에 대한 Image ID 값을 찾은 뒤, VM Spec 목록에서 추가 정보로 제공하는 'CorrespondingImageIds'에서 그 Image ID와 같은 ID를 찾아 해당 Image ID를 지원하는 VMSpec ID를 지정하여 VM을 생성해야함. + - 위와 같이 해당 VM Image를 지원하는 VMSpec ID를 사용해야하는데, 그렇지 않은 경우 KT Cloud에서는 error message로 "general error"를 return함.


​ O Security Group 설정시 주의해야할 사항으로, 본 드라이버는 inbound rule만 지원하고, protocol별 rule이 중복되지 않아야함. From a22c2b40991fc263684045063f9a623b4973d9c7 Mon Sep 17 00:00:00 2001 From: innodreamer Date: Fri, 7 Feb 2025 22:55:32 +0900 Subject: [PATCH 2/5] [KT Cloud] Enhance Supported VM Spec Info --- .../ktcloud/resources/VMSpecHandler.go | 280 +++++++++++------- .../ktcloudvpc/resources/VMSpecHandler.go | 17 +- 2 files changed, 181 insertions(+), 116 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/VMSpecHandler.go b/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/VMSpecHandler.go index 950ebd061..ee7734c8b 100644 --- a/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/VMSpecHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/VMSpecHandler.go @@ -8,21 +8,24 @@ // // by ETRI, 2021.05. // Updated by ETRI, 2023.11. +// Updated by ETRI, 2025.02. package resources import ( - ktsdk "github.com/cloud-barista/ktcloud-sdk-go" - "errors" "strings" "strconv" + "regexp" + "fmt" + "time" // "github.com/davecgh/go-spew/spew" cblog "github.com/cloud-barista/cb-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" + + ktsdk "github.com/cloud-barista/ktcloud-sdk-go" ) type KtCloudVMSpecHandler struct { @@ -40,43 +43,12 @@ func init() { // 'TemplateId' in KT Cloud : supporting OS info ID // 'ServiceOfferingId' in KT Cloud : CPU/Memory info ID -func (vmSpecHandler *KtCloudVMSpecHandler) GetVMSpec(VMSpecName string) (irs.VMSpecInfo, error) { +func (vmSpecHandler *KtCloudVMSpecHandler) GetVMSpec(specName string) (irs.VMSpecInfo, error) { cblogger.Info("KT Cloud cloud driver: called GetVMSpec()!") // Caution!! : KT Cloud doesn't support 'Region' officially, so we use 'Zone info.' which is from the connection info. - var resultVMSpecInfo irs.VMSpecInfo - regionInfo := vmSpecHandler.RegionInfo.Region - zoneId := vmSpecHandler.RegionInfo.Zone - - cblogger.Infof("Region : [%s] / SpecName : [%s]", regionInfo, VMSpecName) - cblogger.Info("RegionInfo.Zone : ", zoneId) - - //var ktZoneInfo ktsdk.Zone - - //To get the Zone Name of the zoneId - // var ktZoneName string - // response, err := vmSpecHandler.Client.ListZones(true, "", "", "") - // if err != nil { - // cblogger.Error("Error to get list of available Zones: %s", err) - - // return irs.VMSpecInfo{}, err - // } - - // for _, Zone := range response.Listzonesresponse.Zone { - // cblogger.Info("# Search criteria of Zoneid : ", zoneId) - - // if Zone.Id == zoneId { - // // KT Cloud는 Zone별로 Spec이 다르므로 Zone 정보도 넘김. - // ktZoneName = Zone.Name - - // break - // } - // } - // cblogger.Info("Zone Name : ", ktZoneName) - - - // Caution!! : KT Cloud는 Image info/VMSpc info 조회시 zoneid로 조회함. - result, err := vmSpecHandler.Client.ListAvailableProductTypes(zoneId) + // Caution!! : When searching for Image info/VMSpc info, KT Cloud inquires using Zoneid. + result, err := vmSpecHandler.Client.ListAvailableProductTypes(vmSpecHandler.RegionInfo.Zone) if err != nil { cblogger.Error("Failed to Get List of Available Product Types: %s", err) return irs.VMSpecInfo{}, err @@ -86,31 +58,37 @@ func (vmSpecHandler *KtCloudVMSpecHandler) GetVMSpec(VMSpecName string) (irs.VMS return irs.VMSpecInfo{}, errors.New("Failed to Find Product types!!") } - // Name ex) d3530ad2-462b-43ad-97d5-e1087b952b7d!87c0a6f6-c684-4fbe-a393-d8412bcf788d_disk100GB - // Caution : 아래의 string split 기호 중 ! 대신 #을 사용하면 CB-Spider API를 통해 call할 시 전체의 string이 전달되지 않고 # 전까지만 전달됨. - instanceSpecString := strings.Split(VMSpecName, "!") + // specName ex) d3530ad2-462b-43ad-97d5-e1087b952b7d!87c0a6f6-c684-4fbe-a393-d8412bcf788d_disk100GB + // Caution) If you use # instead of ! among the string split symbols below, the entire string is not delivered when calling through the CB-Spider API, but only before #. + instanceSpecString := strings.Split(specName, "!") for i := range instanceSpecString { cblogger.Info("instanceSpecString : ", instanceSpecString[i]) } ktVMSpecId := instanceSpecString[0] - cblogger.Info("vmSpecID : ", ktVMSpecId) + // cblogger.Info("vmSpecID : ", ktVMSpecId) // Ex) 87c0a6f6-c684-4fbe-a393-d8412bcf788d_disk100GB tempOfferingString := instanceSpecString[1] - cblogger.Info("tempOfferingString : ", tempOfferingString) + // cblogger.Info("tempOfferingString : ", tempOfferingString) diskOfferingString := strings.Split(tempOfferingString, "_") ktDiskOfferingId := diskOfferingString[0] - cblogger.Info("ktDiskOfferingId : ", ktDiskOfferingId) + // cblogger.Info("ktDiskOfferingId : ", ktDiskOfferingId) + var resultVMSpecInfo irs.VMSpecInfo for _, productType := range result.Listavailableproducttypesresponse.ProductTypes { cblogger.Info("# Search criteria of Serviceofferingid : ", ktVMSpecId) // if serverProductType.ServiceOfferingId == ktVMSpecId { if productType.ServiceOfferingId == ktVMSpecId { if productType.DiskOfferingId == ktDiskOfferingId { - resultVMSpecInfo = mappingVMSpecInfo(zoneId, "", productType) //Spec 상세 정보 조회시 Image 정보는 불필요 + resultVMSpecInfo, err = vmSpecHandler.mappingVMSpecInfo(&productType) + if err != nil { + newErr := fmt.Errorf("Failed to Map the VMSpec info : [%v]", err) + cblogger.Error(newErr.Error()) + return irs.VMSpecInfo{}, newErr + } break } } @@ -121,38 +99,25 @@ func (vmSpecHandler *KtCloudVMSpecHandler) GetVMSpec(VMSpecName string) (irs.VMS func (vmSpecHandler *KtCloudVMSpecHandler) ListVMSpec() ([]*irs.VMSpecInfo, error) { cblogger.Info("KT Cloud cloud driver: called ListVMSpec()!") - regionInfo := vmSpecHandler.RegionInfo.Region - zoneId := vmSpecHandler.RegionInfo.Zone - - cblogger.Infof("Region : [%s] ", regionInfo) - cblogger.Info("vmSpecHandler.RegionInfo.Zone : ", zoneId) - - // Image(KT Cloud Template) list 조회 (Name) imageHandler := KtCloudImageHandler{ Client: vmSpecHandler.Client, RegionInfo: vmSpecHandler.RegionInfo, //CAUTION!! : Must input this!! } - cblogger.Info("imageHandler.RegionInfo.Zone : ", imageHandler.RegionInfo.Zone) //Need to Check!! imageListResult, err := imageHandler.ListImage() if err != nil { cblogger.Infof("Failed to Get Image list!! : ", err) - return nil, errors.New("Failed to Get Image list!!") - } else { - cblogger.Info("Succeeded in Getting Image list!!") - cblogger.Info("Image list Count : ", len(imageListResult)) - // spew.Dump(imageListResult) } - specListResult, err := vmSpecHandler.Client.ListAvailableProductTypes(zoneId) + specListResult, err := vmSpecHandler.Client.ListAvailableProductTypes(vmSpecHandler.RegionInfo.Zone) if err != nil { cblogger.Error("Failed to Get List of Available Product Types: %s", err) return []*irs.VMSpecInfo{}, errors.New("Failed to Get Product Type list!!") } else { cblogger.Info("Succeeded in Getting Product Type list!!") } - cblogger.Info("Spec list Count : ", len(specListResult.Listavailableproducttypesresponse.ProductTypes)) + cblogger.Infof("### Spec list Count : [%d]", len(specListResult.Listavailableproducttypesresponse.ProductTypes)) // spew.Dump(specListResult) // spew.Dump(result) @@ -160,87 +125,126 @@ func (vmSpecHandler *KtCloudVMSpecHandler) ListVMSpec() ([]*irs.VMSpecInfo, erro return []*irs.VMSpecInfo{}, errors.New("Failed to Find Product types!!") } - var vmSpecInfoList []*irs.VMSpecInfo //Cloud-Barista Spec Info. + vmSpecMap := make(map[string]*irs.VMSpecInfo) for _, image := range imageListResult { - cblogger.Info("# 기준 KT Cloud Image ID(Product type -> Template) : ", image.IId.SystemId) - + cblogger.Infof("# Lookup by KT Cloud Image ID(Product type -> Template) : [%s]", image.IId.SystemId) + for _, productType := range specListResult.Listavailableproducttypesresponse.ProductTypes { - var serverProductType ktsdk.ProductTypes - serverProductType = productType - - if image.IId.SystemId == productType.TemplateId { - - vmSpecInfo := mappingVMSpecInfo(zoneId, image.IId.SystemId, serverProductType) - vmSpecInfoList = append(vmSpecInfoList, &vmSpecInfo) + if strings.EqualFold(image.IId.SystemId, productType.TemplateId) { + vmSpecInfo, err := vmSpecHandler.mappingVMSpecInfo(&productType) + if err != nil { + newErr := fmt.Errorf("Failed to Map the VMSpec info : [%v]", err) + cblogger.Error(newErr.Error()) + return nil, newErr + } + + if existingSpec, exists := vmSpecMap[vmSpecInfo.Name]; exists { + // If the VMSpec already exists, add the image ID to the corresponding images list in KeyValueList + found := false + for i, kv := range existingSpec.KeyValueList { + if kv.Key == "CorrespondingImageIds" { + imageIds := strings.Split(kv.Value, ",") + for _, id := range imageIds { + if id == image.IId.SystemId { + found = true + break + } + } + if !found { + existingSpec.KeyValueList[i].Value += "," + image.IId.SystemId + } + break + } + } + // if !found { + // existingSpec.KeyValueList = append(existingSpec.KeyValueList, irs.KeyValue{ + // Key: "CorrespondingImageIds", + // Value: image.IId.SystemId, + // }) + // } + } else { + // If the VMSpec is new, add it to the map and initialize the corresponding images list in KeyValueList + vmSpecInfo.KeyValueList = append(vmSpecInfo.KeyValueList, irs.KeyValue{ + Key: "CorrespondingImageIds", + Value: image.IId.SystemId, + }) + vmSpecMap[vmSpecInfo.Name] = &vmSpecInfo + } + time.Sleep(50 * time.Millisecond) + // To prvent error : "Unable to execute API command listAvailableProductTypes due to ratelimit timeout" } } + } + + var vmSpecInfoList []*irs.VMSpecInfo + for _, specInfo := range vmSpecMap { + vmSpecInfoList = append(vmSpecInfoList, specInfo) } - cblogger.Info("# Supported VM Spec count : ", len(vmSpecInfoList)) + // cblogger.Info("# Supported VM Spec count : ", len(vmSpecInfoList)) return vmSpecInfoList, nil } //Extract instance Specification information -func mappingVMSpecInfo(ZoneId string, ImageId string, ktServerProductType ktsdk.ProductTypes) irs.VMSpecInfo { - cblogger.Infof("\n*** Mapping VMSpecInfo : SpecName: [%s]", ktServerProductType.ServiceOfferingId) +func (vmSpecHandler *KtCloudVMSpecHandler) mappingVMSpecInfo(productType *ktsdk.ProductTypes) (irs.VMSpecInfo, error) { + // cblogger.Infof("\n*** Mapping VMSpecInfo : SpecName: [%s]", productType.ServiceOfferingId) // spew.Dump(vmSpec) - // Caution : 아래의 string split 기호 중 ! 대신 #을 사용하면 CB-Spider API를 통해 call할 시 전체의 string이 전달되지 않고 # 전까지만 전달됨. - ktVMSpecId := ktServerProductType.ServiceOfferingId + "!" + ktServerProductType.DiskOfferingId + "_disk" + ktServerProductType.DiskOfferingDesc - ktVMSpecString := ktServerProductType.ServiceOfferingDesc - // ex) ktServerProductType.Serviceofferingdesc => "XS71 12vCore 16GB" //Caution!! + // Caution: If you use # instead of ! as the string split symbol below, the entire string will not be delivered through the CB-Spider API, only up to the #. + ktVMSpecId := productType.ServiceOfferingId + "!" + productType.DiskOfferingId + "_disk" + productType.DiskOfferingDesc + ktVMSpecString := productType.ServiceOfferingDesc + // ex) productType.Serviceofferingdesc => "XS71 12vCore 16GB" //Caution!! // vCpuCount := strtmp[5:7] // productMem := strtmp[13:15] - // Split 함수로 문자열을 " "(공백) 기준으로 분리 + // Split the string based on " " (space) using the Split function specSlice := strings.Split(ktVMSpecString, " ") - for _, str := range specSlice { - cblogger.Infof("Splited string : [%s]", str) - } + // for _, str := range specSlice { + // cblogger.Infof("Splitted string : [%s]", str) + // } - // KT Cloud에서 core 수를 '4vcore' or '16vCore'와 같은 형태로 제공함.(String 처리시 자리수, 대수문자 주의 필요) - // vCpuCount := productVCpu[0:2] // 24vCore는 괜찮지만, 1vCore 같은 경우도 있어서 전체 자리수가 달라 적당하지 않음 + // KT Cloud provides the number of cores in the form of '4vcore' or '16vCore'. (Be careful with string processing, number of digits, and case sensitivity) + // vCpuCount := productVCpu[0:2] // 24vCore is fine, but 1vCore has a different total number of digits, so it's not appropriate - productVCpu := strings.Replace(specSlice[1], "C", "c", 1) //대문자 C가 있으면 소문자 c로 변경 ex) 1vCore -> 1vcore - productVCpu = strings.TrimSuffix(productVCpu, "vcore") // 문자열 우측에서 'vcore'를 제거 - productMem := strings.TrimRight(specSlice[2], "GB") // 문자열 우측에서 G와 B를 제거 + productVCpu := strings.Replace(specSlice[1], "C", "c", 1) // If there is an uppercase C, change it to lowercase c ex) 1vCore -> 1vcore + productVCpu = strings.TrimSuffix(productVCpu, "vcore") // Remove 'vcore' from the right side of the string + productMem := strings.TrimRight(specSlice[2], "GB") // Remove G and B from the right side of the string //productMem := strings.TrimSuffix(specSlice[2], "GB") - MemCountGb, err := strconv.Atoi(productMem) // 문자열을 숫자으로 변환 + MemCountGb, err := strconv.Atoi(productMem) // Convert string to number if err != nil { cblogger.Error(err) } - - MemCountMb := MemCountGb*1024 - MemCountMbStr := strconv.Itoa(MemCountMb) // 숫자를 문자열로 변환 + MemCountMbStr := strconv.Itoa(MemCountGb * 1024) // Convert number to string - // In case : ktServerProductType.DiskOfferingDesc : 100G (After Disk Add) - // if len(ktServerProductType.DiskOfferingId) > 1 { - // ktVMSpecID = ktServerProductType.ServiceOfferingId + "#" + ktServerProductType.DiskOfferingId + "_disk" + ktServerProductType.DiskOfferingDesc + // ### Note!!) If the diskofferingid value exists, additional data disks are created.(=> So to get the 'Correct RootDiskSize') + diskSize, err := vmSpecHandler.getRootDiskSize(&productType.DiskOfferingDesc, &productType.TemplateId, &productType.DiskOfferingId) + if err != nil { + newErr := fmt.Errorf("Failed to Get the Root Disk Size of the VMSpec: [%v]", err) + cblogger.Error(newErr.Error()) + return irs.VMSpecInfo{}, newErr + } + // In case: productType.DiskOfferingDesc : 100G (After Disk Add) + // if len(productType.DiskOfferingId) > 1 { + // ktVMSpecID = productType.ServiceOfferingId + "#" + productType.DiskOfferingId + "_disk" + productType.DiskOfferingDesc // } vmSpecInfo := irs.VMSpecInfo{ - Region: ktServerProductType.ZoneDesc, - // Region: ktServerProductType.ZoneId, + Region: productType.ZoneDesc, Name: ktVMSpecId, - VCpu: irs.VCpuInfo{Count: productVCpu, Clock: "N/A"}, - // VCpu: irs.VCpuInfo{Count: vCpuCount, Clock: "N/A"}, - Mem: MemCountMbStr, - // Mem: productMem, + VCpu: irs.VCpuInfo{Count: productVCpu, Clock: "-1"}, + Mem: MemCountMbStr, + Disk: diskSize, + Gpu: []irs.GpuInfo{{Count: "-1", Mfr: "NA", Model: "NA", Mem: "-1"}}, - // GPU 정보는 없음. - Gpu: []irs.GpuInfo{{Count: "N/A", Mfr: "N/A", Model: "N/A", Mem: "N/A"}}, - - // KT Cloud는 Zone별로 지원 Spec이 다르므로 Zone 정보도 제시 + // Since KT Cloud supports different specs for each zone, the zone information is also provided. KeyValueList: []irs.KeyValue{ - {Key: "Zone", Value: ktServerProductType.ZoneDesc}, - {Key: "KtServiceOffering", Value: ktServerProductType.ServiceOfferingDesc}, - {Key: "DiskSize", Value: ktServerProductType.DiskOfferingDesc}, - //{Key: "AdditionalDiskOfferingID", Value: ktServerProductType.DiskOfferingId}, - {Key: "SupportingImage(Template)ID", Value: ImageId}, - {Key: "ProductState", Value: ktServerProductType.ProductState}, + {Key: "KtServiceOffering", Value: productType.ServiceOfferingDesc}, + {Key: "TotalDiskSize(includeDataDisk)", Value: productType.DiskOfferingDesc}, + {Key: "ProductState", Value: productType.ProductState}, + {Key: "Zone", Value: productType.ZoneDesc}, }, } - return vmSpecInfo + return vmSpecInfo, nil } func (vmSpecHandler *KtCloudVMSpecHandler) ListOrgVMSpec() (string, error) { @@ -279,3 +283,61 @@ func (vmSpecHandler *KtCloudVMSpecHandler) GetOrgVMSpec(Name string) (string, er } return jsonString, errJson } + +// ### Note!!) If the diskofferingid value exists, additional data disks are created.(=> So to get the 'Correct RootDiskSize') +func (vmSpecHandler *KtCloudVMSpecHandler) getRootDiskSize(diskOfferingDesc *string, templateId *string, diskOfferingId *string) (string, error) { + cblogger.Info("KT Cloud cloud driver: called getRootDiskSize()!") + + if strings.EqualFold(*diskOfferingDesc, "") { + newErr := fmt.Errorf("Invalid diskOfferingDesc value!!") + cblogger.Error(newErr.Error()) + return "", newErr + } + + if strings.EqualFold(*templateId, "") { + newErr := fmt.Errorf("Invalid templateId value!!") + cblogger.Error(newErr.Error()) + return "", newErr + } + + // diskOfferingDesc Ex : "100GB" + re := regexp.MustCompile(`(\d+)GB`) + matches := re.FindStringSubmatch(*diskOfferingDesc) // Find the match + + var offeringDiskSizeStr string + if len(matches) > 1 { + offeringDiskSizeStr = matches[1] // Extract only the numeric part. Ex : "100" + } + if strings.EqualFold(offeringDiskSizeStr, "") { + return "-1", nil + } + + if !strings.EqualFold(*diskOfferingId, "") { + imageHandler := KtCloudImageHandler{ + RegionInfo: vmSpecHandler.RegionInfo, + Client: vmSpecHandler.Client, + } + imageInfo, err := imageHandler.GetImage(irs.IID{SystemId: *templateId}) + if err != nil { + newErr := fmt.Errorf("Failed to Get the VM Image Info : [%v]", err) + return "", newErr + } + + offeringDiskSizeInt, err := strconv.Atoi(offeringDiskSizeStr) + if err != nil { + newErr := fmt.Errorf("Failed to Convert the string to number : [%v]", err) + return "", newErr + } + osDiskSizeInGBInt, err := strconv.Atoi(imageInfo.OSDiskSizeInGB) + if err != nil { + newErr := fmt.Errorf("Failed to Convert the string to number : [%v]", err) + return "", newErr + } + // cblogger.Infof("offeringDiskSizeInt : [%d] / osDiskSizeInGBInt : [%d]", offeringDiskSizeInt, osDiskSizeInGBInt) + + rootDiskSizeInt := offeringDiskSizeInt - osDiskSizeInGBInt + return strconv.Itoa(rootDiskSizeInt), nil // rootDiskSizeStr Ex : "50" + } else { + return offeringDiskSizeStr, nil + } +} diff --git a/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/VMSpecHandler.go b/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/VMSpecHandler.go index b36bc43d4..84c71f1c3 100644 --- a/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/VMSpecHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/VMSpecHandler.go @@ -14,7 +14,7 @@ import ( "fmt" "strconv" "strings" - // "github.com/davecgh/go-spew/spew" + "github.com/davecgh/go-spew/spew" ktvpcsdk "github.com/cloud-barista/ktcloudvpc-sdk-go" "github.com/cloud-barista/ktcloudvpc-sdk-go/openstack/compute/v2/flavors" @@ -174,19 +174,22 @@ func (vmSpecHandler *KTVpcVMSpecHandler) getVMSpecIdWithName(specName string) (s func (vmSpecHandler *KTVpcVMSpecHandler) mappingVMSpecInfo(flavor *flavors.Flavor) *irs.VMSpecInfo { cblogger.Info("KT Cloud VPC Driver: called mappingVMSpecInfo()!") - // spew.Dump(vmSpec) + cblogger.Info("\n\n### flavor : ") + spew.Dump(flavor) + vmSpecInfo := irs.VMSpecInfo { Region: vmSpecHandler.RegionInfo.Zone, Name: flavor.Name, - VCpu: irs.VCpuInfo{Count: strconv.Itoa(flavor.VCPUs), }, + VCpu: irs.VCpuInfo{Count: strconv.Itoa(flavor.VCPUs), Clock: "-1"}, Mem: strconv.Itoa(flavor.RAM), - // Gpu: []irs.GpuInfo{{Count: "N/A", Mfr: "N/A", Model: "N/A", Mem: "N/A"}}, + Gpu: []irs.GpuInfo{{Count: "-1", Mfr: "NA", Model: "NA", Mem: "-1"}}, + Disk: "NA", KeyValueList: []irs.KeyValue{ {Key: "Zone", Value: vmSpecHandler.RegionInfo.Zone}, - {Key: "RootDiskSize(GB)", Value: strconv.Itoa(flavor.Disk)}, - {Key: "EphemeralDiskSize(GB)", Value: strconv.Itoa(flavor.Ephemeral)}, - {Key: "SwapDiskSize(MB)", Value: strconv.Itoa(flavor.Swap)}, + // {Key: "RootDiskSize(GB)", Value: strconv.Itoa(flavor.Disk)}, + // {Key: "EphemeralDiskSize(GB)", Value: strconv.Itoa(flavor.Ephemeral)}, + // {Key: "SwapDiskSize(MB)", Value: strconv.Itoa(flavor.Swap)}, {Key: "IsPublic", Value: strconv.FormatBool(flavor.IsPublic)}, {Key: "VMSpecID", Value: flavor.ID}, }, From d8f23514ca6414e301d49d1c3b2189c9c3043d89 Mon Sep 17 00:00:00 2001 From: innodreamer Date: Fri, 7 Feb 2025 22:56:42 +0900 Subject: [PATCH 3/5] [KT Cloud] Enhance Supported RegionZone Info --- .../drivers/ktcloud/resources/RegionZoneHandler.go | 8 ++++++-- .../drivers/ktcloudvpc/resources/RegionZoneHandler.go | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/RegionZoneHandler.go b/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/RegionZoneHandler.go index 8d83d179b..8246b591e 100644 --- a/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/RegionZoneHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/RegionZoneHandler.go @@ -4,6 +4,7 @@ // KT Cloud RegionZone Handler // // Created by ETRI, 2023.10. +// Updated by ETRI, 2025.02. //================================================================================================== package resources @@ -39,10 +40,10 @@ type KtRegionInfo struct { func getSupportedRegions() []KtRegionInfo { regionInfoList := []KtRegionInfo { { RegionCode: "KOR-Seoul", - RegionName: "서울", + RegionName: "Seoul", }, { RegionCode: "KOR-Central", - RegionName: "천안", + RegionName: "Cheonan", }, } return regionInfoList @@ -78,6 +79,7 @@ func (regionZoneHandler *KtCloudRegionZoneHandler) ListRegionZone() ([]*irs.Regi regionZoneInfo := irs.RegionZoneInfo{ Name: region.RegionCode, DisplayName: region.RegionName, + CSPDisplayName: region.RegionName, } zoneInfoList, err := regionZoneHandler.getZoneInfoList(region.RegionCode) @@ -129,6 +131,7 @@ func (regionZoneHandler KtCloudRegionZoneHandler) GetRegionZone(regionCode strin regionZoneInfo = irs.RegionZoneInfo { Name: region.RegionCode, DisplayName: region.RegionName, + CSPDisplayName: region.RegionName, } break } @@ -235,6 +238,7 @@ func (regionZoneHandler KtCloudRegionZoneHandler) getZoneInfoList(regionCode str zoneInfo := irs.ZoneInfo{ Name: zone.ID, DisplayName: zone.Name, + CSPDisplayName: zone.Name, } if strings.EqualFold(zone.AllocationState, "Enabled") { zoneInfo.Status = irs.ZoneAvailable diff --git a/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/RegionZoneHandler.go b/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/RegionZoneHandler.go index 527b30a4a..d340e37a0 100644 --- a/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/RegionZoneHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/RegionZoneHandler.go @@ -4,6 +4,7 @@ // KT Cloud VPC RegionZone Handler // // Created by ETRI, 2023.10. +// Updated by ETRI, 2025.02. //================================================================================================== package resources @@ -33,10 +34,10 @@ type KtVpcRegion struct { func getSupportedRegionZones() []KtVpcRegion { regionList := []KtVpcRegion { { RegionCode: "KR1", - RegionName: "서울", + RegionName: "Seoul", ZoneList: []KtVpcZone { { ZoneCode: "DX-M1", - ZoneName: "목동-1", + ZoneName: "Mokdong-1", }, }, }, @@ -66,6 +67,7 @@ func (regionZoneHandler *KTVpcRegionZoneHandler) ListRegionZone() ([]*irs.Region regionZoneInfo := irs.RegionZoneInfo{ Name: region.RegionCode, DisplayName: region.RegionName, + CSPDisplayName: region.RegionName, } zoneInfoList, err := regionZoneHandler.getZoneInfoList(region.RegionCode) @@ -105,6 +107,7 @@ func (regionZoneHandler KTVpcRegionZoneHandler) GetRegionZone(regionCode string) regionZoneInfo = irs.RegionZoneInfo { Name: region.RegionCode, DisplayName: region.RegionName, + CSPDisplayName: region.RegionName, } } } @@ -240,6 +243,7 @@ func (regionZoneHandler KTVpcRegionZoneHandler) getZoneInfoList(regionCode strin zoneInfo := irs.ZoneInfo{ Name: zone.ZoneCode, DisplayName: zone.ZoneName, + CSPDisplayName: zone.ZoneName, Status: irs.NotSupported, } zoneInfoList = append(zoneInfoList, zoneInfo) From c4c653b9e4507b55d1e978fc96fb64f8c02235fc Mon Sep 17 00:00:00 2001 From: innodreamer Date: Fri, 7 Feb 2025 22:57:27 +0900 Subject: [PATCH 4/5] [KT Cloud] Enhance Supported VM Image Info --- .../drivers/ktcloud/resources/ImageHandler.go | 140 ++++++++++++------ .../ktcloudvpc/resources/ImageHandler.go | 55 +++++-- 2 files changed, 135 insertions(+), 60 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/ImageHandler.go b/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/ImageHandler.go index 9d8472666..f72505574 100644 --- a/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/ImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/ImageHandler.go @@ -7,6 +7,7 @@ // KT Cloud Image Handler // // by ETRI, 2021.05. +// Updated by ETRI, 2025.02. package resources @@ -14,8 +15,8 @@ import ( "errors" "fmt" "strings" - - //"github.com/davecgh/go-spew/spew" + "regexp" + // "github.com/davecgh/go-spew/spew" ktsdk "github.com/cloud-barista/ktcloud-sdk-go" @@ -36,9 +37,10 @@ func init() { } // 'Image' in KT Cloud API manual means an image created for Volume or Snapshot of VM stopped state. - func (imageHandler *KtCloudImageHandler) GetImage(imageIID irs.IID) (irs.ImageInfo, error) { cblogger.Info("KT Cloud cloud driver: called GetImage()!!") + // cblogger.Infof("KT Cloud image ID(Templateid) : [%s]", imageIID.SystemId) + // cblogger.Info("imageHandler.RegionInfo.Zone : ", zoneId) if strings.EqualFold(imageIID.SystemId, "") { newErr := fmt.Errorf("Invalid Image SystemId!!") @@ -46,45 +48,26 @@ func (imageHandler *KtCloudImageHandler) GetImage(imageIID irs.IID) (irs.ImageIn return irs.ImageInfo{}, newErr } - var resultImageInfo irs.ImageInfo - zoneId := imageHandler.RegionInfo.Zone - - cblogger.Infof("KT Cloud image ID(Templateid) : [%s]", imageIID.SystemId) - cblogger.Info("imageHandler.RegionInfo.Zone : ", zoneId) - - // Caution!! : KT Cloud searches by 'zoneId' when searching Image info/VMSpc info. - result, err := imageHandler.Client.ListAvailableProductTypes(zoneId) + // Note!!) Use ListImage() to search within the organized image list information + imageListResult, err := imageHandler.ListImage() if err != nil { - cblogger.Error("Failed to Get List of Available Product Types: %s", err) - return irs.ImageInfo{}, err - } - - if len(result.Listavailableproducttypesresponse.ProductTypes) < 1 { - return irs.ImageInfo{}, errors.New("Failed to Get Any Product Types!!") + newErr := fmt.Errorf("Failed to Get the VMSpec info list!! : [%v]", err) + cblogger.Error(newErr.Error()) + return irs.ImageInfo{}, newErr } - var foundImgId string - for _, productType := range result.Listavailableproducttypesresponse.ProductTypes { - // cblogger.Info("# Search criteria of Image Template ID : ", imageIID.SystemId) - if strings.EqualFold(productType.TemplateId, imageIID.SystemId) { - foundImgId = productType.TemplateId - resultImageInfo = mappingImageInfo(productType) - break + for _, image := range imageListResult { + if strings.EqualFold(image.Name, imageIID.SystemId) { + return *image, nil } } - if strings.EqualFold(foundImgId, "") { - return irs.ImageInfo{}, fmt.Errorf("Failed to Find Any Image(Template) info with the ID.") - } - return resultImageInfo, nil + return irs.ImageInfo{}, errors.New("Failed to find the VM Image info : '" + imageIID.SystemId) } func (imageHandler *KtCloudImageHandler) ListImage() ([]*irs.ImageInfo, error) { cblogger.Info("KT Cloud cloud driver: called ListImage()!") - - var vmImageList []*irs.ImageInfo - zoneId := imageHandler.RegionInfo.Zone - result, err := imageHandler.Client.ListAvailableProductTypes(zoneId) + result, err := imageHandler.Client.ListAvailableProductTypes(imageHandler.RegionInfo.Zone) if err != nil { cblogger.Error("Failed to Get List of Available Product Types: %s", err) return []*irs.ImageInfo{}, err @@ -94,35 +77,79 @@ func (imageHandler *KtCloudImageHandler) ListImage() ([]*irs.ImageInfo, error) { if len(result.Listavailableproducttypesresponse.ProductTypes) < 1 { return []*irs.ImageInfo{}, errors.New("Failed to Find Product Types!!") } - + // ### In order to remove the list of identical duplicates over and over again - tempID := "" + var vmImageInfoMap = make(map[string]*irs.ImageInfo) // Map to track unique VMSpec Info. for _, productType := range result.Listavailableproducttypesresponse.ProductTypes { - // if (tempID == "") || (productType.Templateid != tempID) { - if productType.TemplateId != tempID { - imageInfo := mappingImageInfo(productType) - vmImageList = append(vmImageList, &imageInfo) - - tempID = productType.TemplateId - // cblogger.Infof("\nImage Template Id : " + tempID) + // ### Caution!!) If the diskofferingid value exists, additional data disks are created.(=> So Not include to image list for 'Correct RootDiskSize') + if strings.EqualFold(productType.DiskOfferingId, "") { + imageInfo := mappingImageInfo(&productType) + if _, exists := vmImageInfoMap[imageInfo.Name]; exists { + // break + } else { + vmImageInfoMap[imageInfo.Name] = &imageInfo + } } } - cblogger.Info("# Supported Image Product Count : ", len(vmImageList)) - return vmImageList, nil + + // Convert the map to a list + var vmImageInfoList []*irs.ImageInfo + for _, imageInfo := range vmImageInfoMap { + vmImageInfoList = append(vmImageInfoList, imageInfo) + } + // cblogger.Info("# Supported Image Product Count : ", len(vmImageInfoList)) + return vmImageInfoList, nil } -func mappingImageInfo(ktServerProductType ktsdk.ProductTypes) irs.ImageInfo { - cblogger.Info("KT Cloud Cloud Driver: called mappingImageInfo()!") +func mappingImageInfo(productType *ktsdk.ProductTypes) irs.ImageInfo { + // cblogger.Info("KT Cloud Cloud Driver: called mappingImageInfo()!") + // cblogger.Info("\n\n### productType : ") + // spew.Dump(productType) + // cblogger.Info("\n") + + var osPlatform irs.OSPlatform + if productType.TemplateDesc != "" { + if strings.Contains(productType.TemplateDesc, "WIN ") { + osPlatform = irs.Windows + } else { + osPlatform = irs.Linux_UNIX + } + } else { + osPlatform = irs.PlatformNA + } + + var imageStatus irs.ImageStatus + if productType.ProductState != "" { + if strings.EqualFold(productType.ProductState, "available") { + imageStatus = irs.ImageAvailable + } else { + imageStatus = irs.ImageUnavailable + } + } else { + imageStatus = irs.ImageNA + } + + diskSize := getImageDiskSize(productType.DiskOfferingDesc) + imageInfo := irs.ImageInfo{ // NOTE!! : TemplateId -> Image Name (TemplateId as Image Name) - IId: irs.IID{ktServerProductType.TemplateId, ktServerProductType.TemplateId}, - GuestOS: ktServerProductType.TemplateDesc, - Status: ktServerProductType.ProductState, + IId: irs.IID{NameId: productType.TemplateId, SystemId: productType.TemplateId}, + GuestOS: productType.TemplateDesc, + Status: productType.ProductState, + + Name: productType.TemplateId, + OSArchitecture: irs.ArchitectureNA, + OSPlatform: osPlatform, + OSDistribution: productType.TemplateDesc, + OSDiskType: "NA", + OSDiskSizeInGB: diskSize, + ImageStatus: imageStatus, } // Since KT Cloud has different supported images for each zone, zone information is also presented. keyValueList := []irs.KeyValue{ - {Key: "Zone", Value: ktServerProductType.ZoneDesc}, + {Key: "ProductType", Value: productType.Product}, + {Key: "Zone", Value: productType.ZoneDesc}, } imageInfo.KeyValueList = keyValueList return imageInfo @@ -199,3 +226,18 @@ func (imageHandler *KtCloudImageHandler) getKTProductType(imageIID irs.IID) (*kt } return nil, nil } + +func getImageDiskSize(sizeGB string) (string) { + // sizeGB Ex : "100GB" + re := regexp.MustCompile(`(\d+)GB`) + matches := re.FindStringSubmatch(sizeGB) // Find the match + + var diskSize string + if len(matches) > 1 { + diskSize = matches[1] // Extract only the numeric part + } + if strings.EqualFold(diskSize, "") { + diskSize = "-1" + } + return diskSize // diskSize Ex : "100" +} diff --git a/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/ImageHandler.go b/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/ImageHandler.go index 2d4b9e20c..10d8e9689 100644 --- a/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/ImageHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ktcloudvpc/resources/ImageHandler.go @@ -13,6 +13,7 @@ package resources import ( "strings" "fmt" + // "strconv" // "github.com/davecgh/go-spew/spew" ktvpcsdk "github.com/cloud-barista/ktcloudvpc-sdk-go" @@ -50,8 +51,6 @@ func (imageHandler *KTVpcImageHandler) ListImage() ([]*irs.ImageInfo, error) { return nil, newErr } loggingInfo(callLogInfo, start) - - // # To Check!! // cblogger.Info("### allPages : ") // spew.Dump(allPages) @@ -62,14 +61,12 @@ func (imageHandler *KTVpcImageHandler) ListImage() ([]*irs.ImageInfo, error) { loggingError(callLogInfo, newErr) return nil, newErr } - - // # To Check!! // cblogger.Info("### imageList : ") // spew.Dump(imageList) var imageInfoList []*irs.ImageInfo - for _, vmImage := range imageList { - imageInfo, err := imageHandler.mappingImageInfo(vmImage) + for _, image := range imageList { + imageInfo, err := imageHandler.mappingImageInfo(&image) if err != nil { newErr := fmt.Errorf("Failed to Map KT Cloud VPC Image Info. [%v]", err) cblogger.Error(newErr.Error()) @@ -105,7 +102,7 @@ func (imageHandler *KTVpcImageHandler) GetImage(imageIID irs.IID) (irs.ImageInfo //Ref) 'Image API' return struct of image :ktcloudvpc-sdk-go/openstack/imageservice/v2/images/results.go //Ref) 'Compute API' return struct of image : ktcloudvpc-sdk-go/openstack/compute/v2/images/results.go - imageInfo, err := imageHandler.mappingImageInfo(*ktImage) + imageInfo, err := imageHandler.mappingImageInfo(ktImage) if err != nil { cblogger.Error(err.Error()) loggingError(callLogInfo, err) @@ -153,9 +150,11 @@ func (imageHandler *KTVpcImageHandler) DeleteImage(imageIID irs.IID) (bool, erro return true, fmt.Errorf("Does not support DeleteImage() yet!!") } -func (imageHandler *KTVpcImageHandler) mappingImageInfo(image images.Image) (*irs.ImageInfo, error) { +func (imageHandler *KTVpcImageHandler) mappingImageInfo(image *images.Image) (*irs.ImageInfo, error) { cblogger.Info("KT Cloud VPC Driver: called mappingImageInfo()!") + // cblogger.Info("\n\n### image : ") // spew.Dump(image) + // cblogger.Info("\n") //Ref) 'Image API' return struct of image :ktcloudvpc-sdk-go/openstack/imageservice/v2/images/results.go //Ref) 'Compute API' return struct of image : ktcloudvpc-sdk-go/openstack/compute/v2/images/results.go @@ -171,6 +170,32 @@ func (imageHandler *KTVpcImageHandler) mappingImageInfo(image images.Image) (*ir imgAvailability = "unavailable" } + var osPlatform irs.OSPlatform + if image.Name != "" { + if strings.Contains(image.Name, "Windows") || strings.Contains(image.Name, "windows") || strings.Contains(image.Name, "win") { + osPlatform = irs.Windows + } else { + osPlatform = irs.Linux_UNIX + } + } else { + osPlatform = irs.PlatformNA + } + + var imageStatus irs.ImageStatus + if image.Status != "" { + if strings.EqualFold(string(image.Status), "active") { + imageStatus = irs.ImageAvailable + } else { + imageStatus = irs.ImageUnavailable + } + } else { + imageStatus = irs.ImageNA + } + + // # Note) image.SizeBytes is not Root Disk Size + // valueInGB := float64(image.SizeBytes) / (1024 * 1024 * 1024) + // diskSizeInGB := strconv.FormatFloat(valueInGB, 'f', 0, 64) + imageInfo := &irs.ImageInfo { IId: irs.IID{ NameId: image.ID, // Caution!! @@ -178,13 +203,21 @@ func (imageHandler *KTVpcImageHandler) mappingImageInfo(image images.Image) (*ir }, GuestOS: image.Name, // Caution!! Status: imgAvailability, + + Name: image.ID, + OSArchitecture: "NA", + OSPlatform: osPlatform, + OSDistribution: image.Name, + OSDiskType: "NA", + OSDiskSizeInGB: "NA", + ImageStatus: imageStatus, } keyValueList := []irs.KeyValue{ - {Key: "Zone", Value: imageHandler.RegionInfo.Zone}, - {Key: "DiskFormat:", Value: string(image.DiskFormat)}, + {Key: "Zone", Value: imageHandler.RegionInfo.Zone}, + {Key: "DiskFormat:", Value: string(image.DiskFormat)}, {Key: "ContainerFormat:", Value: string(image.ContainerFormat)}, - {Key: "Visibility:", Value: string(image.Visibility)}, + {Key: "Visibility:", Value: string(image.Visibility)}, } imageInfo.KeyValueList = keyValueList From 0a3aca10dd7b5f04a2dac324da8a17adb624e2da Mon Sep 17 00:00:00 2001 From: innodreamer Date: Fri, 7 Feb 2025 23:26:26 +0900 Subject: [PATCH 5/5] [KT Cloud Classic] Update GetVMSpec() to include 'CorrespondingImageIds' parameter --- .../ktcloud/resources/VMSpecHandler.go | 55 +++++-------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/VMSpecHandler.go b/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/VMSpecHandler.go index ee7734c8b..560ea2ef1 100644 --- a/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/VMSpecHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ktcloud/resources/VMSpecHandler.go @@ -47,53 +47,26 @@ func (vmSpecHandler *KtCloudVMSpecHandler) GetVMSpec(specName string) (irs.VMSpe cblogger.Info("KT Cloud cloud driver: called GetVMSpec()!") // Caution!! : KT Cloud doesn't support 'Region' officially, so we use 'Zone info.' which is from the connection info. - // Caution!! : When searching for Image info/VMSpc info, KT Cloud inquires using Zoneid. - result, err := vmSpecHandler.Client.ListAvailableProductTypes(vmSpecHandler.RegionInfo.Zone) - if err != nil { - cblogger.Error("Failed to Get List of Available Product Types: %s", err) - return irs.VMSpecInfo{}, err - } - - if len(result.Listavailableproducttypesresponse.ProductTypes) < 1 { - return irs.VMSpecInfo{}, errors.New("Failed to Find Product types!!") + if strings.EqualFold(specName, "") { + newErr := fmt.Errorf("Invalid specName!!") + cblogger.Error(newErr.Error()) + return irs.VMSpecInfo{}, newErr } - // specName ex) d3530ad2-462b-43ad-97d5-e1087b952b7d!87c0a6f6-c684-4fbe-a393-d8412bcf788d_disk100GB - // Caution) If you use # instead of ! among the string split symbols below, the entire string is not delivered when calling through the CB-Spider API, but only before #. - instanceSpecString := strings.Split(specName, "!") - for i := range instanceSpecString { - cblogger.Info("instanceSpecString : ", instanceSpecString[i]) + // Note!!) Use ListVMSpec() to include 'CorrespondingImageIds' parameter. + specListResult, err := vmSpecHandler.ListVMSpec() + if err != nil { + newErr := fmt.Errorf("Failed to Get the VMSpec info list!! : [%v]", err) + cblogger.Error(newErr.Error()) + return irs.VMSpecInfo{}, newErr } - ktVMSpecId := instanceSpecString[0] - // cblogger.Info("vmSpecID : ", ktVMSpecId) - - // Ex) 87c0a6f6-c684-4fbe-a393-d8412bcf788d_disk100GB - tempOfferingString := instanceSpecString[1] - // cblogger.Info("tempOfferingString : ", tempOfferingString) - - diskOfferingString := strings.Split(tempOfferingString, "_") - - ktDiskOfferingId := diskOfferingString[0] - // cblogger.Info("ktDiskOfferingId : ", ktDiskOfferingId) - - var resultVMSpecInfo irs.VMSpecInfo - for _, productType := range result.Listavailableproducttypesresponse.ProductTypes { - cblogger.Info("# Search criteria of Serviceofferingid : ", ktVMSpecId) - // if serverProductType.ServiceOfferingId == ktVMSpecId { - if productType.ServiceOfferingId == ktVMSpecId { - if productType.DiskOfferingId == ktDiskOfferingId { - resultVMSpecInfo, err = vmSpecHandler.mappingVMSpecInfo(&productType) - if err != nil { - newErr := fmt.Errorf("Failed to Map the VMSpec info : [%v]", err) - cblogger.Error(newErr.Error()) - return irs.VMSpecInfo{}, newErr - } - break - } + for _, spec := range specListResult { + if strings.EqualFold(spec.Name, specName) { + return *spec, nil } } - return resultVMSpecInfo, nil + return irs.VMSpecInfo{}, errors.New("Failed to find the VMSpec info : '" + specName) } func (vmSpecHandler *KtCloudVMSpecHandler) ListVMSpec() ([]*irs.VMSpecInfo, error) {