diff --git a/.gitignore b/.gitignore index 27a6a82..cc0ea5f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,9 @@ conf container-volume ./bin/** -config.yaml \ No newline at end of file +config.yaml + +# SSH keys (test artifacts) +id_rsa +id_rsa.pub +*.pem \ No newline at end of file diff --git a/internal/core/cost/repository.go b/internal/core/cost/repository.go index 48dacb2..f923e3b 100644 --- a/internal/core/cost/repository.go +++ b/internal/core/cost/repository.go @@ -144,7 +144,7 @@ func (r *CostRepository) GetMatchingEstimateCostTx(ctx context.Context, param Re err := r.execInTransaction(ctx, func(d *gorm.DB) error { q := d.Model(&EstimateCostInfo{}). Where( - "LOWER(provider_name) = ? AND LOWER(region_name) = ? AND instance_type = ? AND price_policy = ? AND last_updated_at >= ?", + "LOWER(provider_name) = ? AND LOWER(region_name) = ? AND LOWER(instance_type) = ? AND price_policy = ? AND last_updated_at >= ?", strings.ToLower(param.ProviderName), strings.ToLower(param.RegionName), strings.ToLower(param.InstanceType), diff --git a/internal/core/cost/service.go b/internal/core/cost/service.go index 08150de..42b7279 100644 --- a/internal/core/cost/service.go +++ b/internal/core/cost/service.go @@ -93,7 +93,7 @@ func (c *CostService) UpdateAndGetEstimateCost(param UpdateAndGetEstimateCostPar var err error var estimateCostInfos EstimateCostInfos - possibleFetch := true + possibleIbmOrAzureFetch := false if p.ProviderName == "ibm" || p.ProviderName == "azure" { r, err := c.costRepo.GetMatchingEstimateCostWithoutTypeTx(ctx, v, param.TimeStandard, param.PricePolicy) @@ -124,7 +124,7 @@ func (c *CostService) UpdateAndGetEstimateCost(param UpdateAndGetEstimateCostPar if len(temp) > 0 { estimateCostInfos = temp } else { - possibleFetch = false + possibleIbmOrAzureFetch = true } } } else if strings.Contains(p.ProviderName, "ncp") { @@ -139,7 +139,7 @@ func (c *CostService) UpdateAndGetEstimateCost(param UpdateAndGetEstimateCostPar return } - if len(estimateCostInfos) == 0 || possibleFetch { + if len(estimateCostInfos) == 0 || possibleIbmOrAzureFetch { log.Info().Msgf("No matching estimate cost found from database for spec: %+v, fetching from price collector", p) resList, err := c.priceCollector.FetchPriceInfos(ctx, p) diff --git a/internal/core/load/load_generator_install_service.go b/internal/core/load/load_generator_install_service.go index e0ceeeb..ea05c5f 100644 --- a/internal/core/load/load_generator_install_service.go +++ b/internal/core/load/load_generator_install_service.go @@ -689,16 +689,24 @@ func (l *LoadService) getFallbackImage(connectionName string) (string, error) { } // selectBestImage selects the most appropriate image from the search results +// v0.12.1: OSType 필드를 활용하여 더 정확한 이미지 선택 func (l *LoadService) selectBestImage(images []tumblebug.ImageInfo, preferredOS string) tumblebug.ImageInfo { // 우선순위별로 이미지 분류 var basicImages []tumblebug.ImageInfo + var osMatchedImages []tumblebug.ImageInfo var serverImages []tumblebug.ImageInfo var otherImages []tumblebug.ImageInfo for _, img := range images { + // v0.12.1: OSType 필드를 사용하여 더 정확한 매칭 + osType := img.GetOSType() // v0.11.19 및 v0.12.1 호환 + // isBasicImage=true인 이미지 우선 if img.IsBasicImage { basicImages = append(basicImages, img) + } else if osType != "" && strings.Contains(strings.ToLower(osType), strings.ToLower(preferredOS)) { + // v0.12.1: OSType이 선호 OS와 일치하는 경우 + osMatchedImages = append(osMatchedImages, img) } else if strings.Contains(strings.ToLower(img.Name), "server") || strings.Contains(strings.ToLower(img.Name), "jammy") { // server 또는 jammy가 포함된 이미지 @@ -711,31 +719,38 @@ func (l *LoadService) selectBestImage(images []tumblebug.ImageInfo, preferredOS // 1순위: isBasicImage=true인 이미지 if len(basicImages) > 0 { - log.Info().Msgf("Found %d basic images, selecting first one", len(basicImages)) + // 기본 이미지 중에서도 OSType이 선호 OS와 일치하는 것 우선 + for _, img := range basicImages { + osType := img.GetOSType() + if osType != "" && strings.Contains(strings.ToLower(osType), strings.ToLower(preferredOS)) { + log.Info().Msgf("Found basic image with matching OSType '%s': %s", osType, img.Name) + return img + } + } + log.Info().Msgf("Found %d basic images, selecting first one: %s", len(basicImages), basicImages[0].Name) return basicImages[0] } - // 2순위: server/jammy 이미지 (daily, pro 제외) + // 2순위: v0.12.1 OSType이 선호 OS와 일치하는 이미지 + if len(osMatchedImages) > 0 { + for _, img := range osMatchedImages { + // daily, pro, minimal, fips, k8s, deep-learning 등 제외 + if l.isSuitableImage(img) { + log.Info().Msgf("Found OSType matched image: %s (OSType: %s)", img.Name, img.GetOSType()) + return img + } + } + } + + // 3순위: server/jammy 이미지 (daily, pro 제외) for _, img := range serverImages { - // daily, pro, minimal, fips, k8s, deep-learning 등 제외 - if !strings.Contains(strings.ToLower(img.Name), "daily") && - !strings.Contains(strings.ToLower(img.Name), "pro") && - !strings.Contains(strings.ToLower(img.Name), "minimal") && - !strings.Contains(strings.ToLower(img.Name), "fips") && - !strings.Contains(strings.ToLower(img.Name), "k8s") && - !strings.Contains(strings.ToLower(img.Name), "kubernetes") && - !strings.Contains(strings.ToLower(img.Name), "container") && - !strings.Contains(strings.ToLower(img.Name), "deep") && - !strings.Contains(strings.ToLower(img.Name), "learning") && - !strings.Contains(strings.ToLower(img.Name), "neuron") && - !strings.Contains(strings.ToLower(img.Name), "parallelcluster") && - !strings.Contains(strings.ToLower(img.Name), "sql") { + if l.isSuitableImage(img) { log.Info().Msgf("Found suitable server image: %s", img.Name) return img } } - // 3순위: 첫 번째 이미지 (폴백) + // 4순위: 첫 번째 이미지 (폴백) if len(images) > 0 { log.Warn().Msgf("No optimal image found, using first available: %s", images[0].Name) return images[0] @@ -745,6 +760,37 @@ func (l *LoadService) selectBestImage(images []tumblebug.ImageInfo, preferredOS return tumblebug.ImageInfo{} } +// isSuitableImage checks if an image is suitable (excludes daily, pro, minimal, fips, k8s, etc.) +func (l *LoadService) isSuitableImage(img tumblebug.ImageInfo) bool { + name := strings.ToLower(img.Name) + osDistribution := strings.ToLower(img.OSDistribution) + + // 제외할 패턴 목록 + excludePatterns := []string{ + "daily", "pro", "minimal", "fips", "k8s", "kubernetes", + "container", "deep", "learning", "neuron", "parallelcluster", + "sql", "ecs", "eks", "bottlerocket", + } + + for _, pattern := range excludePatterns { + if strings.Contains(name, pattern) || strings.Contains(osDistribution, pattern) { + return false + } + } + + // v0.12.1: Kubernetes 이미지 제외 + if img.IsKubernetesImage { + return false + } + + // v0.12.1: Deprecated 이미지 제외 + if img.ImageStatus == tumblebug.ImageDeprecated { + return false + } + + return true +} + // getAvailableImageTraditional uses the traditional image selection method func (l *LoadService) getAvailableImageTraditional(ctx context.Context, connectionName string) (string, error) { // CB-Tumblebug에서 사용 가능한 이미지 목록 조회 시도 diff --git a/internal/infra/db/db.go b/internal/infra/db/db.go index 1987f79..37c7648 100644 --- a/internal/infra/db/db.go +++ b/internal/infra/db/db.go @@ -20,7 +20,7 @@ import ( type zerologGormLogger struct{} func (z zerologGormLogger) Printf(format string, v ...interface{}) { - log.Printf((fmt.Sprintf(format, v...))) + log.Printf("%s", (fmt.Sprintf(format, v...))) } func migrateDB(defaultDb *gorm.DB) error { diff --git a/internal/infra/outbound/tumblebug/req.go b/internal/infra/outbound/tumblebug/req.go index 31c2efe..5fcca17 100644 --- a/internal/infra/outbound/tumblebug/req.go +++ b/internal/infra/outbound/tumblebug/req.go @@ -204,20 +204,44 @@ type SshKeyInfo struct { PrivateKey string `json:"privateKey,omitempty"` } -// CB-Tumblebug v0.11.8+ 스마트 매칭 구조체 +// CB-Tumblebug v0.12.1 스마트 매칭 구조체 type SearchImageRequest struct { - MatchedSpecId string `json:"matchedSpecId,omitempty"` - ProviderName string `json:"providerName"` - RegionName string `json:"regionName"` - OSType string `json:"osType"` - OSArchitecture string `json:"osArchitecture"` - IsGPUImage *bool `json:"isGPUImage,omitempty"` - IsKubernetesImage *bool `json:"isKubernetesImage,omitempty"` - IsRegisteredByAsset *bool `json:"isRegisteredByAsset,omitempty"` - IncludeDeprecatedImage *bool `json:"includeDeprecatedImage,omitempty"` - IncludeBasicImageOnly *bool `json:"includeBasicImageOnly,omitempty"` - MaxResults int `json:"maxResults,omitempty"` - DetailSearchKeys []string `json:"detailSearchKeys,omitempty"` + // MatchedSpecId is the ID of the matched spec. + // If specified, only the images that match this spec will be returned. + MatchedSpecId string `json:"matchedSpecId,omitempty"` + + // Cloud Service Provider (ex: "aws", "azure", "gcp", etc.) + ProviderName string `json:"providerName,omitempty"` + + // Cloud Service Provider Region (ex: "us-east-1", "us-west-2", etc.) + RegionName string `json:"regionName,omitempty"` + + // Simplified OS name and version string. Space-separated for AND condition (ex: "ubuntu 22.04") + OSType string `json:"osType,omitempty"` + + // The architecture of the operating system of the image. (ex: "x86_64", "arm64", etc.) + OSArchitecture string `json:"osArchitecture,omitempty"` + + // Whether the image is ready for GPU usage or not. + IsGPUImage *bool `json:"isGPUImage,omitempty"` + + // Whether the image is specialized image only for Kubernetes nodes. + IsKubernetesImage *bool `json:"isKubernetesImage,omitempty"` + + // Whether the image is registered by CB-Tumblebug asset file or not. + IsRegisteredByAsset *bool `json:"isRegisteredByAsset,omitempty"` + + // Whether the search results should include deprecated images or not. + IncludeDeprecatedImage *bool `json:"includeDeprecatedImage,omitempty"` + + // Return basic OS distribution only without additional applications. + IncludeBasicImageOnly *bool `json:"includeBasicImageOnly,omitempty"` + + // Maximum number of images to be returned in the search results. + MaxResults int `json:"maxResults,omitempty"` + + // Keywords for searching images in detail. + DetailSearchKeys []string `json:"detailSearchKeys,omitempty"` } type SearchImageResponse struct { diff --git a/internal/infra/outbound/tumblebug/res.go b/internal/infra/outbound/tumblebug/res.go index de038d0..f44d8f2 100644 --- a/internal/infra/outbound/tumblebug/res.go +++ b/internal/infra/outbound/tumblebug/res.go @@ -221,22 +221,104 @@ type SpecInfo struct { type RecommendVmResList = SpecInfoList type RecommendVmRes = SpecInfo -// CB-Tumblebug 이미지 정보 구조체 +// OSArchitecture represents the architecture of the operating system +type OSArchitecture string + +const ( + ARM32 OSArchitecture = "arm32" + ARM64 OSArchitecture = "arm64" + ARM64_MAC OSArchitecture = "arm64_mac" + X86_32 OSArchitecture = "x86_32" + X86_64 OSArchitecture = "x86_64" + X86_32_MAC OSArchitecture = "x86_32_mac" + X86_64_MAC OSArchitecture = "x86_64_mac" + S390X OSArchitecture = "s390x" + ArchitectureNA OSArchitecture = "NA" + ArchitectureUnknown OSArchitecture = "" +) + +// OSPlatform represents the platform of the operating system +type OSPlatform string + +const ( + Linux_UNIX OSPlatform = "Linux/UNIX" + Windows OSPlatform = "Windows" + PlatformNA OSPlatform = "NA" +) + +// ImageStatus represents the status of an image +type ImageStatus string + +const ( + ImageAvailable ImageStatus = "Available" + ImageUnavailable ImageStatus = "Unavailable" + ImageDeprecated ImageStatus = "Deprecated" + ImageNA ImageStatus = "NA" +) + +// ImageSourceCommandHistory represents a single remote command execution record +type ImageSourceCommandHistory struct { + Index int `json:"index"` + CommandExecuted string `json:"commandExecuted"` +} + +// CB-Tumblebug 이미지 정보 구조체 (v0.11.19 및 v0.12.1 호환) type ImageInfo struct { - Id string `json:"id"` - Uid string `json:"uid,omitempty"` - Name string `json:"name"` - ConnectionName string `json:"connectionName,omitempty"` - CspImageId string `json:"cspImageId,omitempty"` - CspImageName string `json:"cspImageName,omitempty"` - Description string `json:"description,omitempty"` + // 기본 필드 + Id string `json:"id"` + Uid string `json:"uid,omitempty"` + Name string `json:"name"` + ConnectionName string `json:"connectionName,omitempty"` + CspImageId string `json:"cspImageId,omitempty"` + CspImageName string `json:"cspImageName,omitempty"` + Description string `json:"description,omitempty"` + SystemLabel string `json:"systemLabel,omitempty"` + IsBasicImage bool `json:"isBasicImage,omitempty"` + + // v0.11.19 호환성 필드 (deprecated) GuestOS string `json:"guestOS,omitempty"` Status string `json:"status,omitempty"` KeyValueList []string `json:"keyValueList,omitempty"` AssociatedObjectList []string `json:"associatedObjectList,omitempty"` IsAutoGenerated bool `json:"isAutoGenerated,omitempty"` - IsBasicImage bool `json:"isBasicImage,omitempty"` - SystemLabel string `json:"systemLabel,omitempty"` + + // v0.12.1 호환성 필드 + ResourceType string `json:"resourceType,omitempty"` + Namespace string `json:"namespace,omitempty"` + ProviderName string `json:"providerName,omitempty"` + RegionList []string `json:"regionList,omitempty"` + SourceVmUid string `json:"sourceVmUid,omitempty"` + SourceCspImageName string `json:"sourceCspImageName,omitempty"` + InfraType string `json:"infraType,omitempty"` + FetchedTime string `json:"fetchedTime,omitempty"` + CreationDate string `json:"creationDate,omitempty"` + IsGPUImage bool `json:"isGPUImage,omitempty"` + IsKubernetesImage bool `json:"isKubernetesImage,omitempty"` + OSType string `json:"osType,omitempty"` + OSArchitecture OSArchitecture `json:"osArchitecture,omitempty"` + OSPlatform OSPlatform `json:"osPlatform,omitempty"` + OSDistribution string `json:"osDistribution,omitempty"` + OSDiskType string `json:"osDiskType,omitempty"` + OSDiskSizeGB float64 `json:"osDiskSizeGB,omitempty"` + ImageStatus ImageStatus `json:"imageStatus,omitempty"` + Details []KeyValue `json:"details,omitempty"` + CommandHistory []ImageSourceCommandHistory `json:"commandHistory,omitempty"` +} + +// GetOSType returns OSType if available, otherwise falls back to GuestOS (v0.11.19 compatibility) +func (i *ImageInfo) GetOSType() string { + if i.OSType != "" { + return i.OSType + } + return i.GuestOS +} + +// GetImageStatus returns ImageStatus if available, otherwise falls back to Status (v0.11.19 compatibility) +func (i *ImageInfo) GetImageStatus() string { + if i.ImageStatus != "" { + return string(i.ImageStatus) + } + return i.Status } type CommandStatusInfo struct {