fix(mcp-server): mcp use climc struct (#25184)

This commit is contained in:
屈轩
2026-07-20 17:17:24 +08:00
committed by GitHub
parent 574cac011a
commit 21822dc778
94 changed files with 4282 additions and 5669 deletions

View File

@@ -24,7 +24,7 @@ func init() {
cmd := shell.NewResourceCmd(&modules.Cloudaccounts).WithKeyword("cloud-account")
cmd.List(&options.CloudaccountListOptions{})
cmd.Show(&options.SCloudAccountIdOptions{})
cmd.Show(&options.CloudaccountShowOptions{})
cmd.Delete(&options.SCloudAccountIdOptions{})
cmd.Update(&options.SCloudAccountUpdateBaseOptions{})
// cmd.PerformClassWithKeyword("preparenets-vmware", "prepare-nets", &options.SVMwareCloudAccountPrepareNetsOptions{})

View File

@@ -34,16 +34,18 @@ func init() {
cmd.Perform("purge", &compute.CloudregionPurgeOptions{})
type CloudregionListOptions struct {
_ struct{} `mcp-desc:"【创建流程中的中间步骤】本工具不能完成创建。指定云厂商时必须传 provider例如创建阿里云provider=[\"Aliyun\"]。创建虚拟机时必须 usable=trueMCP 默认注入)。查完后继续 capability/镜像/sku最后 climc_server_create。严禁只查区域后停止"`
options.BaseListOptions
Usable *bool `help:"List regions where networks are usable"`
UsableVpc *bool `help:"List regions where VPC are usable"`
Service string `help:"List regions which service has available skus" choices:"dbinstances|servers|elasticcaches"`
Usable *bool `help:"只列网络可用的区域;创建虚拟机时必须为 trueMCP 默认注入 usable=true" mcp:"true"`
UsableVpc *bool `help:"List regions where VPC are usable" mcp:"true"`
Service string `help:"List regions which service has available skus" choices:"dbinstances|servers|elasticcaches" mcp:"true"`
ReadOnly *bool `help:"List regions with read only account"`
City string `help:"List regions in the specified city"`
City string `help:"List regions in the specified city" mcp:"true"`
Capability []string `help:"capability filter" choices:"project|compute|network|loadbalancer|objectstore|rds|cache|event"`
Capability []string `help:"capability filter" choices:"project|compute|network|loadbalancer|objectstore|rds|cache|event" mcp:"true"`
DistinctField string `help:"list the specified distinct field, e.g. city, region"`
@@ -224,8 +226,10 @@ func init() {
})
type CloudregionCapabiltyOptions struct {
ID string `help:"ID or name of cloud region to check" json:"-"`
Domain string `help:"cloud region domain"`
_ struct{} `mcp-desc:"【创建流程中的中间步骤】查询区域能力尤其是可用磁盘存储类型storage_types2 / system_storage_types。公有云创建前必须调用ID 用 climc_cloud_region_list 返回的 id。从返回中选系统盘 backend如 cloud_essd、cloud_ssd写入 disk 的 backend=。查完继续镜像/网络/sku最后 climc_server_create"`
ID string `help:"ID or name of cloud region to check必须用 cloud-region-list 的 id" json:"-" mcp:"required"`
Domain string `help:"cloud region domain" mcp:"true"`
ShowEmulated bool `help:"show emulated cloud region"`
}

View File

@@ -24,7 +24,7 @@ var (
R = shell.R
printList = shell.PrintList
printObject = shell.PrintObject
printBatchResults = printutils.PrintJSONBatchResults
printBatchResults = shell.PrintBatchResults
InvalidUpdateError = shell.InvalidUpdateError
printObjectRecursive = printutils.PrintJSONObjectRecursive

View File

@@ -29,7 +29,7 @@ func init() {
cmd.List(&compute.DBInstanceListOptions{})
cmd.Create(&compute.DBInstanceCreateOptions{})
cmd.Update(&compute.DBInstanceUpdateOptions{})
cmd.Show(&compute.DBInstanceIdOptions{})
cmd.Show(&compute.DBInstanceShowOptions{})
cmd.Delete(&compute.DBInstanceDeleteOptions{})
cmd.Perform("renew", &compute.DBInstanceRenewOptions{})
cmd.Perform("change-config", &compute.DBInstanceChangeConfigOptions{})

View File

@@ -42,7 +42,7 @@ import (
func init() {
cmd := shell.NewResourceCmd(&modules.Disks)
cmd.List(&compute_options.DiskListOptions{})
cmd.Show(&compute_options.DiskIdOptions{})
cmd.Show(&compute_options.DiskShowOptions{})
cmd.Perform("public", &compute_options.DiskIdOptions{})
cmd.Perform("private", &compute_options.DiskIdOptions{})
cmd.Perform("syncstatus", &compute_options.DiskIdOptions{})
@@ -55,13 +55,7 @@ func init() {
cmd.Perform("change-billing-type", new(compute_options.DiskChangeBillingTypeOptions))
cmd.Perform("change-storage-type", &compute_options.DiskChangeStorageTypeOptions{})
type DiskDeleteOptions struct {
ID []string `help:"ID of disks to delete" metavar:"DISK"`
OverridePendingDelete bool `help:"Delete disk directly instead of pending delete" short-token:"f"`
DeleteSnapshots bool `help:"Delete disk snapshots before delete disk"`
}
R(&DiskDeleteOptions{}, "disk-delete", "Delete a disk", func(s *mcclient.ClientSession, args *DiskDeleteOptions) error {
R(&compute_options.DiskDeleteOptions{}, "disk-delete", "Delete a disk", func(s *mcclient.ClientSession, args *compute_options.DiskDeleteOptions) error {
params := jsonutils.NewDict()
if args.OverridePendingDelete {
params.Add(jsonutils.JSONTrue, "override_pending_delete")
@@ -173,11 +167,7 @@ func init() {
return nil
})
type DiskResizeOptions struct {
DISK string `help:"ID or name of disk"`
SIZE string `help:"Size of disk"`
}
R(&DiskResizeOptions{}, "disk-resize", "Resize a disk", func(s *mcclient.ClientSession, args *DiskResizeOptions) error {
R(&compute_options.DiskResizeOptions{}, "disk-resize", "Resize a disk", func(s *mcclient.ClientSession, args *compute_options.DiskResizeOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.SIZE), "size")
disk, err := modules.Disks.PerformAction(s, args.DISK, "resize", params)

View File

@@ -27,7 +27,7 @@ import (
func init() {
cmd := shell.NewResourceCmd(&modules.ElasticCache).WithKeyword("elastic-cache")
cmd.List(&compute.ElasticCacheListOptions{})
cmd.Show(&compute.ElasticCacheIdOption{})
cmd.Show(&compute.ElasticCacheShowOptions{})
cmd.Create(&compute.ElasticCacheCreateOptions{})
cmd.Delete(&compute.ElasticCacheIdOption{})
cmd.Perform("restart", &compute.ElasticCacheIdOption{})

View File

@@ -27,7 +27,7 @@ func init() {
cmd.Create(&compute.EipCreateOptions{})
cmd.Delete(&options.BaseIdOptions{})
cmd.Update(&compute.EipUpdateOptions{})
cmd.Show(&options.BaseShowOptions{})
cmd.Show(&compute.EipShowOptions{})
cmd.Perform("purge", &options.BaseIdOptions{})
cmd.Perform("associate", &compute.EipAssociateOptions{})
cmd.Perform("dissociate", &compute.EipDissociateOptions{})

View File

@@ -36,7 +36,7 @@ import (
func init() {
cmd := shell.NewResourceCmd(&modules.Hosts)
cmd.List(&compute.HostListOptions{})
cmd.List(&compute.HostListForMcpOptions{})
cmd.GetMetadata(&options.BaseIdOptions{})
cmd.GetProperty(&compute.HostStatusStatisticsOptions{})
cmd.Update(&compute.HostUpdateOptions{})

View File

@@ -25,7 +25,7 @@ func init() {
cmd := shell.NewResourceCmd(&modules.Kafkas)
cmd.List(&compute.KafkaListOptions{})
cmd.Update(&compute.KafkaUpdateOptions{})
cmd.Show(&compute.KafkaIdOption{})
cmd.Show(&compute.KafkaShowOptions{})
cmd.Get("topics", &options.BaseIdOptions{})
cmd.Delete(&compute.KafkaIdOption{})
cmd.Perform("syncstatus", &compute.KafkaIdOption{})

View File

@@ -25,7 +25,7 @@ func init() {
cmd := shell.NewResourceCmd(&modules.MongoDB)
cmd.List(&compute.MongoDBListOptions{})
cmd.Update(&compute.MongoDBUpdateOptions{})
cmd.Show(&options.BaseIdOptions{})
cmd.Show(&compute.MongoDBShowOptions{})
cmd.Delete(&options.BaseIdOptions{})
cmd.Get("backups", &options.BaseIdOptions{})
cmd.Perform("syncstatus", &options.BaseIdOptions{})

View File

@@ -17,15 +17,14 @@ package compute
import (
"yunion.io/x/onecloud/cmd/climc/shell"
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/mcclient/options"
"yunion.io/x/onecloud/pkg/mcclient/options/compute"
)
func init() {
cmd := shell.NewResourceCmd(&modules.SecGroupRules).WithKeyword("secgroup-rule")
cmd.List(&compute.SecGroupRulesListOptions{})
cmd.Show(&options.BaseShowOptions{})
cmd.Delete(&options.BaseIdOptions{})
cmd.Show(&compute.SecGroupRuleShowOptions{})
cmd.Delete(&compute.SecGroupRuleDeleteOptions{})
cmd.Create(&compute.SecGroupRulesCreateOptions{})
cmd.Update(&compute.SecGroupRulesUpdateOptions{})
}

View File

@@ -25,9 +25,9 @@ func init() {
cmd := shell.NewResourceCmd(&modules.SecGroups)
cmd.List(&options.SecgroupListOptions{})
cmd.Create(&options.SecgroupCreateOptions{})
cmd.Show(&options.SecgroupIdOptions{})
cmd.Show(&options.SecgroupShowOptions{})
cmd.Update(&baseoptions.BaseUpdateOptions{})
cmd.Delete(&options.SecgroupIdOptions{})
cmd.Delete(&options.SecgroupDeleteOptions{})
cmd.Perform("public", &options.SecgroupIdOptions{})
cmd.Perform("syncstatus", &options.SecgroupIdOptions{})
cmd.Perform("private", &options.SecgroupIdOptions{})

View File

@@ -113,10 +113,12 @@ func init() {
})
type ServerAttachDiskOptions struct {
_ struct{} `mcp-desc:"将已有硬盘挂载到虚机。SERVER/DISK 为 id/name可选 driver。先 climc_server_list + climc_disk_list"`
SERVER string `help:"ID or name of server"`
DISK string `help:"ID of name of disk to attach"`
Driver string `help:"Driver" choices:"virtio|ide|scsi"`
Cache string `help:"Cache mode" choices:"writeback|none|writethrought"`
Driver string `help:"Driver" choices:"virtio|ide|scsi" mcp:"true"`
Cache string `help:"Cache mode" choices:"writeback|none|writethrought" mcp:"true"`
}
R(&ServerAttachDiskOptions{}, "server-attach-disk", "Attach an existing virtual disks to a virtual server", func(s *mcclient.ClientSession, args *ServerAttachDiskOptions) error {
params := jsonutils.NewDict()
@@ -136,9 +138,11 @@ func init() {
})
type ServerDetachDiskOptions struct {
_ struct{} `mcp-desc:"从虚机卸载硬盘。SERVER/DISK 为 id/name可选 delete-disk。先 climc_server_list / climc_disk_list"`
SERVER string `help:"ID or name of server"`
DISK string `help:"ID or name of disk to detach"`
DeleteDisk bool `help:"Delete disk if the disk not has flag of auto_delete when detached"`
DeleteDisk bool `help:"Delete disk if the disk not has flag of auto_delete when detached" mcp:"true"`
}
R(&ServerDetachDiskOptions{}, "server-detach-disk", "Detach a disk from a virtual server", func(s *mcclient.ClientSession, args *ServerDetachDiskOptions) error {
params := jsonutils.NewDict()

View File

@@ -127,8 +127,9 @@ func init() {
cmd.Perform("update-sub-ips", &options.ServerUpdateSubIpsOptions{})
cmd.BatchPerform("restore-virtual-isolated-devices", &options.ServerIdsOptions{})
cmd.BatchPerform("set-os-info", &options.ServerSetOSInfoOptions{})
cmd.BatchPerform("start-rescue", &options.ServerStartOptions{})
cmd.BatchPerform("stop-rescue", &options.ServerStartOptions{})
// 与 server-start 复用参数时单独包一层,避免继承 mcp-desc 被注册为 MCP tool
cmd.BatchPerform("start-rescue", &options.ServerStartRescueOptions{})
cmd.BatchPerform("stop-rescue", &options.ServerStopRescueOptions{})
cmd.BatchPerform("sync-os-info", &options.ServerIdsOptions{})
cmd.BatchPerform("set-root-disk-matcher", &options.ServerSetRootDiskMatcher{})
cmd.Perform("disable-auto-merge-snapshot", &options.ServerDisableAutoMergeSnapshot{})

View File

@@ -22,9 +22,9 @@ import (
var (
R = shell.R
printList = printutils.PrintJSONList
printObject = printutils.PrintJSONObject
printBatchResults = printutils.PrintJSONBatchResults
printList = shell.PrintList
printObject = shell.PrintObject
printBatchResults = shell.PrintBatchResults
InvalidUpdateError = shell.InvalidUpdateError
printObjectRecursive = printutils.PrintJSONObjectRecursive

View File

@@ -32,10 +32,12 @@ func init() {
cmd.GetProperty(&identity_options.DomainGetPropertyTagValuePairOptions{})
cmd.GetProperty(&identity_options.DomainGetPropertyTagValueTreeOptions{})
type DomainDetailOptions struct {
type DomainShowOptions struct {
_ struct{} `mcp-desc:"查询域详情。ID 可用 climc_domain_list 返回的 id/name"`
ID string `help:"ID or domain"`
}
R(&DomainDetailOptions{}, "domain-show", "Show detail of domain", func(s *mcclient.ClientSession, args *DomainDetailOptions) error {
R(&DomainShowOptions{}, "domain-show", "Show detail of domain", func(s *mcclient.ClientSession, args *DomainShowOptions) error {
result, err := modules.Domains.Get(s, args.ID, nil)
if err != nil {
return err
@@ -43,7 +45,12 @@ func init() {
printObject(result)
return nil
})
R(&DomainDetailOptions{}, "domain-delete", "Delete a domain", func(s *mcclient.ClientSession, args *DomainDetailOptions) error {
type DomainDeleteOptions struct {
_ struct{} `mcp-desc:"删除域。若尚不知 id先用 climc_domain_list 定位;确认域下无项目/用户后再删"`
ID string `help:"ID or domain"`
}
R(&DomainDeleteOptions{}, "domain-delete", "Delete a domain", func(s *mcclient.ClientSession, args *DomainDeleteOptions) error {
objId, err := modules.Domains.GetId(s, args.ID, nil)
if err != nil {
return err
@@ -72,12 +79,14 @@ func init() {
}) */
type DomainCreateOptions struct {
NAME string `help:"Name of domain"`
Desc string `help:"Description"`
Enabled bool `help:"Set the domain enabled"`
Disabled bool `help:"Set the domain disabled"`
_ struct{} `mcp-desc:"创建域。NAME 必填;可选 displayname/desc/enabled"`
Displayname string `help:"display name"`
NAME string `help:"Name of domain"`
Desc string `help:"Description" mcp:"true"`
Enabled bool `help:"Set the domain enabled" mcp:"true"`
Disabled bool `help:"Set the domain disabled" mcp:"true"`
Displayname string `help:"display name" mcp:"true"`
}
R(&DomainCreateOptions{}, "domain-create", "Create a new domain", func(s *mcclient.ClientSession, args *DomainCreateOptions) error {
params := jsonutils.NewDict()

View File

@@ -43,8 +43,10 @@ func init() {
cmd.PerformClass("clean", &identity_options.ProjectCleanOptions{})
type ProjectShowOptions struct {
_ struct{} `mcp-desc:"查询项目详情。ID 可用 climc_project_list 返回的 id/name跨域时可传 domain"`
ID string `help:"ID or Name of project"`
Domain string `help:"Domain"`
Domain string `help:"Domain" mcp:"true"`
}
R(&ProjectShowOptions{}, "project-show", "Show details of project", func(s *mcclient.ClientSession, args *ProjectShowOptions) error {
query := jsonutils.NewDict()
@@ -62,7 +64,13 @@ func init() {
printObject(result)
return nil
})
R(&ProjectShowOptions{}, "project-delete", "Delete a project", func(s *mcclient.ClientSession, args *ProjectShowOptions) error {
type ProjectDeleteOptions struct {
_ struct{} `mcp-desc:"删除项目。若尚不知 id先用 climc_project_list 定位;跨域时可传 domain"`
ID string `help:"ID or Name of project"`
Domain string `help:"Domain" mcp:"true"`
}
R(&ProjectDeleteOptions{}, "project-delete", "Delete a project", func(s *mcclient.ClientSession, args *ProjectDeleteOptions) error {
query := jsonutils.NewDict()
if len(args.Domain) > 0 {
domainId, err := modules.Domains.GetId(s, args.Domain, nil)
@@ -83,12 +91,14 @@ func init() {
})
type ProjectCreateOptions struct {
_ struct{} `mcp-desc:"创建项目。NAME 必填;可选 domain/displayname/desc"`
NAME string `help:"Name of new project"`
Displayname string `help:"display name"`
Domain string `help:"Domain"`
Desc string `help:"Description"`
Enabled bool `help:"Project is enabled"`
Disabled bool `help:"Project is disabled"`
Displayname string `help:"display name" mcp:"true"`
Domain string `help:"Domain" mcp:"true"`
Desc string `help:"Description" mcp:"true"`
Enabled bool `help:"Project is enabled" mcp:"true"`
Disabled bool `help:"Project is disabled" mcp:"true"`
}
R(&ProjectCreateOptions{}, "project-create", "Create a project", func(s *mcclient.ClientSession, args *ProjectCreateOptions) error {
params := jsonutils.NewDict()

View File

@@ -64,12 +64,14 @@ func init() {
return nil
})*/
type UserDetailOptions struct {
type UserShowOptions struct {
_ struct{} `mcp-desc:"查询用户详情。ID 可用 climc_user_list 返回的 id/name跨域时可传 domain"`
ID string `help:"ID of user"`
Domain string `help:"Domain"`
System bool `help:"show system user"`
Domain string `help:"Domain" mcp:"true"`
System bool `help:"show system user" mcp:"true"`
}
R(&UserDetailOptions{}, "user-show", "Show details of user", func(s *mcclient.ClientSession, args *UserDetailOptions) error {
R(&UserShowOptions{}, "user-show", "Show details of user", func(s *mcclient.ClientSession, args *UserShowOptions) error {
query := jsonutils.NewDict()
if len(args.Domain) > 0 {
domainId, err := modules.Domains.GetId(s, args.Domain, nil)
@@ -89,7 +91,14 @@ func init() {
return nil
})
R(&UserDetailOptions{}, "user-delete", "Delete user", func(s *mcclient.ClientSession, args *UserDetailOptions) error {
type UserDeleteOptions struct {
_ struct{} `mcp-desc:"删除用户。若尚不知 id先用 climc_user_list 定位;跨域时可传 domain"`
ID string `help:"ID of user"`
Domain string `help:"Domain" mcp:"true"`
System bool `help:"show system user" mcp:"true"`
}
R(&UserDeleteOptions{}, "user-delete", "Delete user", func(s *mcclient.ClientSession, args *UserDeleteOptions) error {
query := jsonutils.NewDict()
if len(args.Domain) > 0 {
domainId, err := modules.Domains.GetId(s, args.Domain, nil)
@@ -109,6 +118,11 @@ func init() {
return nil
})
type UserDetailOptions struct {
ID string `help:"ID of user"`
Domain string `help:"Domain"`
System bool `help:"show system user"`
}
R(&UserDetailOptions{}, "user-project-list", "List projects of user", func(s *mcclient.ClientSession, args *UserDetailOptions) error {
query := jsonutils.NewDict()
if len(args.Domain) > 0 {
@@ -170,29 +184,31 @@ func init() {
})
type UserCreateOptions struct {
NAME string `help:"Name of the new user"`
Domain string `help:"Domain"`
Desc string `help:"Description"`
Password *string `help:"Password"`
Displayname string `help:"Displayname"`
Email string `help:"Email"`
Mobile string `help:"Mobile"`
Enabled bool `help:"Enabled"`
Disabled bool `help:"Disabled"`
_ struct{} `mcp-desc:"创建用户。NAME 必填;建议传 password可选 domain/email/mobile/displayname"`
SkipPasswordComplexityCheck bool `help:"do password complexity check, default is false"`
NAME string `help:"Name of the new user"`
Domain string `help:"Domain" mcp:"true"`
Desc string `help:"Description" mcp:"true"`
Password *string `help:"Password" mcp:"true"`
Displayname string `help:"Displayname" mcp:"true"`
Email string `help:"Email" mcp:"true"`
Mobile string `help:"Mobile" mcp:"true"`
Enabled bool `help:"Enabled" mcp:"true"`
Disabled bool `help:"Disabled" mcp:"true"`
SkipPasswordComplexityCheck bool `help:"do password complexity check, default is false" mcp:"true"`
// DefaultProject string `help:"Default project"`
SystemAccount bool `help:"is a system account?"`
NoWebConsole bool `help:"allow web console access"`
EnableMfa bool `help:"enable TOTP mfa"`
SystemAccount bool `help:"is a system account?" mcp:"true"`
NoWebConsole bool `help:"allow web console access" mcp:"true"`
EnableMfa bool `help:"enable TOTP mfa" mcp:"true"`
IdpId string `help:"Id of identity provider to link with"`
IdpEntityId string `help:"Entity id of identity provider to link with"`
IdpId string `help:"Id of identity provider to link with" mcp:"true"`
IdpEntityId string `help:"Entity id of identity provider to link with" mcp:"true"`
Lang string `help:"user default language"`
Lang string `help:"user default language" mcp:"true"`
Expire string `help:"user expired at"`
Expire string `help:"user expired at" mcp:"true"`
}
R(&UserCreateOptions{}, "user-create", "Create a user", func(s *mcclient.ClientSession, args *UserCreateOptions) error {
params := jsonutils.NewDict()

View File

@@ -22,9 +22,9 @@ import (
var (
R = shell.R
printList = printutils.PrintJSONList
printObject = printutils.PrintJSONObject
printBatchResults = printutils.PrintJSONBatchResults
printList = shell.PrintList
printObject = shell.PrintObject
printBatchResults = shell.PrintBatchResults
InvalidUpdateError = shell.InvalidUpdateError
printObjectRecursive = printutils.PrintJSONObjectRecursive

View File

@@ -47,9 +47,11 @@ type BaseActionListOptions struct {
}
type ActionListOptions struct {
_ struct{} `mcp-desc:"查询操作日志(谁在何时对资源做了什么)。可按对象类型 type如 server/disk/host、对象 id、时间 since/until、action、user、project、succ/fail 过滤;默认 limit=20。排查创建失败、误操作、审计时优先调用"`
BaseActionListOptions
Service []string `help:"service name"`
Id string `help:"" metavar:"OBJ_ID"`
Id string `help:"filter by object id" metavar:"OBJ_ID"`
Type []string `help:"Type of relevant object" metavar:"OBJ_TYPE"`
}

View File

@@ -15,13 +15,11 @@
package logger
import (
"yunion.io/x/pkg/util/printutils"
"yunion.io/x/onecloud/cmd/climc/shell"
)
var (
R = shell.R
printList = printutils.PrintJSONList
printObject = printutils.PrintJSONObject
printList = shell.PrintList
printObject = shell.PrintObject
)

View File

@@ -15,17 +15,15 @@
package monitor
import (
"yunion.io/x/pkg/util/printutils"
"yunion.io/x/onecloud/cmd/climc/shell"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
)
var (
R = shell.R
printList = printutils.PrintJSONList
printObject = printutils.PrintJSONObject
printBatchResults = printutils.PrintJSONBatchResults
printList = shell.PrintList
printObject = shell.PrintObject
printBatchResults = shell.PrintBatchResults
)
func NewResourceCmd(manager modulebase.IBaseManager) *shell.ResourceCmd {

View File

@@ -22,9 +22,9 @@ import (
var (
R = shell.R
printList = printutils.PrintJSONList
printObject = printutils.PrintJSONObject
printBatchResults = printutils.PrintJSONBatchResults
printList = shell.PrintList
printObject = shell.PrintObject
printBatchResults = shell.PrintBatchResults
InvalidUpdateError = shell.InvalidUpdateError
printObjectRecursive = printutils.PrintJSONObjectRecursive

View File

@@ -15,10 +15,15 @@
package shell
import (
"bytes"
"fmt"
"io"
"os"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/util/printutils"
@@ -37,45 +42,111 @@ const (
var outputFormat = OUTPUT_FORMAT_TABLE
// goroutine 本地输出MCP 并发 tools/call 时避免劫持全局 os.Stdout。
type outputState struct {
writer io.Writer
format string
}
var outputStates sync.Map // uint64(goid) -> *outputState
func OutputFormat(s string) {
outputFormat = s
}
// PushOutput 将当前 goroutine 的 shell 输出重定向到 w并可选覆盖格式。
// 返回的 restore 必须在同一 goroutine 调用。
func PushOutput(w io.Writer, format string) (restore func()) {
id := goroutineID()
prev, _ := outputStates.Load(id)
outputStates.Store(id, &outputState{writer: w, format: format})
return func() {
if prev != nil {
outputStates.Store(id, prev)
} else {
outputStates.Delete(id)
}
}
}
func currentWriter() io.Writer {
if v, ok := outputStates.Load(goroutineID()); ok {
if s := v.(*outputState); s != nil && s.writer != nil {
return s.writer
}
}
return os.Stdout
}
func currentFormat() string {
if v, ok := outputStates.Load(goroutineID()); ok {
if s := v.(*outputState); s != nil && s.format != "" {
return s.format
}
}
return outputFormat
}
func goroutineID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
i := bytes.IndexByte(b, ' ')
if i <= 0 {
return 0
}
n, _ := strconv.ParseUint(string(b[:i]), 10, 64)
return n
}
func PrintList(list *printutils.ListResult, columns []string) {
switch outputFormat {
w := currentWriter()
switch currentFormat() {
case OUTPUT_FORMAT_TABLE:
if w == os.Stdout {
printutils.PrintJSONList(list, columns)
return
}
fmt.Fprint(w, jsonutils.Marshal(list).PrettyString())
fmt.Fprint(w, "\n")
case OUTPUT_FORMAT_JSON:
fmt.Print(jsonutils.Marshal(list).PrettyString())
fmt.Print("\n")
fmt.Fprint(w, jsonutils.Marshal(list).PrettyString())
fmt.Fprint(w, "\n")
case OUTPUT_FORMAT_YAML:
fmt.Print(jsonutils.Marshal(list).YAMLString())
fmt.Fprint(w, jsonutils.Marshal(list).YAMLString())
default:
fmt.Fprintf(os.Stderr, "unknown output format: %q\n", outputFormat)
fmt.Fprintf(os.Stderr, "unknown output format: %q\n", currentFormat())
}
}
func PrintObject(obj jsonutils.JSONObject) {
switch outputFormat {
w := currentWriter()
switch currentFormat() {
case OUTPUT_FORMAT_TABLE:
if w == os.Stdout {
printutils.PrintJSONObject(obj)
return
}
fmt.Fprint(w, obj.PrettyString())
fmt.Fprint(w, "\n")
case OUTPUT_FORMAT_KV:
printObjectFmtKv(obj)
case OUTPUT_FORMAT_JSON:
fmt.Print(obj.PrettyString())
fmt.Print("\n")
fmt.Fprint(w, obj.PrettyString())
fmt.Fprint(w, "\n")
case OUTPUT_FORMAT_YAML:
fmt.Print(obj.YAMLString())
fmt.Fprint(w, obj.YAMLString())
case OUTPUT_FORMAT_FLATTEN_TABLE:
printObjectRecursive(obj)
case OUTPUT_FORMAT_FLATTEN_KV:
printObjectRecursiveEx(obj, printObjectFmtKv)
default:
fmt.Fprintf(os.Stderr, "unknown output format: %q\n", outputFormat)
fmt.Fprintf(os.Stderr, "unknown output format: %q\n", currentFormat())
}
}
func printObjectFmtKv(obj jsonutils.JSONObject) {
w := currentWriter()
m, _ := obj.GetMap()
maxWidth := 0
keys := make([]string, 0, len(m))
@@ -95,7 +166,7 @@ func printObjectFmtKv(obj jsonutils.JSONObject) {
} else {
s = objV.String()
}
fmt.Printf("%*s: %s\n", maxWidth, k, s)
fmt.Fprintf(w, "%*s: %s\n", maxWidth, k, s)
}
}
@@ -107,8 +178,24 @@ func printObjectRecursiveEx(obj jsonutils.JSONObject, cb printutils.PrintJSONObj
printutils.PrintJSONObjectRecursiveEx(obj, cb)
}
func printBatchResults(results []printutils.SubmitResult, columns []string) {
func PrintBatchResults(results []printutils.SubmitResult, columns []string) {
w := currentWriter()
switch currentFormat() {
case OUTPUT_FORMAT_JSON:
fmt.Fprint(w, jsonutils.Marshal(results).PrettyString())
fmt.Fprint(w, "\n")
default:
if w == os.Stdout {
printutils.PrintJSONBatchResults(results, columns)
return
}
fmt.Fprint(w, jsonutils.Marshal(results).PrettyString())
fmt.Fprint(w, "\n")
}
}
func printBatchResults(results []printutils.SubmitResult, columns []string) {
PrintBatchResults(results, columns)
}
func ExportList(list *printutils.ListResult, file string, exportKeys string, exportTexts string, columns []string) {

View File

@@ -1,59 +1,24 @@
# MCP Server
MCP Server 是 Cloudpods 多云管理平台的核心组件之一,负责处理多云资源的统一管理和调度
Cloudpods MCP Server通过 MCP 协议把 climc 能力暴露给 AI 客户端Cursor / Claude 等)
## 目录结构
```
├── adapters/ # 适配器模块用于对接不同云平台的API
├── config/ # 配置模块,处理服务配置和加载
├── models/ # 数据模型,定义云资源的数据结构
├── registry/ # 注册中心,管理可用的工具和服务
├── server/ # 服务核心,包含服务启动和初始化逻辑
└── tools/ # 工具模块,实现各种云资源管理功能
├── adapters/ # Cloudpods 认证与 ClientSession
├── climcgen/ # 从 climc CommandTable + Options tag 生成 MCP tools
├── options/ # 服务配置
├── registry/ # MCP tool 注册
├── server/ # SSE / stdio 服务
└── service/ # 进程入口装配
```
## 架构设计
MCP Server 采用模块化设计,主要包括以下几个核心模块:
1. **适配器模块 (Adapters)**: 负责与不同云平台的API进行交互实现资源的统一管理。
2. **配置模块 (Config)**: 处理服务的配置加载和管理,支持多种配置方式。
3. **数据模型 (Models)**: 定义云资源的数据结构,为其他模块提供统一的数据访问接口。
4. **注册中心 (Registry)**: 管理可用的工具和服务,支持动态注册和发现。
5. **服务核心 (Server)**: 负责服务的启动、初始化和生命周期管理。
6. **工具模块 (Tools)**: 实现各种云资源管理功能如VPC、网络、镜像等。
## 运行机制
1. 服务启动时,首先加载配置文件并初始化各个模块。
2. 适配器模块根据配置连接到相应的云平台。
3. 注册中心注册所有可用的工具和服务。
4. 服务核心启动HTTP服务器监听客户端请求。
5. 客户端通过API调用相应的工具来管理云资源。
1. 启动时 blank-import climc shell 包,填充 `shell.CommandTable`
2. 扫描 Options 上带 `mcp-desc` 的命令,用 Options struct tag 生成 schema 并注册 tools
3. 工具调用时用 AK/SK或 Header建 session执行对应 climc callbackJSON 输出返回给客户端
## 主要功能
## 扩展工具
- 统一管理多云资源VPC、网络、镜像、主机等
- 支持多种云平台AWS、Azure、阿里云等
- 提供RESTful API接口
- 支持资源的查询、创建、更新和删除操作
## 配置说明
配置文件位于 `options/options.go`,主要包含以下配置项:
- ServerConfig: 服务配置,如监听地址、端口等
- MCPConfig: MCP相关配置
- ExternalConfig: 外部服务配置
## 开发指南
1. 实现新的云资源管理功能时,需要在 `tools/` 目录下创建相应的工具文件。
2. 工具需要实现 `Tool` 接口,包括 `GetTool``Handle``GetName` 方法。
3. 数据模型定义在 `models/` 目录下需要根据云平台API文档进行定义。
4. 适配器实现在 `adapters/` 目录下用于与云平台API进行交互。
## 贡献
欢迎提交Issue和Pull Request来改进MCP Server。
在对应 climc Options 上增加 `_ struct{} \`mcp-desc:"..."\``(并按需给字段加 `mcp:"true"`),重启 mcp-server 即可注册。

View File

@@ -86,6 +86,8 @@ func mcpAgentChatStreamHandler(ctx context.Context, w http.ResponseWriter, r *ht
// Prepare request to backend
headers := http.Header{}
headers.Set("Content-Type", "application/json")
headers.Set("Accept", "text/event-stream")
headers.Set("Accept-Encoding", "identity")
// Forward the request body to the backend
var bodyReader io.Reader
@@ -125,7 +127,9 @@ func mcpAgentChatStreamHandler(ctx context.Context, w http.ResponseWriter, r *ht
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// For now just standard SSE headers.
w.Header().Set("X-Accel-Buffering", "no")
// 禁止中间层 gzip 缓冲整段 SSE
w.Header().Set("Content-Encoding", "identity")
if f, ok := w.(http.Flusher); ok {
f.Flush()
@@ -160,6 +164,8 @@ func mcpAgentDefaultChatStreamHandler(ctx context.Context, w http.ResponseWriter
headers := http.Header{}
headers.Set("Content-Type", "application/json")
headers.Set("Accept", "text/event-stream")
headers.Set("Accept-Encoding", "identity")
var bodyReader io.Reader
if r.Body != nil {
@@ -194,6 +200,8 @@ func mcpAgentDefaultChatStreamHandler(ctx context.Context, w http.ResponseWriter
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Content-Encoding", "identity")
if f, ok := w.(http.Flusher); ok {
f.Flush()
}

View File

@@ -10,6 +10,7 @@ import (
const (
SERVICE_TYPE = "llm"
SERVICE_VERSION = ""
)
type LLMBaseListDetails struct {

View File

@@ -13,15 +13,16 @@ const (
LLM_CLIENT_OLLAMA LLMClientType = "ollama"
LLM_CLIENT_OPENAI LLMClientType = "openai"
MCP_AGENT_SYSTEM_PROMPT = `你是一个 Cloudpods 云平台管理助手。你可以使用提供的工具来帮助用户管理云资源。
MCP_AGENT_SYSTEM_PROMPT = `你是一个 %s 云平台管理助手。你可以使用提供的工具来帮助用户管理云资源。
## 你的能力
- 查询云平台资源(虚拟机、镜像、网络、存储、区域等)
- 查询云平台资源(虚拟机、镜像、网络、存储、区域、套餐等)
- 管理虚拟机(创建、启动、停止、重启、删除、重置密码)
- 获取虚拟机监控信息和实时统计数据
## 重要规则(必须严格遵守)
**如果用户的问题涉及查询、创建、修改或删除云资源,你必须先调用相应的工具,而不是直接回答。**
- 严禁空口编造:在未真正调用工具并拿到返回结果前,禁止声称“已查到区域/镜像/网络”“正在创建成功”等。
- 对于需要查询资源的问题(如"列出虚拟机"、"查询状态"等),必须调用工具获取数据后再回答
- 对于需要操作资源的问题(如"创建"、"启动"、"停止"等),必须调用工具执行操作后再回答
- 只有在以下情况才可以直接回复:
@@ -29,9 +30,17 @@ const (
2. 没有合适的工具可以解决用户的问题
3. 工具调用失败后需要向用户说明错误原因
## 创建虚拟机标准流程(同一轮对话中连续调用工具,不要只说不做;查询可并行以节省轮次)
1. climc_cloud_region_list公有云必须 provider=["Aliyun"] 等;创建时 usable=true
2. climc_cloud_region_capabilityID=区域 id从 storage_types2 取系统盘 backend
3. climc_cached_image_list公有云或 climc_image_listKVM公有云必须带 provider + region=区域 id不要重复调用
4. climc_server_sku_list公有云带 provider+cloudregion用户说 2c2g/2核2G 时传 spec="2c2g"
5. climc_server_createname、disk 须含 image+backend、instance-type 或 ncpu/mem-spec公有云 hypervisor=aliyun、prefer-region=区域 id。net 可省略:未指定时自动 randomnets:[{exit:false}])调度,默认不要先 network-list/vpc-list
查询工具的返回不等于任务完成;必须最终调用 climc_server_create。创建失败时根据工具错误向用户说明原因。
## 工作流程
1. 理解用户的需求
2. **优先检查是否有合适的工具可以完成任务,如果有则必须调用工具**
2. **优先检查是否有合适的工具可以完成任务,如果有则必须调用工具(发 tool_calls不要只输出计划文字**
3. 分析工具返回的结果
4. 如果需要更多信息,继续调用其他工具
5. 最后用自然语言总结结果给用户

View File

@@ -304,10 +304,12 @@ func (o *openai) doChatStreamRequest(ctx context.Context, mcpAgent *models.SMCPA
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return errors.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
return formatLLMHTTPError(resp.StatusCode, mcpAgent.Model, body)
}
scanner := bufio.NewScanner(resp.Body)
// 工具调用场景下单行 SSE 可能很大,提高 buffer
scanner.Buffer(make([]byte, 64*1024), 2*1024*1024)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSpace(line)
@@ -394,7 +396,7 @@ func (o *openai) doChatRequest(ctx context.Context, mcpAgent *models.SMCPAgent,
}
if resp.StatusCode != http.StatusOK {
return nil, errors.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
return nil, formatLLMHTTPError(resp.StatusCode, mcpAgent.Model, body)
}
var chatResp OpenAIChatResponse
@@ -511,6 +513,57 @@ func (o *openai) ConvertMCPTools(mcpTools []mcp.Tool) []models.ILLMTool {
return tools
}
// formatLLMHTTPError 把上游 JSON 错误整理成可读单行提示。
func formatLLMHTTPError(status int, model string, body []byte) error {
msg := extractLLMErrorMessage(body)
if msg == "" {
msg = strings.TrimSpace(string(body))
msg = strings.ReplaceAll(msg, "\n", " ")
msg = strings.Join(strings.Fields(msg), " ")
}
if msg == "" {
msg = http.StatusText(status)
}
lower := strings.ToLower(msg)
switch {
case strings.Contains(lower, "unsupported model"):
return errors.Errorf("模型不支持:当前配置为 %q%s", model, msg)
case status == http.StatusUnauthorized || strings.Contains(lower, "invalid api key") || strings.Contains(lower, "incorrect api key"):
return errors.Errorf("鉴权失败:请检查 Agent 的 API Key%s", msg)
case status == http.StatusTooManyRequests:
return errors.Errorf("请求过于频繁,请稍后重试(%s", msg)
default:
return errors.Errorf("大模型接口返回 %d%s", status, msg)
}
}
func extractLLMErrorMessage(body []byte) string {
body = bytes.TrimSpace(body)
if len(body) == 0 {
return ""
}
var payload map[string]interface{}
if err := json.Unmarshal(body, &payload); err != nil {
return ""
}
if errObj, ok := payload["error"].(map[string]interface{}); ok {
if m, ok := errObj["message"].(string); ok && strings.TrimSpace(m) != "" {
return strings.TrimSpace(m)
}
if m, ok := errObj["msg"].(string); ok && strings.TrimSpace(m) != "" {
return strings.TrimSpace(m)
}
}
if m, ok := payload["message"].(string); ok && strings.TrimSpace(m) != "" {
return strings.TrimSpace(m)
}
if m, ok := payload["msg"].(string); ok && strings.TrimSpace(m) != "" {
return strings.TrimSpace(m)
}
return ""
}
// Structures
type OpenAIChatMessage struct {

View File

@@ -0,0 +1,30 @@
package llm_client
import (
"net/http"
"strings"
"testing"
)
func TestFormatLLMHTTPErrorUnsupportedModel(t *testing.T) {
body := []byte(`{
"error": {
"code": "400",
"message": "Unsupported model MiMo-V2.5"
}
}`)
err := formatLLMHTTPError(http.StatusBadRequest, "MiMo-V2.5", body)
if err == nil {
t.Fatal("expected error")
}
msg := err.Error()
if !strings.Contains(msg, "模型不支持") {
t.Fatalf("want 模型不支持, got %q", msg)
}
if !strings.Contains(msg, "MiMo-V2.5") {
t.Fatalf("want model name in message, got %q", msg)
}
if strings.Contains(msg, "\n") {
t.Fatalf("error should be single line, got %q", msg)
}
}

View File

@@ -199,14 +199,20 @@ func (mcp *SMCPAgent) GetApiKey() (string, error) {
func (man *SMCPAgentManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) {
man.SSharableVirtualResourceBaseManager.CustomizeHandlerInfo(info)
// log.Infoln("query name of handler info", info.GetName(nil))
switch info.GetName(nil) {
case "get_specific":
info.SetProcessTimeout(time.Hour * 4).SetWorkerManager(mcpAgentWorkerMan)
}
}
func (man *SMCPAgentManager) SetHandlerProcessTimeout(info *appsrv.SHandlerInfo, r *http.Request) time.Duration {
// 仅 llm 侧 mcp_agents/*/chat-stream
if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "chat-stream") {
return 4 * time.Hour
}
return man.SSharableVirtualResourceBaseManager.SetHandlerProcessTimeout(info, r)
}
func (man *SMCPAgentManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input *api.MCPAgentCreateInput) (*api.MCPAgentCreateInput, error) {
var err error
input.SharableVirtualResourceCreateInput, err = man.SSharableVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.SharableVirtualResourceCreateInput)
@@ -472,9 +478,12 @@ func (mcp *SMCPAgent) PerformChatStream(
}
w := appParams.Response
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Content-Encoding", "identity")
appParams.OverrideResponseBodyWrapper = true
if f, ok := w.(http.Flusher); ok {
f.Flush()
@@ -482,29 +491,67 @@ func (mcp *SMCPAgent) PerformChatStream(
return nil, errors.Error("Streaming unsupported!")
}
_, err := mcp.process(ctx, userCred, &input, func(content string) error {
if len(content) > 0 {
for line := range strings.SplitSeq(content, "\n") {
fmt.Fprintf(w, "data: %s\n", line)
}
fmt.Fprintf(w, "\n")
// 立刻推一条注释帧,避免 ListTools/首轮推理期间前端只看到「思考中」
if _, err := fmt.Fprintf(w, ": connected\n\n"); err == nil {
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
_, err := mcp.process(ctx, userCred, &input, func(content string) error {
if len(content) == 0 {
return nil
}
// 单个 SSE 事件:多行 content 用多条 data: 表示(前端按事件拼接为 \n
content = strings.ReplaceAll(content, "\r\n", "\n")
content = strings.ReplaceAll(content, "\r", "\n")
for _, line := range strings.Split(content, "\n") {
if _, err := fmt.Fprintf(w, "data: %s\n", line); err != nil {
return err
}
}
if _, err := fmt.Fprintf(w, "\n"); err != nil {
return err
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
return nil
})
if err != nil {
fmt.Fprintf(w, "data: Error: %v\n\n", err)
// 单行推送,避免换行 JSON 被 SSE 截断;去掉冗长 wrap 前缀
msg := friendlyChatStreamError(err)
fmt.Fprintf(w, "data: Error: %s\n\n", msg)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
return nil, nil
}
// process 处理用户请求
// friendlyChatStreamError 面向用户的短错误文案(单行,适合 SSE
func friendlyChatStreamError(err error) string {
if err == nil {
return "未知错误"
}
msg := err.Error()
// 去掉 "chat stream round N: " 包装,突出真正原因
const wrap = "chat stream round "
if i := strings.Index(msg, wrap); i >= 0 {
rest := msg[i+len(wrap):]
if j := strings.Index(rest, ": "); j >= 0 {
msg = rest[j+2:]
}
}
msg = strings.ReplaceAll(msg, "\r\n", " ")
msg = strings.ReplaceAll(msg, "\n", " ")
return strings.Join(strings.Fields(msg), " ")
}
// process 处理用户请求(多轮工具调用,直到模型不再发 tool_calls 或达到上限)
func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCredential, req *api.LLMMCPAgentRequestInput, onStream func(string) error) (*api.MCPAgentResponse, error) {
// 获取 MCP Server 的工具列表
mcpServerUrl, err := mcp.GetMcpServerUrl(ctx, userCred)
if err != nil {
return nil, errors.Wrap(err, "GetMcpServerUrl")
@@ -516,41 +563,42 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
return nil, errors.Wrap(err, "list MCP tools")
}
log.Infof("Got %d tools from MCP Server", len(mcpTools))
if onStream != nil {
_ = onStream("正在准备…\n")
}
// get llmClient
llmClient := mcp.GetLLMClientDriver()
if llmClient == nil {
return nil, errors.Error("failed to get LLM client driver")
}
tools := llmClient.ConvertMCPTools(mcpTools)
// 构建系统提示词
systemPrompt := buildSystemPrompt()
// 初始化消息历史
messages := make([]ILLMChatMessage, 0)
messages = append(messages, llmClient.NewSystemMessage(systemPrompt))
// 处理历史消息
messages = append(messages, llmClient.NewSystemMessage(buildSystemPrompt()))
if len(req.History) > 0 {
historyMessages := processHistoryMessages(
messages = append(messages, processHistoryMessages(
req.History,
llmClient,
options.Options.MCPAgentUserCharLimit,
options.Options.MCPAgentAssistantCharLimit,
)
messages = append(messages, historyMessages...)
)...)
}
messages = append(messages, llmClient.NewUserMessage(req.Message))
// 记录工具调用
maxRounds := options.Options.MCPAgentMaxToolRounds
if maxRounds <= 0 {
maxRounds = 8
}
var toolCallRecords []api.MCPAgentToolCallRecord
var finalAnswer strings.Builder
nudged := false
resourceOp := looksLikeResourceOperation(req.Message)
labels := newProgressLabelCache()
log.Infof("Phase 1: Thinking & Acting...")
for round := 1; round <= maxRounds; round++ {
log.Infof("MCP agent tool round %d/%d", round, maxRounds)
// 处理流式的工具调用参数
type accumToolCall struct {
Id string
Name string
@@ -560,18 +608,19 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
var accumulatedContent strings.Builder
var accumulatedReasoning strings.Builder
hasToolCalls := false
// 首轮资源操作可能被 nudge先不流式避免把「计划文案」推给用户
optimisticStream := !(round == 1 && resourceOp && !nudged && len(tools) > 0)
streamedAnswer := false
// 有 tool_calls 时不推送中间文案;纯文本最终答复边生成边推送
err = llmClient.ChatStream(ctx, mcp, messages, tools, func(chunk ILLMChatResponse) error {
if chunk.HasToolCalls() {
hasToolCalls = true
for _, tc := range chunk.GetToolCalls() {
idx := tc.GetIndex()
if _, exists := accToolCalls[idx]; !exists {
accToolCalls[idx] = &accumToolCall{
Id: tc.GetId(),
accToolCalls[idx] = &accumToolCall{Id: tc.GetId()}
}
}
atc := accToolCalls[idx]
if id := tc.GetId(); id != "" {
atc.Id = id
@@ -584,15 +633,14 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
}
}
}
if r := chunk.GetReasoningContent(); len(r) > 0 {
accumulatedReasoning.WriteString(r)
}
content := chunk.GetContent()
if len(content) > 0 {
if content := chunk.GetContent(); len(content) > 0 {
accumulatedContent.WriteString(content)
if onStream != nil {
// 尚未出现 tool_calls 时按 token 增量推送,避免最终结果整段一次性返回
if onStream != nil && optimisticStream && !hasToolCalls {
streamedAnswer = true
if err := onStream(content); err != nil {
return err
}
@@ -600,44 +648,58 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "phase 1 chat stream error")
return nil, errors.Wrapf(err, "chat stream round %d", round)
}
// 检查是否有工具调用
if !hasToolCalls {
// 如果阶段一没有调用工具,直接返回结果
answer := accumulatedContent.String()
// 首轮对资源操作却不调工具:强制再试一轮,要求发 tool_calls
if round == 1 && resourceOp && !nudged && len(tools) > 0 {
nudged = true
log.Warningf("MCP agent round1 returned no tool_calls for resource op; nudging model")
if answer != "" {
messages = append(messages, llmClient.NewAssistantMessage(answer))
}
messages = append(messages, llmClient.NewUserMessage(
"请立刻调用合适的 climc_* 工具完成我的请求,不要只描述计划或编造查询结果。若要创建虚拟机,请从 climc_cloud_region_list 开始连续调用直到 climc_server_create。",
))
continue
}
// 未走增量推送时的兜底(例如首轮 nudge 关闭了 optimisticStream
if onStream != nil && !streamedAnswer && answer != "" {
if err := onStream(answer); err != nil {
return nil, err
}
}
finalAnswer.WriteString(answer)
return &api.MCPAgentResponse{
Success: true,
Answer: accumulatedContent.String(),
Answer: finalAnswer.String(),
ToolCalls: toolCallRecords,
}, nil
}
// Convert accumulated tool calls to ILLMToolCall
var toolCalls []ILLMToolCall
// Find max index
toolCalls := make([]ILLMToolCall, 0)
maxIdx := -1
for idx := range accToolCalls {
if idx > maxIdx {
maxIdx = idx
}
}
for i := 0; i <= maxIdx; i++ {
if atc, ok := accToolCalls[i]; ok {
var args map[string]interface{}
atc, ok := accToolCalls[i]
if !ok {
continue
}
args := make(map[string]interface{})
rawArgs := atc.RawArguments.String()
if len(rawArgs) > 0 {
if err := json.Unmarshal([]byte(rawArgs), &args); err != nil {
log.Errorf("Failed to unmarshal arguments for tool %s: %v. Raw: %s", atc.Name, err, rawArgs)
args = make(map[string]interface{})
}
} else {
args = make(map[string]interface{})
}
toolCalls = append(toolCalls, &SLLMToolCall{
Id: atc.Id,
Function: SLLMFunctionCall{
@@ -646,51 +708,75 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden
},
})
}
}
log.Infof("Got %d tool calls from Phase 1", len(toolCalls))
log.Infof("Round %d got %d tool calls", round, len(toolCalls))
toolCallRecords, toolMessages, err := processToolCalls(ctx, toolCalls, accumulatedReasoning.String(), accumulatedContent.String(), mcpClient, llmClient)
records, toolMessages, err := processToolCalls(ctx, toolCalls, accumulatedReasoning.String(), accumulatedContent.String(), mcpClient, llmClient, onStream, labels)
if err != nil {
return nil, errors.Wrap(err, "process tool calls")
}
// 将工具调用相关的消息加入历史
toolCallRecords = append(toolCallRecords, records...)
messages = append(messages, toolMessages...)
log.Infof("Phase 2: Streaming Response...")
var finalAnswer strings.Builder
err = llmClient.ChatStream(ctx, mcp, messages, tools, func(chunk ILLMChatResponse) error {
content := chunk.GetContent()
if len(content) > 0 {
// 聚合最终答案
finalAnswer.WriteString(content)
// 实时流式输出
if onStream != nil {
if err := onStream(content); err != nil {
return err
}
// 工具轮次用尽后,再给模型一轮纯文本总结(不再传 tools避免只回“达到上限”而不解释最后一次工具错误
log.Infof("MCP agent tool rounds exhausted (%d); requesting final summary without tools", maxRounds)
messages = append(messages, llmClient.NewUserMessage(
"工具调用轮次已用尽。请根据上述工具返回结果,用中文向用户总结成功或失败原因;若创建失败请说明关键错误(如 sched_fail与建议不要再调用工具。",
))
var summary strings.Builder
err = llmClient.ChatStream(ctx, mcp, messages, nil, func(chunk ILLMChatResponse) error {
if content := chunk.GetContent(); len(content) > 0 {
summary.WriteString(content)
if onStream != nil {
return onStream(content)
}
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "phase 2 stream error")
log.Warningf("MCP agent final summary failed: %v", err)
msg := fmt.Sprintf("已达到最大工具调用轮次(%d),请根据已有结果继续或缩小请求范围。", maxRounds)
if onStream != nil {
_ = onStream(msg)
}
return &api.MCPAgentResponse{
Success: true,
Answer: finalAnswer.String(),
Answer: msg,
ToolCalls: toolCallRecords,
}, nil
}
answer := strings.TrimSpace(summary.String())
if answer == "" {
answer = fmt.Sprintf("已达到最大工具调用轮次(%d),请根据已有结果继续或缩小请求范围。", maxRounds)
if onStream != nil {
_ = onStream(answer)
}
}
return &api.MCPAgentResponse{
Success: true,
Answer: answer,
ToolCalls: toolCallRecords,
}, nil
}
// buildSystemPrompt 构建系统提示词
func looksLikeResourceOperation(msg string) bool {
m := strings.ToLower(msg)
keys := []string{
"创建", "查询", "列出", "列表", "启动", "停止", "重启", "删除", "销毁",
"虚拟机", "主机", "镜像", "网络", "区域", "套餐", "规格", "密码",
"create", "list", "start", "stop", "restart", "delete", "server", "vm",
}
for _, k := range keys {
if strings.Contains(m, k) {
return true
}
}
return false
}
// buildSystemPrompt 构建系统提示词(平台名来自 BaseOptions.PlatformName支持热更新
func buildSystemPrompt() string {
return api.MCP_AGENT_SYSTEM_PROMPT
return fmt.Sprintf(api.MCP_AGENT_SYSTEM_PROMPT, options.ResolvedPlatformName())
}
func processHistoryMessages(
@@ -744,6 +830,8 @@ func processToolCalls(
reasoningContent, content string,
mcpClient *utils.MCPClient,
llmClient ILLMClient,
onStream func(string) error,
labels *progressLabelCache,
) ([]api.MCPAgentToolCallRecord, []ILLMChatMessage, error) {
toolCallRecords := make([]api.MCPAgentToolCallRecord, 0)
messagesToAdd := make([]ILLMChatMessage, 0)
@@ -775,6 +863,15 @@ func processToolCalls(
Result: resultText,
})
labels.rememberFromTool(toolName, resultText)
// 向用户流式展示资源选择/查询摘要,而不是工具名
if onStream != nil {
if progress := summarizeToolProgress(toolName, arguments, resultText, labels); progress != "" {
_ = onStream(progress)
}
}
// 将工具执行结果加入历史
messagesToAdd = append(messagesToAdd, llmClient.NewToolMessage(tc.GetId(), toolName, resultText))
}

View File

@@ -0,0 +1,624 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"encoding/json"
"fmt"
"strings"
)
// progressLabelCache 缓存 list 结果中的 id→可读名称供后续进度文案使用。
type progressLabelCache struct {
regions map[string]string
}
func newProgressLabelCache() *progressLabelCache {
return &progressLabelCache{regions: make(map[string]string)}
}
func (c *progressLabelCache) rememberFromTool(toolName, resultText string) {
if c == nil || isToolResultError(resultText) {
return
}
name := strings.TrimPrefix(toolName, "climc_")
if strings.Contains(name, "cloud_region_list") {
c.rememberRegions(resultText)
}
}
func (c *progressLabelCache) rememberRegions(resultText string) {
if c.regions == nil {
c.regions = make(map[string]string)
}
items, _ := extractListItems(resultText)
for _, item := range items {
id := jsonString(item, "id")
name := jsonString(item, "name")
if id == "" || name == "" {
continue
}
ext := jsonString(item, "external_id")
label := name
if ext != "" && !strings.EqualFold(ext, name) {
label = fmt.Sprintf("%s%s", name, ext)
}
c.regions[id] = label
c.regions[name] = label
if ext != "" {
c.regions[ext] = label
}
}
}
func (c *progressLabelCache) regionLabel(idOrName string) string {
if idOrName == "" {
return ""
}
if c != nil && c.regions != nil {
if v := c.regions[idOrName]; v != "" {
return v
}
}
return idOrName
}
// summarizeToolProgress 将工具调用结果整理成面向用户的进度文案(逐项展示资源,而非工具名)。
func summarizeToolProgress(toolName string, args map[string]interface{}, resultText string, labels *progressLabelCache) string {
name := strings.TrimPrefix(toolName, "climc_")
if isToolResultError(resultText) {
return fmt.Sprintf("✗ %s失败%s\n", progressLabel(name), truncateRunes(stripMCPHint(resultText), 180))
}
switch {
case name == "docs_search" || strings.HasSuffix(name, "docs_search"):
return formatResourceListProgress("文档", resultText, []string{"name", "title", "path"})
case name == "docs_get" || strings.HasSuffix(name, "docs_get"):
path := firstArg(args, "path", "PATH")
if path != "" {
return fmt.Sprintf("✓ 已阅读文档:%s\n", path)
}
return "✓ 已阅读文档\n"
case strings.Contains(name, "cloud_region_capability"):
id := firstArg(args, "id", "ID", "name")
region := labels.regionLabel(id)
types := extractStorageTypeHints(resultText)
if region != "" && types != "" {
return fmt.Sprintf("✓ 区域 %s 可用磁盘类型:%s\n", region, types)
}
if types != "" {
return fmt.Sprintf("✓ 可用磁盘类型:%s\n", types)
}
return fmt.Sprintf("✓ 已查询区域能力%s\n", paren(region))
case strings.Contains(name, "cloud_region_list"):
return formatResourceListProgress("区域", resultText, []string{"name", "external_id", "id"})
case strings.Contains(name, "cached_image_list"), strings.Contains(name, "image_list"):
return formatResourceListProgress("镜像", resultText, []string{"name", "os_type", "os_distribution", "id"})
case strings.Contains(name, "server_sku_list"):
return formatResourceListProgress("套餐", resultText, []string{"name", "instance_type_category", "cpu_core_count", "memory_size_mb", "id"})
case strings.Contains(name, "network_list"):
return formatResourceListProgress("网络", resultText, []string{"name", "guest_ip_prefix", "vpc", "id"})
case strings.Contains(name, "vpc_list"):
return formatResourceListProgress("VPC", resultText, []string{"name", "cidr_block", "id"})
case strings.Contains(name, "storage_list"):
return formatResourceListProgress("存储", resultText, []string{"name", "storage_type", "capacity", "id"})
case strings.Contains(name, "server_list"):
return formatResourceListProgress("虚拟机", resultText, []string{"name", "status", "id"})
case strings.Contains(name, "server_create"):
return formatServerCreateProgress(args, resultText, labels)
case strings.Contains(name, "server_show"):
return formatSingleResourceProgress("虚拟机详情", resultText, []string{"name", "status", "id"})
case strings.HasPrefix(name, "server_"):
id := firstArg(args, "id", "ID", "name")
action := strings.TrimPrefix(name, "server_")
return fmt.Sprintf("✓ 虚拟机%s%s\n", actionLabel(action), paren(id))
default:
return formatGenericProgress(name, args, resultText)
}
}
func isToolResultError(resultText string) bool {
s := strings.TrimSpace(resultText)
return strings.Contains(s, "调用失败") ||
strings.Contains(s, "返回错误") ||
strings.HasPrefix(s, "工具 ") && strings.Contains(s, "失败")
}
func progressLabel(toolName string) string {
switch {
case strings.Contains(toolName, "cloud_region_list"):
return "查询区域"
case strings.Contains(toolName, "cloud_region_capability"):
return "查询区域能力"
case strings.Contains(toolName, "cached_image"):
return "查询镜像"
case strings.Contains(toolName, "image_list"):
return "查询镜像"
case strings.Contains(toolName, "server_sku"):
return "查询套餐"
case strings.Contains(toolName, "network_list"):
return "查询网络"
case strings.Contains(toolName, "vpc_list"):
return "查询 VPC"
case strings.Contains(toolName, "server_create"):
return "创建虚拟机"
default:
return toolName
}
}
func actionLabel(action string) string {
switch action {
case "start":
return "已启动"
case "stop":
return "已停止"
case "restart":
return "已重启"
case "delete":
return "已删除"
case "set_password", "set-password":
return "已重置密码"
default:
return "操作完成"
}
}
func formatServerCreateProgress(args map[string]interface{}, resultText string, labels *progressLabelCache) string {
var b strings.Builder
b.WriteString("✓ 选用资源创建虚拟机:")
parts := make([]string, 0, 6)
if v := firstArg(args, "name", "NAME"); v != "" {
if isTruthy(args["generate-name"]) || isTruthy(args["generate_name"]) || isTruthy(args["GenerateName"]) {
parts = append(parts, "名称模板="+v+"(自动去重)")
} else {
parts = append(parts, "名称="+v)
}
}
if v := firstArg(args, "hypervisor"); v != "" {
parts = append(parts, "平台="+v)
}
if v := firstArg(args, "prefer-region", "prefer_region", "region"); v != "" {
parts = append(parts, "区域="+labels.regionLabel(v))
}
if v := firstArg(args, "instance-type", "instance_type", "sku"); v != "" {
parts = append(parts, "规格="+v)
}
if v := firstArg(args, "ncpu"); v != "" {
parts = append(parts, "CPU="+v)
}
if v := firstArg(args, "mem-spec", "mem_spec"); v != "" {
parts = append(parts, "内存="+v)
}
if disks := argStringSlice(args, "disk"); len(disks) > 0 {
parts = append(parts, "磁盘="+truncateRunes(disks[0], 80))
}
if nets := argStringSlice(args, "net"); len(nets) > 0 {
parts = append(parts, "网络="+strings.Join(nets, ","))
} else {
parts = append(parts, "网络=自动调度")
}
b.WriteString(strings.Join(parts, ""))
b.WriteByte('\n')
body := stripMCPHint(resultText)
if obj := parseJSONObject(body); obj != nil {
status := jsonString(obj, "final_status")
sid := jsonString(obj, "server_id")
sname := ""
if sid == "" {
if srv, ok := obj["server"].(map[string]interface{}); ok {
sid = jsonString(srv, "id")
sname = jsonString(srv, "name")
if status == "" {
status = jsonString(srv, "status")
}
}
} else if srv, ok := obj["server"].(map[string]interface{}); ok {
sname = jsonString(srv, "name")
}
if waitErr := jsonString(obj, "wait_error"); waitErr != "" {
b.WriteString(fmt.Sprintf(" 创建未完成:%s\n", truncateRunes(waitErr, 160)))
if hint := jsonString(obj, "hint"); hint != "" {
b.WriteString(fmt.Sprintf(" 提示:%s\n", truncateRunes(hint, 160)))
}
return b.String()
}
if sid != "" || status != "" || sname != "" {
b.WriteString(" 结果:")
bits := make([]string, 0, 3)
if sname != "" {
bits = append(bits, "名称="+sname)
}
if sid != "" {
bits = append(bits, "id="+sid)
}
if status != "" {
bits = append(bits, "状态="+status)
}
b.WriteString(strings.Join(bits, ""))
b.WriteByte('\n')
}
}
return b.String()
}
func isTruthy(v interface{}) bool {
switch x := v.(type) {
case bool:
return x
case string:
s := strings.ToLower(strings.TrimSpace(x))
return s == "true" || s == "1" || s == "yes" || s == "on"
case float64:
return x != 0
case int:
return x != 0
default:
return false
}
}
func formatResourceListProgress(kind, resultText string, fields []string) string {
items, total := extractListItems(resultText)
if len(items) == 0 {
if total == 0 {
return fmt.Sprintf("✓ 未找到可用%s\n", kind)
}
return fmt.Sprintf("✓ 已查询%s共 %d 条)\n", kind, total)
}
if total <= 0 {
total = len(items)
}
labels := make([]string, 0, 5)
for i, item := range items {
if i >= 5 {
break
}
labels = append(labels, formatItemLabel(item, fields))
}
more := ""
if total > len(labels) {
more = fmt.Sprintf("等共 %d 个", total)
} else {
more = fmt.Sprintf("共 %d 个", total)
}
return fmt.Sprintf("✓ 已找到%s%s%s\n", kind, strings.Join(labels, "、"), more)
}
func formatSingleResourceProgress(kind, resultText string, fields []string) string {
obj := parseJSONObject(stripMCPHint(resultText))
if obj == nil {
return fmt.Sprintf("✓ 已获取%s\n", kind)
}
return fmt.Sprintf("✓ %s%s\n", kind, formatItemLabel(obj, fields))
}
func formatGenericProgress(toolName string, args map[string]interface{}, resultText string) string {
id := firstArg(args, "id", "ID", "name", "NAME")
items, total := extractListItems(resultText)
if len(items) > 0 {
return formatResourceListProgress(progressLabel(toolName), resultText, []string{"name", "id"})
}
if id != "" {
return fmt.Sprintf("✓ %s完成%s\n", progressLabel(toolName), paren(id))
}
_ = resultText
if total > 0 {
return fmt.Sprintf("✓ %s完成%d 条)\n", progressLabel(toolName), total)
}
return fmt.Sprintf("✓ %s完成\n", progressLabel(toolName))
}
func formatItemLabel(item map[string]interface{}, fields []string) string {
parts := make([]string, 0, 3)
seen := map[string]bool{}
for _, f := range fields {
v := jsonString(item, f)
if v == "" || seen[v] {
continue
}
seen[v] = true
parts = append(parts, v)
if len(parts) >= 2 {
break
}
}
if len(parts) == 0 {
return "(未命名)"
}
if len(parts) == 1 {
return parts[0]
}
return fmt.Sprintf("%s(%s)", parts[0], parts[1])
}
func extractListItems(resultText string) ([]map[string]interface{}, int) {
body := stripMCPHint(resultText)
obj := parseJSONObject(body)
if obj != nil {
total := jsonInt(obj, "total")
if total <= 0 {
total = jsonInt(obj, "count")
}
for _, key := range []string{"data", "hits"} {
data, ok := obj[key].([]interface{})
if !ok {
continue
}
items := make([]map[string]interface{}, 0, len(data))
for _, d := range data {
if m, ok := d.(map[string]interface{}); ok {
items = append(items, m)
}
}
if total <= 0 {
total = len(items)
}
return items, total
}
// 单对象结果
if jsonString(obj, "id") != "" || jsonString(obj, "name") != "" {
return []map[string]interface{}{obj}, 1
}
}
if arr := parseJSONArray(body); len(arr) > 0 {
items := make([]map[string]interface{}, 0, len(arr))
for _, d := range arr {
if m, ok := d.(map[string]interface{}); ok {
items = append(items, m)
}
}
return items, len(items)
}
return nil, 0
}
func extractStorageTypeHints(resultText string) string {
obj := parseJSONObject(stripMCPHint(resultText))
if obj == nil {
return ""
}
for _, key := range []string{"storage_types2", "StorageTypes2"} {
raw, ok := obj[key]
if !ok {
continue
}
m, ok := raw.(map[string]interface{})
if !ok {
continue
}
set := make([]string, 0, 8)
seen := map[string]bool{}
for _, v := range m {
arr, ok := v.([]interface{})
if !ok {
continue
}
for _, x := range arr {
s, ok := x.(string)
if !ok || s == "" || seen[s] {
continue
}
seen[s] = true
if i := strings.Index(s, "/"); i > 0 {
s = s[:i]
}
set = append(set, s)
if len(set) >= 6 {
return strings.Join(set, "、")
}
}
}
if len(set) > 0 {
return strings.Join(set, "、")
}
}
return ""
}
func stripMCPHint(s string) string {
s = strings.TrimSpace(s)
if i := strings.Index(s, "[MCP下一步]"); i >= 0 {
s = strings.TrimSpace(s[:i])
}
return s
}
func parseJSONObject(s string) map[string]interface{} {
s = strings.TrimSpace(s)
if s == "" || s[0] != '{' {
// 可能前后有非 JSON 文本,尝试截取第一个对象
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start < 0 || end <= start {
return nil
}
s = s[start : end+1]
}
var obj map[string]interface{}
if err := json.Unmarshal([]byte(s), &obj); err != nil {
return nil
}
return obj
}
func parseJSONArray(s string) []interface{} {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
if s[0] != '[' {
start := strings.Index(s, "[")
end := strings.LastIndex(s, "]")
if start < 0 || end <= start {
return nil
}
s = s[start : end+1]
}
var arr []interface{}
if err := json.Unmarshal([]byte(s), &arr); err != nil {
return nil
}
return arr
}
func firstArg(args map[string]interface{}, keys ...string) string {
if args == nil {
return ""
}
normalize := func(k string) string {
return strings.ReplaceAll(strings.ToLower(k), "_", "-")
}
for _, key := range keys {
if v, ok := args[key]; ok {
if s := stringifyArg(v); s != "" {
return s
}
}
want := normalize(key)
for k, v := range args {
if normalize(k) == want {
if s := stringifyArg(v); s != "" {
return s
}
}
}
}
return ""
}
func argStringSlice(args map[string]interface{}, key string) []string {
if args == nil {
return nil
}
v, ok := args[key]
if !ok {
alt := strings.ReplaceAll(key, "-", "_")
v, ok = args[alt]
if !ok {
return nil
}
}
switch x := v.(type) {
case []string:
return x
case []interface{}:
out := make([]string, 0, len(x))
for _, item := range x {
if s := stringifyArg(item); s != "" {
out = append(out, s)
}
}
return out
case string:
if x != "" {
return []string{x}
}
}
return nil
}
func stringifyArg(v interface{}) string {
switch x := v.(type) {
case string:
return strings.TrimSpace(x)
case float64:
if x == float64(int64(x)) {
return fmt.Sprintf("%d", int64(x))
}
return fmt.Sprintf("%v", x)
case int:
return fmt.Sprintf("%d", x)
case int64:
return fmt.Sprintf("%d", x)
case bool:
return fmt.Sprintf("%v", x)
case []interface{}:
if len(x) == 0 {
return ""
}
return stringifyArg(x[0])
case []string:
if len(x) == 0 {
return ""
}
return x[0]
default:
return strings.TrimSpace(fmt.Sprint(x))
}
}
func jsonString(m map[string]interface{}, key string) string {
if m == nil {
return ""
}
v, ok := m[key]
if !ok {
return ""
}
return stringifyArg(v)
}
func jsonInt(m map[string]interface{}, key string) int {
if m == nil {
return 0
}
v, ok := m[key]
if !ok {
return 0
}
switch x := v.(type) {
case float64:
return int(x)
case int:
return x
case int64:
return int(x)
case json.Number:
n, _ := x.Int64()
return int(n)
default:
return 0
}
}
func paren(s string) string {
if s == "" {
return ""
}
return "" + s + ""
}
func truncateRunes(s string, max int) string {
rs := []rune(strings.TrimSpace(s))
if max <= 0 || len(rs) <= max {
return string(rs)
}
return string(rs[:max]) + "…"
}

View File

@@ -0,0 +1,69 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"strings"
"testing"
)
func TestSummarizeToolProgressRegionList(t *testing.T) {
result := `{
"total": 2,
"data": [
{"id": "r1", "name": "北京", "external_id": "cn-beijing"},
{"id": "r2", "name": "上海", "external_id": "cn-shanghai"}
]
}
[MCP下一步] ignore`
got := summarizeToolProgress("climc_cloud_region_list", nil, result, nil)
if !strings.Contains(got, "已找到区域") || !strings.Contains(got, "北京") {
t.Fatalf("got %q", got)
}
if strings.Contains(got, "正在调用工具") {
t.Fatalf("should not mention tool name style: %q", got)
}
}
func TestSummarizeToolProgressCreate(t *testing.T) {
args := map[string]interface{}{
"name": "ubuntu-22-04",
"hypervisor": "aliyun",
"prefer-region": "r1",
"instance-type": "ecs.t6-c1m1.large",
"disk": []interface{}{"size=30g,image=img1,backend=cloud_essd"},
}
result := `{"server_id":"s1","final_status":"running","server":{"id":"s1","status":"running"}}`
got := summarizeToolProgress("climc_server_create", args, result, nil)
if !strings.Contains(got, "选用资源创建虚拟机") || !strings.Contains(got, "ubuntu-22-04") {
t.Fatalf("got %q", got)
}
if !strings.Contains(got, "状态=running") {
t.Fatalf("expected final status in %q", got)
}
}
func TestSummarizeToolProgressCapability(t *testing.T) {
labels := newProgressLabelCache()
labels.regions["reg-1"] = "华东1杭州"
result := `{"storage_types2":{"aliyun":["cloud_essd/ssd","cloud_ssd/ssd"]}}`
got := summarizeToolProgress("climc_cloud_region_capability", map[string]interface{}{"id": "reg-1"}, result, labels)
if !strings.Contains(got, "cloud_essd") || !strings.Contains(got, "华东1杭州") {
t.Fatalf("got %q", got)
}
if strings.Contains(got, "reg-1") {
t.Fatalf("should show region name not id: %q", got)
}
}

View File

@@ -14,7 +14,11 @@
package options
import common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
import (
"strings"
common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
)
type LLMOptions struct {
common_options.CommonOptions
@@ -49,10 +53,11 @@ type LLMOptions struct {
// MCP Agent 配置
MCPServerURL string `help:"MCP Server URL" default:"http://default-mcp-server:30876"`
MCPAgentTimeout int `help:"MCP Agent request timeout in seconds" default:"120"`
MCPAgentTimeout int `help:"MCP Agent request timeout in seconds" default:"180"`
MCPAgentUserCharLimit int `help:"MCP Agent user char limit" default:"3200"`
MCPAgentAssistantCharLimit int `help:"MCP Agent assistant char limit" default:"6400"`
MCPAgentMaxToolRounds int `help:"Max MCP tool-call rounds per chat request" default:"16"`
// LLM model catalog (browsable curated entries). Value can be either an
// http(s) URL or a local file path; sources without an http:// or https://
@@ -73,3 +78,28 @@ type LLMOptions struct {
var (
Options LLMOptions
)
const DefaultPlatformName = "Cloudpods"
// ResolvedPlatformName 返回配置中的平台展示名,空则回退 DefaultPlatformName。
func ResolvedPlatformName() string {
name := strings.TrimSpace(Options.PlatformName)
if name == "" {
return DefaultPlatformName
}
return name
}
func OnOptionsChange(oldO, newO interface{}) bool {
oldOpts := oldO.(*LLMOptions)
newOpts := newO.(*LLMOptions)
changed := false
if common_options.OnCommonOptionsChange(&oldOpts.CommonOptions, &newOpts.CommonOptions) {
changed = true
}
if common_options.OnDBOptionsChange(&oldOpts.DBOptions, &newOpts.DBOptions) {
changed = true
}
return changed
}

View File

@@ -42,6 +42,7 @@ func StartService() {
app_common.InitAuth(commonOpts, func() {
log.Infof("Auth complete!!")
})
common_options.StartOptionManager(opts, opts.ConfigSyncPeriodSeconds, api.SERVICE_TYPE, api.SERVICE_VERSION, options.OnOptionsChange)
app := app_common.InitApp(&opts.BaseOptions, false)

View File

@@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
@@ -32,6 +33,8 @@ import (
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/llm/options"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
)
@@ -60,24 +63,51 @@ type MCPClient struct {
messageID int64
mu sync.Mutex
initialized bool
closed atomic.Bool
userCred mcclient.TokenCredential
// requestTimeout 单次 JSON-RPC含 tools/call等待 SSE 回包的上限。
// 须覆盖 climc_server_create 的 forecast+等待ServerCreateWaitSeconds且小于整段 chat 超时。
requestTimeout time.Duration
pendingReqs map[int64]chan *rawMCPResponse
reqMu sync.Mutex
}
// NewMCPClient 创建一个新的 MCP 客户端
// NewMCPClient 创建一个新的 MCP 客户端
// timeout 为单次 RPC 等待 SSE 响应的超时SSE 长连接本身不设整体 Timeout避免读 body 被提前掐断。
func NewMCPClient(serverURL string, timeout time.Duration, userCred mcclient.TokenCredential) *MCPClient {
if timeout <= 0 {
timeout = time.Duration(options.Options.MCPAgentTimeout) * time.Second
}
if timeout <= 0 {
timeout = 3 * time.Minute
}
return &MCPClient{
serverURL: strings.TrimSuffix(serverURL, "/"),
client: &http.Client{
Timeout: timeout,
Timeout: 0,
Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: 30 * time.Second}).DialContext,
ResponseHeaderTimeout: 60 * time.Second,
},
},
requestTimeout: timeout,
userCred: userCred,
pendingReqs: make(map[int64]chan *rawMCPResponse),
}
}
// setAuthHeaders 将当前用户 token 写入请求头,供 mcp-server SSE/message 鉴权。
func (c *MCPClient) setAuthHeaders(req *http.Request) {
if c.userCred == nil {
return
}
if tok := strings.TrimSpace(c.userCred.GetTokenString()); tok != "" {
req.Header.Set(identity.AUTH_TOKEN_HEADER, tok)
}
}
// connectSSE 连接 SSE 端点并开始事件循环
func (c *MCPClient) connectSSE(ctx context.Context) error {
// 连接 SSE 端点获取 session URL
@@ -88,6 +118,7 @@ func (c *MCPClient) connectSSE(ctx context.Context) error {
}
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
c.setAuthHeaders(req)
resp, err := c.client.Do(req)
if err != nil {
@@ -95,8 +126,8 @@ func (c *MCPClient) connectSSE(ctx context.Context) error {
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return errors.Errorf("SSE connection failed with status %d: %s", resp.StatusCode, string(body))
}
@@ -125,7 +156,8 @@ func (c *MCPClient) connectSSE(ctx context.Context) error {
if err != nil {
if !foundSession {
initErr = err
} else {
} else if !isExpectedSSEClose(err) && !c.closed.Load() {
// 主动 Close / 对端正常结束时不刷告警
log.Warningf("SSE connection closed: %v", err)
}
return
@@ -212,7 +244,7 @@ func (c *MCPClient) Initialize(ctx context.Context) error {
ProtocolVersion: "2024-11-05",
Capabilities: mcp.ClientCapabilities{},
ClientInfo: mcp.Implementation{
Name: "cloudpods-mcp-agent",
Name: fmt.Sprintf("%s-mcp-agent", options.ResolvedPlatformName()),
Version: "1.0.0",
},
}
@@ -318,7 +350,22 @@ func (c *MCPClient) sendRequest(ctx context.Context, req mcp.JSONRPCRequest) (*r
return &mcpResp, nil
}
// 如果响应为空,等待 SSE 推送
// 如果响应为空,等待 SSE 推送/message 常返回空 body结果走 SSE
wait := c.requestTimeout
if wait <= 0 {
wait = 3 * time.Minute
}
if deadline, ok := ctx.Deadline(); ok {
remain := time.Until(deadline)
if remain <= 0 {
return nil, ctx.Err()
}
if remain < wait {
wait = remain
}
}
timer := time.NewTimer(wait)
defer timer.Stop()
select {
case mcpResp := <-respChan:
log.Debugf("MCP response (SSE): ID=%v", mcpResp.ID)
@@ -328,8 +375,8 @@ func (c *MCPClient) sendRequest(ctx context.Context, req mcp.JSONRPCRequest) (*r
return mcpResp, nil
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(30 * time.Second):
return nil, errors.Error("timeout waiting for SSE response")
case <-timer.C:
return nil, errors.Errorf("timeout waiting for SSE response after %s", wait)
}
}
@@ -423,14 +470,29 @@ func FormatToolResult(toolName string, result *mcp.CallToolResult, err error) st
return GetToolResultText(result)
}
// isExpectedSSEClose 判断是否为正常关闭(主动 Close / EOF / 连接已关)
func isExpectedSSEClose(err error) bool {
if err == nil {
return true
}
if errors.Cause(err) == io.EOF || errors.Cause(err) == net.ErrClosed {
return true
}
msg := err.Error()
return strings.Contains(msg, "use of closed network connection") ||
strings.Contains(msg, "closed network connection") ||
strings.Contains(msg, "http: read on closed response body")
}
// Close 关闭客户端连接
func (c *MCPClient) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.closed.Store(true)
c.initialized = false
c.sessionURL = ""
if c.sseBody != nil {
c.sseBody.Close()
_ = c.sseBody.Close()
c.sseBody = nil
}
return nil

View File

@@ -198,27 +198,29 @@ const (
ListOrderDesc = "desc"
)
// BaseListOptions 通用列表参数。
// 字段上的 mcp:"true" 表示该参数应对 AI/MCP 暴露(见 pkg/mcp-server/climcgen
type BaseListOptions struct {
Limit *int `default:"20" help:"Page limit"`
Offset *int `default:"0" help:"Page offset"`
OrderBy []string `help:"Name of the field to be ordered by"`
Order string `help:"List order" choices:"desc|asc"`
Details *bool `help:"Show more details" default:"false"`
Limit *int `default:"20" help:"Page limit" mcp:"true"`
Offset *int `default:"0" help:"Page offset" mcp:"true"`
OrderBy []string `help:"Name of the field to be ordered by" mcp:"true"`
Order string `help:"List order" choices:"desc|asc" mcp:"true"`
Details *bool `help:"Show more details" default:"false" mcp:"true"`
ShowFailReason *bool `help:"show fail reason fields"`
Search string `help:"Filter results by a simple keyword search"`
Search string `help:"Filter results by a simple keyword search" mcp:"true"`
Meta *bool `help:"Piggyback metadata information" json:"with_meta" token:"meta"`
Filter []string `help:"Filters"`
Filter []string `help:"Filters" mcp:"true"`
JointFilter []string `help:"Filters with joint table col; joint_tbl.related_key(origin_key).filter_col.filter_cond(filters)"`
FilterAny *bool `help:"If true, match if any of the filters matches; otherwise, match if all of the filters match"`
Admin *bool `help:"Is an admin call?"`
Tenant string `help:"Tenant ID or Name" alias:"project"`
Admin *bool `help:"Is an admin call?" mcp:"true"`
Tenant string `help:"Tenant ID or Name" alias:"project" mcp:"true"`
ProjectDomain string `help:"Project domain filter"`
User string `help:"User ID or Name"`
Field []string `help:"Show only specified fields"`
Scope string `help:"resource scope" choices:"system|domain|project|user"`
Field []string `help:"Show only specified fields" mcp:"true"`
Scope string `help:"resource scope" choices:"system|domain|project|user|max" mcp:"true"`
System *bool `help:"Show system resource"`
System *bool `help:"Show system resource" mcp:"true"`
PendingDelete *bool `help:"Show only pending deleted resources"`
PendingDeleteAll *bool `help:"Show also pending-deleted resources" json:"-"`
DeleteAll *bool `help:"Show also deleted resources" json:"-"`
@@ -245,7 +247,7 @@ type BaseListOptions struct {
Manager []string `help:"List objects belonging to the cloud provider" json:"manager,omitempty"`
Account string `help:"List objects belonging to the cloud account" json:"account,omitempty"`
Provider []string `help:"List objects from the provider" choices:"OneCloud|VMware|Aliyun|Apsara|Qcloud|Azure|Aws|Huawei|OpenStack|Ucloud|RockBase|VolcEngine|ZStack|Google|Ctyun|Cloudpods|Nutanix|BingoCloud|IncloudSphere|JDcloud|Proxmox|Ceph|CephFS|Ecloud|HCSO|HCS|HCSOP|H3C|S3|RemoteFile|Ksyun|Baidu|QingCloud|OracleCloud|SangFor|ZettaKit|UIS|CNWare" json:"provider,omitempty"`
Provider []string `help:"List objects from the provider;公有云创建时必传,例如 Aliyun、Aws、Huawei" choices:"OneCloud|VMware|Aliyun|Apsara|Qcloud|Azure|Aws|Huawei|OpenStack|Ucloud|RockBase|VolcEngine|ZStack|Google|Ctyun|Cloudpods|Nutanix|BingoCloud|IncloudSphere|JDcloud|Proxmox|Ceph|CephFS|Ecloud|HCSO|HCS|HCSOP|H3C|S3|RemoteFile|Ksyun|Baidu|QingCloud|OracleCloud|SangFor|ZettaKit|UIS|CNWare" json:"provider,omitempty" mcp:"true"`
Brand []string `help:"List objects belonging to a special brand"`
CloudEnv string `help:"Cloud environment" choices:"public|private|onpremise|private_or_onpremise" json:"cloud_env,omitempty"`
PublicCloud *bool `help:"List objects belonging to public cloud" json:"public_cloud"`
@@ -263,7 +265,7 @@ type BaseListOptions struct {
Id []string `help:"filter by id"`
// Name []string `help:"fitler by name"`
Status []string `help:"filter by status"`
Status []string `help:"filter by status" mcp:"true"`
SummaryStats bool `help:"show summary stats" json:"summary_stats"`
}

View File

@@ -21,14 +21,17 @@ import (
)
type CachedImageListOptions struct {
_ struct{} `mcp-desc:"【创建流程中的中间步骤】公有云/非KVM 选镜像。必须带 provider如 [\"Aliyun\"]region 必须传 climc_cloud_region_list 返回的 idUUID禁止传 cn-shanghai 这类云厂商 region code。不要用 ISO。查完后继续 network/sku最后 climc_server_create"`
options.BaseListOptions
ImageType string `help:"image type" choices:"system|customized|shared|market"`
ImageType string `help:"image type;公有云系统盘常用 system" choices:"system|customized|shared|market" mcp:"true"`
Region string `help:"show images cached at cloud region"`
Zone string `help:"show images cached at zone"`
// Region 序列化为 cloudregion_id优先传 cloudregion UUID
Region string `help:"cloudregion id推荐或 name不要传 cn-shanghai 这类外部 region code" json:"cloudregion_id" mcp:"true"`
Zone string `help:"show images cached at zone" mcp:"true"`
HostSchedtagId string `help:"filter cached image with host schedtag"`
Valid *bool `help:"valid cachedimage"`
HostSchedtagId string `help:"filter cached image with host schedtag" mcp:"true"`
Valid *bool `help:"valid cachedimage" mcp:"true"`
}
func (opts *CachedImageListOptions) Params() (jsonutils.JSONObject, error) {

View File

@@ -26,12 +26,14 @@ import (
)
type CloudaccountListOptions struct {
baseoptions.BaseListOptions
Capability []string `help:"capability filter" choices:"project|compute|network|loadbalancer|objectstore|rds|cache|event|tablestore"`
_ struct{} `mcp-desc:"列出云账号。可用 search/provider/status 等过滤;详情用 climc_cloud_account_show同步用 climc_cloud_account_sync"`
ReadOnly *bool `help:"filter read only account" negative:"no-read-only"`
baseoptions.BaseListOptions
Capability []string `help:"capability filter" choices:"project|compute|network|loadbalancer|objectstore|rds|cache|event|tablestore" mcp:"true"`
ReadOnly *bool `help:"filter read only account" negative:"no-read-only" mcp:"true"`
//DistinctField string `help:"distinct field"`
ProxySetting string `help:"Proxy setting id or name"`
ProxySetting string `help:"Proxy setting id or name" mcp:"true"`
// 按宿主机数量排序
OrderByHostCount string
// 按虚拟机数量排序
@@ -509,6 +511,13 @@ func (opts *SCloudAccountIdOptions) Params() (jsonutils.JSONObject, error) {
return nil, nil
}
// CloudaccountShowOptions 单独包装,避免 SCloudAccountIdOptions 被 delete/enable 等复用时误注册。
type CloudaccountShowOptions struct {
_ struct{} `mcp-desc:"查询云账号详情(含 sync_status、provider、余额等。ID 可用 climc_cloud_account_list 返回的 id/name"`
SCloudAccountIdOptions
}
type SVMwareCloudAccountUpdateCredentialOptions struct {
SCloudAccountIdOptions
SUserPasswordCredential
@@ -1182,9 +1191,18 @@ func (opts *CloudaccountUpdateCredentialOptions) Params() (jsonutils.JSONObject,
}
type CloudaccountSyncOptions struct {
_ struct{} `mcp-desc:"同步云账号资源(异步拉取公有云/私有云库存。ID 用 climc_cloud_account_list 返回的 id/name常用 force=true 强制同步,可用 region/resources 限定范围。调用后可用 climc_cloud_account_show 查看 sync_status"`
SCloudAccountIdOptions
api.SyncRangeInput
Force bool `help:"Force sync" json:"force" mcp:"true"`
FullSync bool `help:"Full sync" json:"full_sync" mcp:"true"`
DeepSync bool `help:"Deep sync" json:"deep_sync" mcp:"true"`
Xor bool `help:"Incremental xor sync mode" json:"xor" mcp:"true"`
Region []string `help:"Only sync specified regions" json:"region" mcp:"true"`
Zone []string `help:"Only sync specified zones" json:"zone" mcp:"true"`
Host []string `help:"Only sync specified hosts" json:"host" mcp:"true"`
Resources []string `help:"Resource types to sync" json:"resources" mcp:"true" choices:"project|compute|network|eip|loadbalancer|objectstore|rds|cache|event|cloudid|dnszone|public_ip|intervpcnetwork|saml_auth|quota|nat|nas|waf|mongodb|es|kafka|app|cdn|container|ipv6_gateway|tablestore|modelarts|vpcpeer|misc|image"`
}
func (opts *CloudaccountSyncOptions) Params() (jsonutils.JSONObject, error) {

View File

@@ -73,10 +73,12 @@ func (opts *DBInstanceCreateOptions) Params() (jsonutils.JSONObject, error) {
}
type DBInstanceListOptions struct {
_ struct{} `mcp-desc:"列出 RDSdbinstance。可用 search/provider/region 等过滤;详情用 climc_dbinstance_show"`
options.BaseListOptions
BillingType string `help:"billing type" choices:"postpaid|prepaid"`
IpAddr []string
SecgroupId string
BillingType string `help:"billing type" choices:"postpaid|prepaid" mcp:"true"`
IpAddr []string `mcp:"true"`
SecgroupId string `mcp:"true"`
}
func (opts *DBInstanceListOptions) Params() (jsonutils.JSONObject, error) {
@@ -95,6 +97,13 @@ func (opts *DBInstanceIdOptions) Params() (jsonutils.JSONObject, error) {
return nil, nil
}
// DBInstanceShowOptions 单独包装,避免 IdOptions 被 reboot/sync 等复用时误注册。
type DBInstanceShowOptions struct {
_ struct{} `mcp-desc:"查询 RDSdbinstance详情。ID 可用 climc_dbinstance_list 返回的 id/name"`
DBInstanceIdOptions
}
type DBInstanceRenewOptions struct {
DBInstanceIdOptions
DURATION string `help:"Duration of renew, ADMIN only command"`

View File

@@ -92,6 +92,28 @@ func (o *DiskIdOptions) Params() (jsonutils.JSONObject, error) {
return nil, nil
}
// DiskShowOptions 单独包装,避免 DiskIdOptions 被其它 Perform 复用时误注册。
type DiskShowOptions struct {
_ struct{} `mcp-desc:"查询单块硬盘详情。ID 可用 climc_disk_list 返回的 id/name"`
DiskIdOptions
}
type DiskResizeOptions struct {
_ struct{} `mcp-desc:"硬盘扩容。DISK 为硬盘 id/nameSIZE 为目标容量(如 100G扩容前可用 climc_disk_list / climc_disk_show 确认"`
DISK string `help:"ID or name of disk"`
SIZE string `help:"Size of disk"`
}
type DiskDeleteOptions struct {
_ struct{} `mcp-desc:"删除硬盘。若尚不知 id先用 climc_disk_list 定位;删除前确认未被虚机占用或已卸载"`
ID []string `help:"ID of disks to delete" metavar:"DISK"`
OverridePendingDelete bool `help:"Delete disk directly instead of pending delete" short-token:"f" mcp:"true"`
DeleteSnapshots bool `help:"Delete disk snapshots before delete disk" mcp:"true"`
}
type DiskMigrateOptions struct {
DiskIdOptions
@@ -177,24 +199,26 @@ func (o *DiskRebuildOptions) Params() (jsonutils.JSONObject, error) {
}
type DiskListOptions struct {
_ struct{} `mcp-desc:"列出硬盘。可用 search/server-id/unused 等过滤;查详情用 climc_disk_show扩容用 climc_disk_resize删除用 climc_disk_delete"`
options.BaseListOptions
Unused *bool `help:"Show unused disks"`
Share *bool `help:"Show Share storage disks"`
Local *bool `help:"Show Local storage disks"`
ServerId []string `help:"Guest ID or name"`
GuestStatus string `help:"Guest Status"`
Unused *bool `help:"Show unused disks" mcp:"true"`
Share *bool `help:"Show Share storage disks" mcp:"true"`
Local *bool `help:"Show Local storage disks" mcp:"true"`
ServerId []string `help:"Guest ID or name" mcp:"true"`
GuestStatus string `help:"Guest Status" mcp:"true"`
OrderByServer string `help:"Order By Server"`
Storage string `help:"Storage ID or name"`
Type string `help:"Disk type" choices:"sys|data|swap|volume"`
CloudType string `help:"Public cloud or private cloud" choices:"Public|Private"`
Storage string `help:"Storage ID or name" mcp:"true"`
Type string `help:"Disk type" choices:"sys|data|swap|volume" mcp:"true"`
CloudType string `help:"Public cloud or private cloud" choices:"Public|Private" mcp:"true"`
OrderByGuestCount string `help:"Order By Guest Count"`
BillingType string `help:"billing type" choices:"postpaid|prepaid"`
BillingType string `help:"billing type" choices:"postpaid|prepaid" mcp:"true"`
SnapshotpolicyId string `help:"snapshotpolicy id"`
StorageHostId string `help:"filter disk by host"`
StorageHostId string `help:"filter disk by host" mcp:"true"`
BindingServerSnapshotpolicy *bool `help:"filter disk by binding server snapshotpolicy" negative:"no-binding-server-snapshotpolicy"`
BindingSnapshotpolicy *bool `help:"filter disk by binding snapshotpolicy" negative:"no-binding-snapshotpolicy"`
}

View File

@@ -21,18 +21,20 @@ import (
)
type ElasticipListOptions struct {
Region string `help:"List eips in cloudregion"`
_ struct{} `mcp-desc:"列出弹性公网 IPEIP。可用 search/region/usable 过滤;详情 climc_eip_show绑虚机 climc_server_associate_eip"`
Usable *bool `help:"List all zones that is usable"`
UsableEipForAssociateType string `help:"With associate id filter which eip can associate" choices:"server|natgateway|loadbalancer"`
UsableEipForAssociateId string `help:"With associate type filter which eip can associate"`
Region string `help:"List eips in cloudregion" mcp:"true"`
Usable *bool `help:"List all zones that is usable" mcp:"true"`
UsableEipForAssociateType string `help:"With associate id filter which eip can associate" choices:"server|natgateway|loadbalancer" mcp:"true"`
UsableEipForAssociateId string `help:"With associate type filter which eip can associate" mcp:"true"`
OrderByIp string
AssociateId []string
AssociateType []string
AssociateId []string `mcp:"true"`
AssociateType []string `mcp:"true"`
AssociateName []string
IsAssociated *bool
IsAssociated *bool `mcp:"true"`
IpAddr []string
IpAddr []string `mcp:"true"`
options.BaseListOptions
}
@@ -40,6 +42,13 @@ func (opts *ElasticipListOptions) Params() (jsonutils.JSONObject, error) {
return options.ListStructToParams(opts)
}
// EipShowOptions 单独包装,避免 BaseShowOptions 被其它资源复用时误注册。
type EipShowOptions struct {
_ struct{} `mcp-desc:"查询 EIP 详情。ID 可用 climc_eip_list 返回的 id/name"`
options.BaseShowOptions
}
type EipCreateOptions struct {
options.BaseCreateOptions
Manager *string `help:"cloud provider"`

View File

@@ -24,9 +24,11 @@ import (
)
type ElasticCacheListOptions struct {
_ struct{} `mcp-desc:"列出 Redis/弹性缓存elastic-cache。可用 search 等过滤;详情用 climc_elastic_cache_show"`
options.BaseListOptions
SecgroupId string
SecgroupId string `mcp:"true"`
}
func (opts *ElasticCacheListOptions) Params() (jsonutils.JSONObject, error) {
@@ -45,6 +47,13 @@ func (opts *ElasticCacheIdOption) Params() (jsonutils.JSONObject, error) {
return nil, nil
}
// ElasticCacheShowOptions 单独包装,避免 IdOption 被 restart/delete 等复用时误注册。
type ElasticCacheShowOptions struct {
_ struct{} `mcp-desc:"查询 Redis/弹性缓存详情。ID 可用 climc_elastic_cache_list 返回的 id/name"`
ElasticCacheIdOption
}
type ElasticCacheCreateOptions struct {
NAME string
Manager string

View File

@@ -22,29 +22,29 @@ import (
)
type HostListOptions struct {
Schedtag string `help:"List hosts in schedtag"`
Zone string `help:"List hosts in zone"`
Region string `help:"List hosts in region"`
Wire string `help:"List hosts in wire"`
Image string `help:"List hosts cached images" json:"cachedimage"`
Storage string `help:"List hosts attached to storages"`
Baremetal string `help:"List hosts that is managed by baremetal system" choices:"true|false"`
Schedtag string `help:"List hosts in schedtag" mcp:"true"`
Zone string `help:"List hosts in zone" mcp:"true"`
Region string `help:"List hosts in region" mcp:"true"`
Wire string `help:"List hosts in wire" mcp:"true"`
Image string `help:"List hosts cached images" json:"cachedimage" mcp:"true"`
Storage string `help:"List hosts attached to storages" mcp:"true"`
Baremetal string `help:"List hosts that is managed by baremetal system" choices:"true|false" mcp:"true"`
Empty bool `help:"show empty host" json:"-"`
Occupied bool `help:"show occupid host" json:"-"`
Enabled bool `help:"Show enabled host only" json:"-"`
Disabled bool `help:"Show disabled host only" json:"-"`
HostType string `help:"Host type filter" choices:"baremetal|hypervisor|esxi|container|hyperv|aliyun|azure|qcloud|aws|huawei|ucloud|google|ctyun"`
HostType string `help:"Host type filter" choices:"baremetal|hypervisor|esxi|container|hyperv|aliyun|azure|qcloud|aws|huawei|ucloud|google|ctyun" mcp:"true"`
AnyMac string `help:"Mac matches one of the host's interface"`
AnyIp []string `help:"IP matches one of the host's interface"`
AnyIp []string `help:"IP matches one of the host's interface" mcp:"true"`
HostStorageType []string `help:"List host in host_storage_type"`
IsBaremetal *bool `help:"filter host list by is_baremetal=true|false"`
IsBaremetal *bool `help:"filter host list by is_baremetal=true|false" mcp:"true"`
ResourceType string `help:"Resource type" choices:"shared|prepaid|dedicated"`
ResourceType string `help:"Resource type" choices:"shared|prepaid|dedicated" mcp:"true"`
Usable *bool `help:"List all zones that is usable"`
Usable *bool `help:"List all zones that is usable" mcp:"true"`
Hypervisor string `help:"filter hosts by hypervisor"`
Hypervisor string `help:"filter hosts by hypervisor" mcp:"true"`
StorageNotAttached bool `help:"List hosts not attach specified storage"`
@@ -105,12 +105,21 @@ func (opts *HostListOptions) Params() (jsonutils.JSONObject, error) {
return params, nil
}
// HostListForMcpOptions 单独包装,避免 HostListOptions 被 host-node-count 等复用时误注册。
type HostListForMcpOptions struct {
_ struct{} `mcp-desc:"列出宿主机。可用 zone/region/host-type/search 过滤;详情用 climc_host_show"`
HostListOptions
}
type HostShowOptions struct {
_ struct{} `mcp-desc:"查询宿主机详情。ID 可用 climc_host_list 返回的 id/name"`
options.BaseShowOptions
ShowMetadata bool `help:"Show host metadata in details"`
ShowNicInfo bool `help:"Show host nic_info in details"`
ShowSysInfo bool `help:"Show host sys_info in details"`
ShowAll bool `help:"Show all of host details" short-token:"a"`
ShowMetadata bool `help:"Show host metadata in details" mcp:"true"`
ShowNicInfo bool `help:"Show host nic_info in details" mcp:"true"`
ShowSysInfo bool `help:"Show host sys_info in details" mcp:"true"`
ShowAll bool `help:"Show all of host details" short-token:"a" mcp:"true"`
}
func (o *HostShowOptions) Params() (jsonutils.JSONObject, error) {

View File

@@ -21,9 +21,18 @@ import (
)
type KafkaListOptions struct {
_ struct{} `mcp-desc:"列出 Kafka 实例。可用 search 等过滤;详情用 climc_kafka_show"`
options.BaseListOptions
}
// KafkaShowOptions 单独包装,避免 IdOption 被 delete/syncstatus 复用时误注册。
type KafkaShowOptions struct {
_ struct{} `mcp-desc:"查询 Kafka 实例详情。ID 可用 climc_kafka_list 返回的 id/name"`
KafkaIdOption
}
func (opts *KafkaListOptions) Params() (jsonutils.JSONObject, error) {
return options.ListStructToParams(opts)
}

View File

@@ -21,8 +21,10 @@ import (
)
type KeypairList struct {
_ struct{} `mcp-desc:"列出 SSH 密钥对keypair。可用 search/scheme 过滤"`
options.BaseListOptions
Scheme string `help:"Scheme of keypair, default is RSA" choices:"RSA|DSA|ECDSA|ED25519"`
Scheme string `help:"Scheme of keypair, default is RSA" choices:"RSA|DSA|ECDSA|ED25519" mcp:"true"`
}
func (self *KeypairList) Params() (jsonutils.JSONObject, error) {

View File

@@ -21,9 +21,18 @@ import (
)
type MongoDBListOptions struct {
_ struct{} `mcp-desc:"列出 MongoDB 实例。可用 search 等过滤;详情用 climc_mongodb_show"`
options.BaseListOptions
}
// MongoDBShowOptions 单独包装,避免复用 BaseIdOptions 误注册。
type MongoDBShowOptions struct {
_ struct{} `mcp-desc:"查询 MongoDB 实例详情。ID 可用 climc_mongodb_list 返回的 id/name"`
options.BaseIdOptions
}
func (opts *MongoDBListOptions) Params() (jsonutils.JSONObject, error) {
return options.ListStructToParams(opts)
}

View File

@@ -24,22 +24,24 @@ import (
)
type NetworkListOptions struct {
_ struct{} `mcp-desc:"【创建流程中的中间步骤】本工具不能完成创建。公有云须带 provider如 [\"Aliyun\"])及 region查完网络后凑齐同云镜像与规格立刻 climc_server_create。严禁只调用本工具后就停止"`
options.BaseListOptions
Ip string `help:"search networks that contain this IP"`
ZoneIds []string `help:"search networks in zones"`
Ip string `help:"search networks that contain this IP" mcp:"true"`
ZoneIds []string `help:"search networks in zones" mcp:"true"`
Wire string `help:"search networks belongs to a wire" json:"-"`
Host string `help:"search networks attached to a host"`
Vpc string `help:"search networks belongs to a VPC"`
Region string `help:"search networks belongs to a CloudRegion" json:"cloudregion"`
Host string `help:"search networks attached to a host" mcp:"true"`
Vpc string `help:"search networks belongs to a VPC" mcp:"true"`
Region string `help:"search networks belongs to a CloudRegion" json:"cloudregion" mcp:"true"`
City string `help:"search networks belongs to a city"`
Usable *bool `help:"search usable networks"`
ServerType string `help:"search networks belongs to a ServerType" choices:"baremetal|container|eip|guest|ipmi|pxe|hostlocal"`
Usable *bool `help:"search usable networks" mcp:"true"`
ServerType string `help:"search networks belongs to a ServerType" choices:"baremetal|container|eip|guest|ipmi|pxe|hostlocal" mcp:"true"`
Schedtag string `help:"filter networks by schedtag"`
HostSchedtagId string `help:"filter by host schedtag"`
IsAutoAlloc *bool `help:"search network with is_auto_alloc"`
IsAutoAlloc *bool `help:"search network with is_auto_alloc" mcp:"true"`
IsClassic *bool `help:"search classic on-premise network"`
// Status string `help:"filter by network status"`

View File

@@ -28,18 +28,20 @@ import (
)
type SecgroupListOptions struct {
_ struct{} `mcp-desc:"列出安全组。可用 search/server/vpc-id 等过滤;详情 climc_secgroup_show规则 climc_secgroup_rule_list"`
baseoptions.BaseListOptions
Equals string `help:"Secgroup ID or Name, filter secgroups whose rules equals the specified one"`
Server string `help:"Filter secgroups bound to specified server"`
Ip string `help:"Filter secgroup by ip"`
Ports string `help:"Filter secgroup by ports"`
Direction string `help:"Filter secgroup by ports" choices:"all|in|out"`
DBInstance string `help:"Filter secgroups bound to specified rds" json:"dbinstance"`
Cloudregion string `help:"Filter secgroups by region"`
VpcId string
Cloudaccount string `help:"Filter secgroups by account"`
LoadbalancerId string
Equals string `help:"Secgroup ID or Name, filter secgroups whose rules equals the specified one" mcp:"true"`
Server string `help:"Filter secgroups bound to specified server" mcp:"true"`
Ip string `help:"Filter secgroup by ip" mcp:"true"`
Ports string `help:"Filter secgroup by ports" mcp:"true"`
Direction string `help:"Filter secgroup by ports" choices:"all|in|out" mcp:"true"`
DBInstance string `help:"Filter secgroups bound to specified rds" json:"dbinstance" mcp:"true"`
Cloudregion string `help:"Filter secgroups by region" mcp:"true"`
VpcId string `mcp:"true"`
Cloudaccount string `help:"Filter secgroups by account" mcp:"true"`
LoadbalancerId string `mcp:"true"`
}
func (opts *SecgroupListOptions) Params() (jsonutils.JSONObject, error) {
@@ -47,10 +49,12 @@ func (opts *SecgroupListOptions) Params() (jsonutils.JSONObject, error) {
}
type SecgroupCreateOptions struct {
_ struct{} `mcp-desc:"创建安全组。NAME 必填;可传 rules安全规则字符串数组与 vpc-id。创建后可用 climc_secgroup_rule_create 继续加规则"`
baseoptions.BaseCreateOptions
VpcId string
Tags []string
Rules []string `help:"security rule to create"`
VpcId string `mcp:"true"`
Tags []string `mcp:"true"`
Rules []string `help:"security rule to create" mcp:"true"`
}
func (opts *SecgroupCreateOptions) Params() (jsonutils.JSONObject, error) {
@@ -95,6 +99,19 @@ func (opts *SecgroupIdOptions) Params() (jsonutils.JSONObject, error) {
return nil, nil
}
// SecgroupShowOptions / SecgroupDeleteOptions 单独包装,避免与其它 Perform 复用 IdOptions 时注册。
type SecgroupShowOptions struct {
_ struct{} `mcp-desc:"查询安全组详情。ID 可用 climc_secgroup_list 返回的 id/name"`
SecgroupIdOptions
}
type SecgroupDeleteOptions struct {
_ struct{} `mcp-desc:"删除安全组。若尚不知 id先用 climc_secgroup_list 定位;确认无虚机绑定后再删"`
SecgroupIdOptions
}
type SecgroupMergeOptions struct {
SecgroupIdOptions
SECGROUPS []string `help:"source IDs or Names of secgroup"`

View File

@@ -23,15 +23,17 @@ import (
)
type SecGroupRulesListOptions struct {
_ struct{} `mcp-desc:"列出安全组规则。建议传 secgroup安全组 id/name详情 climc_secgroup_rule_show创建 climc_secgroup_rule_create删除 climc_secgroup_rule_delete"`
options.BaseListOptions
Secgroup string `help:"Secgroup ID or Name"`
SecgroupName string `help:"Search rules by fuzzy secgroup name"`
Projects []string `help:"Filter rules by project"`
Direction string `help:"filter Direction of rule" choices:"in|out"`
Protocol string `help:"filter Protocol of rule" choices:"any|tcp|udp|icmp"`
Action string `help:"filter Actin of rule" choices:"allow|deny"`
Ports string `help:"filter Ports of rule"`
Ip string `help:"filter cidr of rule"`
Secgroup string `help:"Secgroup ID or Name" mcp:"true"`
SecgroupName string `help:"Search rules by fuzzy secgroup name" mcp:"true"`
Projects []string `help:"Filter rules by project" mcp:"true"`
Direction string `help:"filter Direction of rule" choices:"in|out" mcp:"true"`
Protocol string `help:"filter Protocol of rule" choices:"any|tcp|udp|icmp" mcp:"true"`
Action string `help:"filter Actin of rule" choices:"allow|deny" mcp:"true"`
Ports string `help:"filter Ports of rule" mcp:"true"`
Ip string `help:"filter cidr of rule" mcp:"true"`
}
func (opts *SecGroupRulesListOptions) Params() (jsonutils.JSONObject, error) {
@@ -39,10 +41,25 @@ func (opts *SecGroupRulesListOptions) Params() (jsonutils.JSONObject, error) {
}
type SecGroupRulesCreateOptions struct {
_ struct{} `mcp-desc:"创建安全组规则。SECGROUP 为安全组 id/nameRULE 为规则字符串(如 in:allow tcp 22可用 climc_secgroup_list 定位安全组"`
SECGROUP string `help:"Secgroup ID or Name" metavar:"Secgroup"`
RULE string `json:"-"`
Priority int64 `help:"priority of Rule" default:"50"`
Desc string `help:"Description" json:"description"`
Priority int64 `help:"priority of Rule" default:"50" mcp:"true"`
Desc string `help:"Description" json:"description" mcp:"true"`
}
// SecGroupRuleShowOptions / SecGroupRuleDeleteOptions 单独包装,避免复用 BaseShow/BaseId 误注册。
type SecGroupRuleShowOptions struct {
_ struct{} `mcp-desc:"查询单条安全组规则详情。ID 可用 climc_secgroup_rule_list 返回的 id"`
options.BaseShowOptions
}
type SecGroupRuleDeleteOptions struct {
_ struct{} `mcp-desc:"删除安全组规则。若尚不知 id先用 climc_secgroup_rule_list建议带 secgroup定位"`
options.BaseIdOptions
}
func (opts *SecGroupRulesCreateOptions) Params() (jsonutils.JSONObject, error) {

View File

@@ -37,26 +37,28 @@ import (
var ErrEmtptyUpdate = errors.Error("No valid update data")
type ServerListOptions struct {
Zone string `help:"Zone ID or Name"`
_ struct{} `mcp-desc:"当用户要求启动/停止/重启/删除/重置密码时:先用本工具定位目标,拿到返回结果中的 id 后,必须立刻继续调用对应操作工具完成操作,不要只查询就结束"`
Zone string `help:"Zone ID or Name" mcp:"true"`
Wire string `help:"Wire ID or Name"`
Network string `help:"Network ID or Name"`
Network string `help:"Network ID or Name" mcp:"true"`
Disk string `help:"Disk ID or Name"`
Host string `help:"Host ID or Name"`
Host string `help:"Host ID or Name" mcp:"true"`
Baremetal *bool `help:"Show baremetal servers"`
Gpu *bool `help:"Show gpu servers"`
Secgroup string `help:"Secgroup ID or Name"`
AdminSecgroup string `help:"AdminSecgroup ID or Name"`
Hypervisor string `help:"Show server of hypervisor" choices:"kvm|esxi|pod|baremetal|aliyun|azure|aws|huawei|ucloud|volcengine|zstack|openstack|google|ctyun|incloudsphere|nutanix|bingocloud|cloudpods|ecloud|jdcloud|remotefile|h3c|hcs|hcso|hcsop|proxmox|ksyun|baidu|cucloud|qingcloud|sangfor|zettakit|uis|cnware"`
Region string `help:"Show servers in cloudregion"`
Hypervisor string `help:"Show server of hypervisor" choices:"kvm|esxi|pod|baremetal|aliyun|azure|aws|huawei|ucloud|volcengine|zstack|openstack|google|ctyun|incloudsphere|nutanix|bingocloud|cloudpods|ecloud|jdcloud|remotefile|h3c|hcs|hcso|hcsop|proxmox|ksyun|baidu|cucloud|qingcloud|sangfor|zettakit|uis|cnware" mcp:"true"`
Region string `help:"Show servers in cloudregion" mcp:"true"`
WithEip *bool `help:"Show Servers with EIP"`
WithoutEip *bool `help:"Show Servers without EIP"`
OsType string `help:"OS Type" choices:"linux|windows|vmware"`
Vpc []string `help:"Vpc id or name"`
OsType string `help:"OS Type" choices:"linux|windows|vmware" mcp:"true"`
Vpc []string `help:"Vpc id or name" mcp:"true"`
UsableServerForEip string `help:"Eip id or name"`
WithoutUserMeta *bool `help:"Show Servers without user metadata"`
EipAssociable *bool `help:"Show Servers can associate with eip"`
HostSn string `help:"Host SN"`
IpAddr string `help:"Fileter by ip"`
IpAddr string `help:"Fileter by ip" mcp:"true"`
IpAddrs []string `help:"Fileter by ips"`
OrderByDisk string `help:"Order by disk size" choices:"asc|desc"`
@@ -128,6 +130,8 @@ func (o *ServerConvertToKvmOptions) Params() (jsonutils.JSONObject, error) {
}
type ServerStartOptions struct {
_ struct{} `mcp-desc:"用户要求启动时必须调用本工具真正执行,仅调用 climc_server_list 查询不算完成。若尚不知 id先用 climc_server_list可用 search定位拿到 id 后立即调用本工具,不要只查询就结束"`
ServerIdsOptions
QemuVersion string `help:"prefer qemu version" json:"qemu_version"`
@@ -138,6 +142,16 @@ func (o *ServerStartOptions) Params() (jsonutils.JSONObject, error) {
return jsonutils.Marshal(o), nil
}
// ServerStartRescueOptions 与启动参数相同,单独包装以免继承 server-start 的 mcp-desc。
type ServerStartRescueOptions struct {
ServerStartOptions
}
// ServerStopRescueOptions 与启动参数相同,单独包装以免继承 server-start 的 mcp-desc。
type ServerStopRescueOptions struct {
ServerStartOptions
}
type ServerIdsOptions struct {
ID []string `help:"ID of servers to operate" metavar:"SERVER" json:"-"`
}
@@ -194,6 +208,8 @@ func (o *ServerCreateBackupOptions) Params() (jsonutils.JSONObject, error) {
}
type ServerShowOptions struct {
_ struct{} `mcp-desc:"查询单台虚拟机详情。ID 可用 climc_server_list 返回的 id/name查看状态、配置、IP 等信息时使用"`
options.BaseShowOptions `id->help:"ID or name of the server"`
}
@@ -249,22 +265,22 @@ func ParseServerDeployInfoList(list []string) ([]*computeapi.DeployConfig, error
}
type ServerCreateCommonConfig struct {
Manager string `help:"Preferred cloudprovider where virtual server should bd created" json:"prefer_manager"`
Region string `help:"Preferred region where virtual server should be created" json:"prefer_region"`
Zone string `help:"Preferred zone where virtual server should be created" json:"prefer_zone"`
Manager string `help:"Preferred cloudprovider where virtual server should bd created" json:"prefer_manager" mcp:"true"`
Region string `help:"Preferred region where virtual server should be created" json:"prefer_region" mcp:"true"`
Zone string `help:"Preferred zone where virtual server should be created" json:"prefer_zone" mcp:"true"`
Zones []string `help:"Preferred zones where virtual server should be created" json:"prefer_zones"`
Wire string `help:"Preferred wire where virtual server should be created" json:"prefer_wire"`
Host string `help:"Preferred host where virtual server should be created" json:"prefer_host"`
Host string `help:"Preferred host where virtual server should be created" json:"prefer_host" mcp:"true"`
ResourceType string `help:"Resource type" choices:"shared|prepaid|dedicated"`
Schedtag []string `help:"Schedule policy, key = aggregate name, value = require|exclude|prefer|avoid" metavar:"<KEY:VALUE>"`
Net []string `help:"Network descriptions" metavar:"NETWORK"`
Net []string `help:"Network descriptions;可省略,省略时 MCP 自动用 random等价 nets:[{exit:false}])走自动调度" metavar:"NETWORK" mcp:"true"`
NetPortMapping []string `help:"Network port mapping, e.g. 'index=0,port=80,host_port=8080,protocol=<tcp|udp>,host_port_range=<int>-<int>,remote_ips=x.x.x.x|y.y.y.y'" short-token:"p"`
NetSchedtag []string `help:"Network schedtag description, e.g. '0:<tag>:<strategy>'"`
IsolatedDevice []string `help:"Isolated device model or ID" metavar:"ISOLATED_DEVICE"`
Project string `help:"'Owner project ID or Name" json:"tenant"`
Project string `help:"'Owner project ID or Name" json:"tenant" mcp:"true"`
User string `help:"Owner user ID or Name"`
Count int `help:"Create multiple simultaneously" default:"1"`
Count int `help:"Create multiple simultaneously" default:"1" mcp:"true"`
Disk []string `help:"
Disk descriptions
size: 500M, 10G
@@ -277,16 +293,17 @@ type ServerCreateCommonConfig struct {
medium: rotate, ssd, hybrid
disk_type: sys, data
mountpoint: /, /opt
storage_type: local, rbd, nas, nfs
storage_type/backend: local, rbd, nas, nfs;公有云用 cloud-region-capability 的 storage_types2如 cloud_essd
snapshot_id: use snapshot-list get snapshot id
disk_id: use disk-list get disk id
storage_id: use storage-list get storage id
image_id: use image-list get image id
image_id: use image-list/cached-image-list get image id
for example:
--disk 'image_id=c2be02a4-7ff2-43e6-8a00-a489e04d2d6f,size=10G,driver=ide,storage_type=rbd,auto_delete=true'
--disk 'size=40g,image=<id>,backend=cloud_essd'
--disk 'size=500M'
--disk 'snpahost_id=1ceb8c6d-6571-451d-8957-4bd3a871af85'
" nargs:"+"`
" nargs:"+" mcp:"true"`
DiskSchedtag []string `help:"Disk schedtag description, e.g. '0:<tag>:<strategy>'"`
}
@@ -371,7 +388,7 @@ func (o ServerCreateCommonConfig) Data() (*computeapi.ServerConfigs, error) {
type ServerConfigs struct {
ServerCreateCommonConfig
Hypervisor string `help:"Hypervisor type" choices:"kvm|pod|esxi|baremetal|container|aliyun|azure|qcloud|aws|huawei|openstack|ucloud|volcengine|zstack|google|ctyun|incloudsphere|bingocloud|cloudpods|ecloud|jdcloud|remotefile|h3c|hcs|hcso|hcsop|proxmox|sangfor|zettakit|uis"`
Hypervisor string `help:"Hypervisor type" choices:"kvm|pod|esxi|baremetal|container|aliyun|azure|qcloud|aws|huawei|openstack|ucloud|volcengine|zstack|google|ctyun|incloudsphere|bingocloud|cloudpods|ecloud|jdcloud|remotefile|h3c|hcs|hcso|hcsop|proxmox|sangfor|zettakit|uis" mcp:"true"`
Backup bool `help:"Create server with backup server"`
BackupHost string `help:"Perfered host where virtual backup server should be created"`
AutoSwitchToBackupOnHostDown bool `help:"Auto switch to backup server on host down"`
@@ -448,30 +465,32 @@ type ServerCreateFromInstanceSnapshot struct {
}
type ServerCreateOptions struct {
_ struct{} `mcp-desc:"【创建虚拟机的最终动作】自动 scheduler-forecast→创建→等待 running/ready。最少name、diskimage+backend、规格。超时返回 wait_pending+server_id用 climc_server_show 续查,勿重复创建"`
ServerCreateOptionalOptions
NAME string `help:"Name of server" json:"-"`
NAME string `help:"虚拟机名称模板;默认配合 generate-name 自动去重" json:"-" mcp:"required"`
}
type ServerCreateOptionalOptions struct {
ServerConfigs
MemSpec string `help:"Memory size Or Instance Type" metavar:"MEMSPEC" json:"-"`
MemSpec string `help:"Memory size Or Instance Type" metavar:"MEMSPEC" json:"-" mcp:"true"`
CpuSockets int `help:"Cpu sockets"`
EnableMemclean bool `help:"clean guest memory after guest exit" json:"enable_memclean"`
EnableTpm bool `help:"enable tpm device" json:"enable_tpm"`
Keypair string `help:"SSH Keypair"`
Password string `help:"Default user password"`
LoginAccount string `help:"Guest login account"`
Iso string `help:"ISO image ID" metavar:"IMAGE_ID" json:"cdrom"`
Keypair string `help:"SSH Keypair" mcp:"true"`
Password string `help:"Default user password" mcp:"true"`
LoginAccount string `help:"Guest login account" mcp:"true"`
Iso string `help:"ISO image ID" metavar:"IMAGE_ID" json:"cdrom" mcp:"true"`
IsoBootIndex *int8 `help:"Iso bootindex" metavar:"IMAGE_BOOT_INDEX" json:"cdrom_boot_index"`
VcpuCount int `help:"#CPU cores of VM server, default 1" default:"1" metavar:"<SERVER_CPU_COUNT>" json:"vcpu_count" token:"ncpu"`
VcpuCount int `help:"#CPU cores of VM server, default 1" default:"1" metavar:"<SERVER_CPU_COUNT>" json:"vcpu_count" token:"ncpu" mcp:"true"`
ExtraCpuCount int `help:"Extra allocate cpu count" json:"extra_cpu_count"`
InstanceType string `help:"instance flavor"`
InstanceType string `help:"instance flavor" mcp:"true"`
Vga string `help:"VGA driver" choices:"std|vmware|cirrus|qxl|virtio"`
Vdi string `help:"VDI protocool" choices:"vnc|spice"`
Bios string `help:"BIOS" choices:"BIOS|UEFI"`
Bios string `help:"BIOS" choices:"BIOS|UEFI" mcp:"true"`
Machine string `help:"Machine type" choices:"pc|q35"`
Desc string `help:"Description" metavar:"<DESCRIPTION>" json:"description"`
Boot string `help:"Boot device" metavar:"<BOOT_DEVICE>" choices:"disk|cdrom" json:"-"`
@@ -479,20 +498,20 @@ type ServerCreateOptionalOptions struct {
NoAccountInit *bool `help:"Not reset account password"`
AllowDelete *bool `help:"Unlock server to allow deleting" json:"-"`
ShutdownBehavior string `help:"Behavior after VM server shutdown" metavar:"<SHUTDOWN_BEHAVIOR>" choices:"stop|terminate|stop_release_gpu"`
AutoStart bool `help:"Auto start server after it is created"`
AutoStart bool `help:"Auto start server after it is created" mcp:"true"`
Deploy []string `help:"Specify deploy files in virtual server file system" json:"-"`
DeployTelegraf bool `help:"Deploy telegraf agent if guest os is supported"`
Group []string `help:"Group ID or Name of virtual server"`
System bool `help:"Create a system VM, sysadmin ONLY option" json:"is_system"`
TaskNotify *bool `help:"Setup task notify" json:"-"`
FakeCreate *bool `help:"Fake create server"`
DryRun *bool `help:"Dry run to test scheduler" json:"-"`
DryRun *bool `help:"Dry run to validate create params (not preschedule)MCP 创建会自动调 scheduler-forecast 预调度,一般无需手动传" json:"-" mcp:"true"`
UserDataFile string `help:"user_data file path" json:"-"`
InstanceSnapshot string `help:"instance snapshot" json:"instance_snapshot"`
Secgroups []string `help:"secgroups" json:"secgroups"`
NetworkTags []string `help:"GCP network tags, google only; when set, secgroups can be omitted" json:"network_tags"`
OsType string `help:"os type, e.g. Linux, Windows, etc."`
OsType string `help:"os type, e.g. Linux, Windows, etc." mcp:"true"`
Duration string `help:"valid duration of the server, e.g. 1H, 1D, 1W, 1M, 1Y, ADMIN ONLY option"`
AutoRenew bool `help:"auto renew for prepaid server"`
@@ -507,7 +526,7 @@ type ServerCreateOptionalOptions struct {
KickstartMaxRetries int `help:"Kickstart max retries" default:"3" json:"-"`
KickstartTimeoutMinutes int `help:"Kickstart timeout in minutes" default:"60" json:"-"`
GenerateName bool `help:"name is generated by pattern" json:"-"`
GenerateName bool `help:"name is generated by pattern" json:"-" mcp:"true"`
EipBw int `help:"allocate EIP with bandwidth in MB when server is created" json:"eip_bw,omitzero"`
EipTxBw int `help:"allocate EIP with上行带宽 in MB when server is created" json:"eip_tx_bw,omitzero"`
@@ -519,7 +538,7 @@ type ServerCreateOptionalOptions struct {
PublicIpBw int `help:"associate public ip with bandwidth in MB where server is created" json:"public_ip_bw,omitzero"`
PublicIpChargeType string `help:"newly allocated public ip charge type" choices:"traffic|bandwidth" json:"public_ip_charge_type,omitempty"`
GuestImageID string `help:"create from guest image, need to specify the guest image id"`
GuestImageID string `help:"create from guest image, need to specify the guest image id" mcp:"true"`
EncryptKey string `help:"encryption key"`
Tags []string `help:"tags in the form of key=value"`
@@ -755,9 +774,11 @@ func (opts *ServerCreateOptions) Params() (*computeapi.ServerCreateInput, error)
}
type ServerStopOptions struct {
_ struct{} `mcp-desc:"用户要求停止时必须调用本工具真正执行,仅调用 climc_server_list 查询不算完成。若尚不知 id先用 climc_server_list可用 search定位拿到 id 后立即调用本工具,不要只查询就结束"`
ID []string `help:"ID or Name of server" json:"-"`
Force *bool `help:"Stop server forcefully" json:"is_force"`
StopCharging *bool `help:"Stop charging when server stop"`
Force *bool `help:"Stop server forcefully" json:"is_force" mcp:"true"`
StopCharging *bool `help:"Stop charging when server stop" mcp:"true"`
}
func (o *ServerStopOptions) GetIds() []string {
@@ -816,12 +837,14 @@ func (opts *ServerUpdateOptions) Params() (jsonutils.JSONObject, error) {
}
type ServerDeleteOptions struct {
_ struct{} `mcp-desc:"用户要求删除时必须调用本工具真正执行,仅调用 climc_server_list 查询不算完成。若尚不知 id先用 climc_server_list可用 search定位拿到 id 后立即调用本工具,不要只查询就结束"`
ServerIdsOptions
OverridePendingDelete *bool `help:"Delete server directly instead of pending delete" short-token:"f"`
DeleteSnapshots *bool `help:"Delete server snapshots"`
DeleteDisks *bool `help:"Delete server disks"`
DeleteEip *bool `help:"Delete eip"`
DeleteBastionServer *bool `help:"Remove from bastion host"`
OverridePendingDelete *bool `help:"Delete server directly instead of pending delete" short-token:"f" mcp:"true"`
DeleteSnapshots *bool `help:"Delete server snapshots" mcp:"true"`
DeleteDisks *bool `help:"Delete server disks" mcp:"true"`
DeleteEip *bool `help:"Delete eip" mcp:"true"`
DeleteBastionServer *bool `help:"Remove from bastion host" mcp:"true"`
}
func (o *ServerDeleteOptions) QueryParams() (jsonutils.JSONObject, error) {
@@ -994,9 +1017,11 @@ func (o *ServerKickstartCompleteOptions) Params() (jsonutils.JSONObject, error)
}
type ServerMonitorOptions struct {
_ struct{} `mcp-desc:"【QEMU Monitor不是监控指标】向虚机发送 HMP/QMP。查 CPU/内存等指标请用 climc_monitor_unifiedmonitor_query"`
ServerIdOptions
Qmp bool `help:"Use qmp protocol, default is hmp"`
Qmp bool `help:"Use qmp protocol, default is hmp" mcp:"true"`
COMMAND string `help:"Qemu Monitor command to send"`
}
@@ -1053,12 +1078,14 @@ func (o *ServerQgaGetNetwork) Params() (jsonutils.JSONObject, error) {
}
type ServerSetPasswordOptions struct {
_ struct{} `mcp-desc:"用户要求重置密码时必须调用本工具真正执行,仅调用 climc_server_list 查询不算完成。若尚不知 id先用 climc_server_list可用 search定位拿到 id 后立即调用本工具,不要只查询就结束"`
ServerIdOptions
Username string `help:"Which user to set password" json:"username"`
Password string `help:"Password content" json:"password"`
ResetPassword bool `help:"Force reset password"`
AutoStart bool `help:"Auto start server after reset password"`
Username string `help:"Which user to set password" json:"username" mcp:"true"`
Password string `help:"Password content" json:"password" mcp:"required"`
ResetPassword bool `help:"Force reset password" mcp:"true"`
AutoStart bool `help:"Auto start server after reset password" mcp:"true"`
}
func (o *ServerSetPasswordOptions) Params() (jsonutils.JSONObject, error) {
@@ -1146,14 +1173,16 @@ func (o *ServerChangeOwnerOptions) Params() (jsonutils.JSONObject, error) {
}
type ServerRebuildRootOptions struct {
_ struct{} `mcp-desc:"重装系统盘。ID 为虚机 id可选 image、password、auto-start。先 climc_server_list / climc_image_list 定位"`
ID string `help:"Server to rebuild root" json:"-"`
ImageId string `help:"New root Image template ID" json:"image_id" token:"image"`
Keypair string `help:"ssh Keypair used for login"`
ImageId string `help:"New root Image template ID" json:"image_id" token:"image" mcp:"true"`
Keypair string `help:"ssh Keypair used for login" mcp:"true"`
Password string `help:"Default user password"`
LoginAccount string `help:"Guest login account"`
NoAccountInit *bool `help:"Not reset account password"`
AutoStart *bool `help:"Auto start server after it is created"`
AllDisks *bool `help:"Rebuild all disks including data disks"`
LoginAccount string `help:"Guest login account" mcp:"true"`
NoAccountInit *bool `help:"Not reset account password" mcp:"true"`
AutoStart *bool `help:"Auto start server after it is created" mcp:"true"`
AllDisks *bool `help:"Rebuild all disks including data disks" mcp:"true"`
UserData string `hlep:"user data scripts"`
}
@@ -1180,16 +1209,18 @@ func (o *ServerRebuildRootOptions) Description() string {
}
type ServerChangeConfigOptions struct {
_ struct{} `mcp-desc:"调整虚机配置CPU/内存/套餐。ID 用 climc_server_list传 ncpu/vmem 或 instance-type。改配前确认状态允许"`
ServerIdOptions
VcpuCount *int `help:"New number of Virtual CPU cores" json:"vcpu_count" token:"ncpu"`
ExtraCpuCount *int `help:"Extra allocate cpu count" json:"extra_cpu_count"`
CpuSockets *int `help:"Cpu sockets"`
VmemSize string `help:"New memory size" json:"vmem_size" token:"vmem"`
Disk []string `help:"Data disk description, from the 1st data disk to the last one, empty string if no change for this data disk"`
VcpuCount *int `help:"New number of Virtual CPU cores" json:"vcpu_count" token:"ncpu" mcp:"true"`
ExtraCpuCount *int `help:"Extra allocate cpu count" json:"extra_cpu_count" mcp:"true"`
CpuSockets *int `help:"Cpu sockets" mcp:"true"`
VmemSize string `help:"New memory size" json:"vmem_size" token:"vmem" mcp:"true"`
Disk []string `help:"Data disk description, from the 1st data disk to the last one, empty string if no change for this data disk" mcp:"true"`
InstanceType string `help:"Instance Type, e.g. S2.SMALL2 for qcloud"`
InstanceType string `help:"Instance Type, e.g. S2.SMALL2 for qcloud" mcp:"true"`
ForceStop *bool `help:"Force stop the server before changing config" json:"force_stop"`
ForceStop *bool `help:"Force stop the server before changing config" json:"force_stop" mcp:"true"`
ResetTrafficLimits []string `help:"reset traffic limits, mac,rx,tx"`
SetTrafficLimits []string `help:"set traffic limits, mac,rx,tx"`
@@ -1296,8 +1327,10 @@ func (o *ServerResetOptions) Params() (jsonutils.JSONObject, error) {
}
type ServerRestartOptions struct {
_ struct{} `mcp-desc:"用户要求重启时必须调用本工具真正执行,仅调用 climc_server_list 查询不算完成。若尚不知 id先用 climc_server_list可用 search定位拿到 id 后立即调用本工具,不要只查询就结束"`
ID []string `help:"ID of servers to operate" metavar:"SERVER" json:"-"`
IsForce *bool `help:"Force reset or not; default false" json:"is_force"`
IsForce *bool `help:"Force reset or not; default false" json:"is_force" mcp:"true"`
}
func (o *ServerRestartOptions) GetIds() []string {
@@ -1403,8 +1436,10 @@ func (opts *ServerBatchMetadataOptions) Params() (jsonutils.JSONObject, error) {
}
type ServerAssociateEipOptions struct {
_ struct{} `mcp-desc:"将 EIP 绑定到虚机。需虚机 ID 与 EIPclimc_eip_list 的 id/name"`
ServerIdOptions
EIP string `help:"ID or name of EIP to associate"`
EIP string `help:"ID or name of EIP to associate" mcp:"required"`
}
func (o *ServerAssociateEipOptions) Params() (jsonutils.JSONObject, error) {

View File

@@ -15,24 +15,33 @@
package compute
import (
"fmt"
"regexp"
"strconv"
"strings"
"yunion.io/x/jsonutils"
baseoptions "yunion.io/x/onecloud/pkg/mcclient/options"
)
type ServerSkusListOptions struct {
_ struct{} `mcp-desc:"【创建流程中间步骤】查规格。口语 2c2g 用 spec公有云须 provider+cloudregion。取 name 作 instance-type 后立刻 climc_server_create"`
baseoptions.BaseListOptions
Cloudregion string `help:"region Id or name"`
Usable bool `help:"Filter usable sku"`
Zone string `help:"zone Id or name"`
Cloudregion string `help:"region Id or name" mcp:"true"`
Usable bool `help:"Filter usable sku" mcp:"true"`
Zone string `help:"zone Id or name" mcp:"true"`
City *string `help:"city name,eg. BeiJing"`
Cpu *int `help:"Cpu core count" json:"cpu_core_count"`
Mem *int `help:"Memory size in MB" json:"memory_size_mb"`
Name string `help:"Name of Sku"`
PostpaidStatus string `help:"Postpaid status" choices:"soldout|available"`
// Spec 口语规格,如 2c2g / 2核2G / 4C8G会解析为 cpu_core_count + memory_size_mb
Spec string `help:"Human spec like 2c2g / 2核2G / 4C8G; expands to cpu+mem(MB)" json:"-" mcp:"true"`
Cpu *int `help:"Cpu core count用户说2核时传2。也可改用 --spec 2c2g" json:"cpu_core_count" mcp:"true"`
Mem *int `help:"Memory size in MB2G=2048。也可改用 --spec 2c2g" json:"memory_size_mb" mcp:"true"`
Name string `help:"Name of Sku" mcp:"true"`
PostpaidStatus string `help:"Postpaid status创建优先 available" choices:"soldout|available" mcp:"true"`
PrepaidStatus string `help:"Prepaid status" choices:"soldout|available"`
CpuArch string `help:"Cpu Arch" choices:"x86|arm"`
Enabled *bool `help:"Filter enabled skus"`
CpuArch string `help:"Cpu Arch" choices:"x86|arm" mcp:"true"`
Enabled *bool `help:"Filter enabled skus" mcp:"true"`
Distinct bool `help:"distinct sku by name"`
OrderByTotalGuestCount string
}
@@ -42,9 +51,70 @@ func (opts *ServerSkusListOptions) GetId() string {
}
func (opts *ServerSkusListOptions) Params() (jsonutils.JSONObject, error) {
if err := opts.applySpec(); err != nil {
return nil, err
}
return baseoptions.ListStructToParams(opts)
}
func (opts *ServerSkusListOptions) applySpec() error {
spec := strings.TrimSpace(opts.Spec)
if spec == "" {
return nil
}
cpu, memMB, err := ParseSkuSpec(spec)
if err != nil {
return err
}
if opts.Cpu == nil {
opts.Cpu = &cpu
}
if opts.Mem == nil {
opts.Mem = &memMB
}
return nil
}
// skuSpecPatterns 支持2c2g、2C2G、4c8g、2核2G、2核2g、2vcpu2gb、2c/2g、2x2g
var skuSpecPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)^\s*(\d+)\s*[cC]\s*[xX/]?\s*(\d+)\s*([gGmM])b?\s*$`),
regexp.MustCompile(`(?i)^\s*(\d+)\s*[xX/]\s*(\d+)\s*([gGmM])b?\s*$`),
regexp.MustCompile(`(?i)^\s*(\d+)\s*核\s*(\d+)\s*([gGmM])b?\s*$`),
regexp.MustCompile(`(?i)^\s*(\d+)\s*v?cpu\s*[xX/]?\s*(\d+)\s*([gGmM])b?\s*$`),
}
// ParseSkuSpec 将口语规格解析为 CPU 核数与内存 MB。
func ParseSkuSpec(spec string) (cpu int, memMB int, err error) {
s := strings.TrimSpace(spec)
if s == "" {
return 0, 0, fmt.Errorf("empty sku spec")
}
for _, re := range skuSpecPatterns {
m := re.FindStringSubmatch(s)
if m == nil {
continue
}
cpu, err = strconv.Atoi(m[1])
if err != nil || cpu <= 0 {
return 0, 0, fmt.Errorf("invalid cpu in spec %q", spec)
}
mem, err := strconv.Atoi(m[2])
if err != nil || mem <= 0 {
return 0, 0, fmt.Errorf("invalid memory in spec %q", spec)
}
switch strings.ToLower(m[3]) {
case "g":
memMB = mem * 1024
case "m":
memMB = mem
default:
return 0, 0, fmt.Errorf("unsupported memory unit in spec %q", spec)
}
return cpu, memMB, nil
}
return 0, 0, fmt.Errorf("unrecognized sku spec %q, expect like 2c2g or 2核2G", spec)
}
type ServerSkusIdOptions struct {
ID string `help:"ID or Name of SKU to show"`
}

View File

@@ -0,0 +1,69 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package compute
import "testing"
func TestParseSkuSpec(t *testing.T) {
cases := []struct {
in string
cpu int
memMB int
wantErr bool
}{
{"2c2g", 2, 2048, false},
{"2C2G", 2, 2048, false},
{"4c8g", 4, 8192, false},
{"2核2G", 2, 2048, false},
{"2核2g", 2, 2048, false},
{" 4核 16G ", 4, 16384, false},
{"2c/2g", 2, 2048, false},
{"2x2g", 2, 2048, false},
{"2vcpu2gb", 2, 2048, false},
{"2c2048m", 2, 2048, false},
{"2g", 0, 0, true},
{"abc", 0, 0, true},
{"", 0, 0, true},
}
for _, c := range cases {
cpu, mem, err := ParseSkuSpec(c.in)
if c.wantErr {
if err == nil {
t.Errorf("ParseSkuSpec(%q) expected error", c.in)
}
continue
}
if err != nil {
t.Errorf("ParseSkuSpec(%q) unexpected err: %v", c.in, err)
continue
}
if cpu != c.cpu || mem != c.memMB {
t.Errorf("ParseSkuSpec(%q)=%d,%d want %d,%d", c.in, cpu, mem, c.cpu, c.memMB)
}
}
}
func TestServerSkusListOptionsApplySpec(t *testing.T) {
opts := &ServerSkusListOptions{Spec: "2核2G"}
params, err := opts.Params()
if err != nil {
t.Fatal(err)
}
cpu, _ := params.Int("cpu_core_count")
mem, _ := params.Int("memory_size_mb")
if cpu != 2 || mem != 2048 {
t.Fatalf("params cpu=%d mem=%d want 2/2048; raw=%s", cpu, mem, params.String())
}
}

View File

@@ -24,18 +24,20 @@ import (
)
type StorageListOptions struct {
_ struct{} `mcp-desc:"【创建流程中的中间步骤】本工具不能完成创建。创建场景下查完存储后立刻 climc_server_create。严禁只调用本工具后就停止"`
options.BaseListOptions
Share *bool `help:"Share storage list"`
Local *bool `help:"Local storage list"`
Usable *bool `help:"Usable storage list"`
Zone string `help:"List storages in zone" json:"-"`
Region string `help:"List storages in region"`
Usable *bool `help:"Usable storage list" mcp:"true"`
Zone string `help:"List storages in zone" json:"-" mcp:"true"`
Region string `help:"List storages in region" mcp:"true"`
Schedtag string `help:"filter storage by schedtag"`
HostId string `help:"filter storages which attached the specified host"`
HostId string `help:"filter storages which attached the specified host" mcp:"true"`
HostSchedtagId string `help:"filter storage by host schedtag"`
ImageId string `help:"filter storage by image"`
ImageId string `help:"filter storage by image" mcp:"true"`
IsBaremetal *bool `help:"Baremetal storage list"`
}

View File

@@ -23,15 +23,17 @@ import (
)
type VpcListOptions struct {
_ struct{} `mcp-desc:"【创建流程中的中间步骤】本工具不能完成创建。创建场景下查完 VPC 后继续 climc_network_list最后 climc_server_create。严禁只调用本工具后就停止"`
baseoptions.BaseListOptions
Usable *bool `help:"Filter usable vpcs"`
Region string `help:"ID or Name of region" json:"-"`
Usable *bool `help:"Filter usable vpcs" mcp:"true"`
Region string `help:"ID or Name of region" json:"-" mcp:"true"`
Globalvpc string `help:"Filter by globalvpc"`
DnsZoneId string `help:"Filter by DnsZone"`
InterVpcNetworkId string `help:"Filter by InterVpcNetwork"`
ExternalAccessMode string `help:"Filter by external access mode" choices:"distgw|eip|eip-distgw"`
ZoneId string `help:"Filter by zone which has networks"`
ZoneId string `help:"Filter by zone which has networks" mcp:"true"`
UsableForInterVpcNetworkId string `help:"Filter usable vpcs for inter vpc network"`
OrderByWireCount string
CidrBlock string `help:"IPv4 cidr block"`

View File

@@ -21,20 +21,22 @@ import (
)
type ImageListOptions struct {
_ struct{} `mcp-desc:"【创建流程中的中间步骤】用于查询 Glance/本地 KVM 可用镜像。公有云或非 KVM 请用 climc_cached_image_list。创建场景下查完镜像后继续查网络/规格并调用 climc_server_create。严禁只调用本工具后就停止"`
options.BaseListOptions
IsPublic string `help:"filter images public or not(True, False or None)" choices:"true|false"`
IsStandard string `help:"filter images standard or non-standard" choices:"true|false"`
IsPublic string `help:"filter images public or not(True, False or None)" choices:"true|false" mcp:"true"`
IsStandard string `help:"filter images standard or non-standard" choices:"true|false" mcp:"true"`
Protected string `help:"filter images by protected" choices:"true|false"`
IsUefi bool `help:"list uefi image"`
Format []string `help:"Disk formats"`
IsUefi bool `help:"list uefi image" mcp:"true"`
Format []string `help:"Disk formats" mcp:"true"`
SubFormats []string `help:"Sub formats"`
Name string `help:"Name filter"`
OsType []string `help:"Type of OS filter e.g. 'Windows, Linux, Freebsd, Android, macOS, VMWare'"`
Name string `help:"Name filter" mcp:"true"`
OsType []string `help:"Type of OS filter e.g. 'Windows, Linux, Freebsd, Android, macOS, VMWare'" mcp:"true"`
OsTypePreciseMatch bool `help:"OS precise match"`
OsArch []string `help:"Type of OS arch filter e.g. 'x86, arm, arm64, x86_64'"`
OsArch []string `help:"Type of OS arch filter e.g. 'x86, arm, arm64, x86_64'" mcp:"true"`
OsArchPreciseMatch bool `help:"OS arch precise match"`
Distribution []string `help:"Distribution filter, e.g. 'CentOS, Ubuntu, Debian, Windows'"`
Distribution []string `help:"Distribution filter, e.g. 'CentOS, Ubuntu, Debian, Windows'" mcp:"true"`
DistributionPreciseMatch bool `help:"Distribution precise match"`
}

View File

@@ -22,9 +22,11 @@ import (
)
type DomainListOptions struct {
_ struct{} `mcp-desc:"列出域domain。可用 search 等过滤;详情 climc_domain_show创建 climc_domain_create删除 climc_domain_delete"`
options.BaseListOptions
IdpId string `help:"filter by idp_id"`
IdpEntityId string `help:"filter by idp_entity_id"`
IdpId string `help:"filter by idp_id" mcp:"true"`
IdpEntityId string `help:"filter by idp_entity_id" mcp:"true"`
}
func (opts *DomainListOptions) Params() (jsonutils.JSONObject, error) {

View File

@@ -23,14 +23,16 @@ import (
)
type ProjectListOptions struct {
_ struct{} `mcp-desc:"列出项目project/tenant。可用 search/domain 等过滤;详情 climc_project_show创建 climc_project_create删除 climc_project_delete"`
options.BaseListOptions
UserId string `help:"filter by user id"`
GroupId string `help:"filter by group id"`
IdpId string `help:"filter by idp id"`
AdminId []string
UserId string `help:"filter by user id" mcp:"true"`
GroupId string `help:"filter by group id" mcp:"true"`
IdpId string `help:"filter by idp id" mcp:"true"`
AdminId []string `mcp:"true"`
OrderByDomain string `help:"order by domain name" choices:"asc|desc"`
OrderByDomain string `help:"order by domain name" choices:"asc|desc" mcp:"true"`
}
func (opts *ProjectListOptions) Params() (jsonutils.JSONObject, error) {

View File

@@ -21,14 +21,16 @@ import (
)
type UserListOptions struct {
_ struct{} `mcp-desc:"列出用户。可用 search/name/domain 等过滤;详情 climc_user_show创建 climc_user_create删除 climc_user_delete"`
options.BaseListOptions
Name string `help:"Filter by name"`
OrderByDomain string `help:"order by domain name" choices:"asc|desc"`
Role string `help:"Filter by role"`
RoleAssignmentDomainId string `help:"filter role assignment domain"`
RoleAssignmentProjectId string `help:"filter role assignment project"`
IdpId string `help:"filter by idp_id"`
IdpEntityId string `help:"filter by idp_entity_id"`
Name string `help:"Filter by name" mcp:"true"`
OrderByDomain string `help:"order by domain name" choices:"asc|desc" mcp:"true"`
Role string `help:"Filter by role" mcp:"true"`
RoleAssignmentDomainId string `help:"filter role assignment domain" mcp:"true"`
RoleAssignmentProjectId string `help:"filter role assignment project" mcp:"true"`
IdpId string `help:"filter by idp_id" mcp:"true"`
IdpEntityId string `help:"filter by idp_entity_id" mcp:"true"`
}
func (opts *UserListOptions) Params() (jsonutils.JSONObject, error) {

View File

@@ -58,9 +58,9 @@ func (o *ResourceMetricsOptions) GetInput() (*api.ResourceMetricsQueryInput, err
}
type MeasurementsQueryOptions struct {
Scope string `json:"scope"`
ProjectDomainId string `json:"project_domin_id"`
ProjectId string `json:"project_id"`
Scope string `json:"scope" mcp:"true"`
ProjectDomainId string `json:"project_domin_id" mcp:"true"`
ProjectId string `json:"project_id" mcp:"true"`
}
func (o MeasurementsQueryOptions) Params() (jsonutils.JSONObject, error) {
@@ -82,19 +82,21 @@ func (o DatabasesQueryOptions) Property() string {
}
type MetricQueryOptions struct {
_ struct{} `mcp-desc:"查询云平台监控指标时序unifiedmonitor非 QEMU。必填 MEASUREMENT+FIELD过滤用 from/to/interval/tags。不要用 climc_server_monitor"`
MeasurementsQueryOptions
MEASUREMENT string `help:"metric measurement. e.g.: cpu, vm_cpu, vm_mem, disk..."`
FIELD string `help:"metric field. e.g.: usage_active, free..."`
Interval string `help:"metric interval. e.g.: 5m, 1h"`
From string `help:"start time(RFC3339 format). e.g.: 2023-12-06T21:54:42.123Z"`
To string `help:"end time(RFC3339 format). e.g.: 2023-12-18T21:54:42.123Z"`
Tags []string `help:"filter tags. e.g.: vm_name=vm1"`
GroupBy []string `help:"group by tag"`
UseMean bool `help:"calcuate mean result for field"`
SkipCheckSeries bool `help:"skip checking series: not fetch extra tags from region service"`
Reducer string `help:"series result reducer. e.g.: sum, percentile(95)"`
Interval string `help:"metric interval. e.g.: 5m, 1h" mcp:"true"`
From string `help:"start time(RFC3339 format). e.g.: 2023-12-06T21:54:42.123Z" mcp:"true"`
To string `help:"end time(RFC3339 format). e.g.: 2023-12-18T21:54:42.123Z" mcp:"true"`
Tags []string `help:"filter tags. e.g.: vm_name=vm1" mcp:"true"`
GroupBy []string `help:"group by tag" mcp:"true"`
UseMean bool `help:"calcuate mean result for field" mcp:"true"`
SkipCheckSeries bool `help:"skip checking series: not fetch extra tags from region service" mcp:"true"`
Reducer string `help:"series result reducer. e.g.: sum, percentile(95)" mcp:"true"`
}
func (o MetricQueryOptions) GetQueryInput() (*api.MetricQueryInput, error) {

View File

@@ -0,0 +1,81 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapters
import (
"context"
"yunion.io/x/log"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcp-server/options"
)
// CloudpodsAdapter 负责 Cloudpods 认证并创建 mcclient.ClientSession供 climc 工具执行使用。
type CloudpodsAdapter struct {
client *mcclient.Client
}
// NewCloudpodsAdapter 创建一个新的 Cloudpods 适配器实例
func NewCloudpodsAdapter() *CloudpodsAdapter {
client := mcclient.NewClient(
options.Options.AuthURL,
options.Options.Timeout,
false,
true,
"",
"",
)
return &CloudpodsAdapter{client: client}
}
func (a *CloudpodsAdapter) authenticate(ak string, sk string) (mcclient.TokenCredential, error) {
return a.client.AuthenticateByAccessKey(ak, sk, "")
}
// GetSession 获取 Cloudpods API 会话
func (a *CloudpodsAdapter) GetSession(ctx context.Context, ak string, sk string) (*mcclient.ClientSession, error) {
if ak == "" && sk == "" {
ak, sk = GetAKSKFromContext(ctx)
}
var userCred mcclient.TokenCredential
if auth.IsAuthed() {
userCred = policy.FetchUserCredential(ctx)
if userCred != nil {
log.Debugf("GetSession with userCred from context")
} else {
token, err := a.authenticate(ak, sk)
if err != nil {
return nil, err
}
userCred = token
}
return auth.GetSession(ctx, userCred, ""), nil
}
token, err := a.authenticate(ak, sk)
if err != nil {
return nil, err
}
return a.client.NewSession(
context.Background(),
"",
"",
api.EndpointInterfaceApigateway,
token,
), nil
}

View File

@@ -1,126 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapters
import (
"context"
"yunion.io/x/log"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcp-server/options"
)
// Context key 类型,用于从 HTTP Header 传入的 AK/SK 存入 context供 Cursor/Claude 等客户端使用)
type headerCredKey string
const (
ContextKeyAK headerCredKey = "mcp_header_ak"
ContextKeySK headerCredKey = "mcp_header_sk"
)
// GetAKSKFromContext 从 context 中读取连接时通过 Header 传入的 AK/SK未设置时返回空字符串
func GetAKSKFromContext(ctx context.Context) (ak, sk string) {
if v := ctx.Value(ContextKeyAK); v != nil {
if s, ok := v.(string); ok {
ak = s
}
}
if v := ctx.Value(ContextKeySK); v != nil {
if s, ok := v.(string); ok {
sk = s
}
}
return ak, sk
}
// CloudpodsAdapter 是与 Cloudpods API 交互的适配器,负责认证和资源管理
type CloudpodsAdapter struct {
client *mcclient.Client
session *mcclient.ClientSession
}
type CloudRegion struct {
RegionId string `json:"region_id"`
}
// NewCloudpodsAdapter 创建一个新的 Cloudpods 适配器实例
func NewCloudpodsAdapter() *CloudpodsAdapter {
client := mcclient.NewClient(
options.Options.AuthURL,
options.Options.Timeout,
false,
true,
"",
"",
)
return &CloudpodsAdapter{
client: client,
}
}
// authenticate 实现 Cloudpods 的认证逻辑,例如获取访问令牌
func (a *CloudpodsAdapter) authenticate(ak string, sk string) (mcclient.TokenCredential, error) {
if a.session != nil {
return a.session.GetToken(), nil
}
token, err := a.client.AuthenticateByAccessKey(ak, sk, "")
if err != nil {
return nil, err
}
return token, nil
}
func (a *CloudpodsAdapter) getSession(ctx context.Context, ak string, sk string) (*mcclient.ClientSession, error) {
// 若工具未传入 ak/sk则使用连接时 Header 中的 AK/SK与 Cursor/Claude 配置一致)
if ak == "" && sk == "" {
ak, sk = GetAKSKFromContext(ctx)
}
var userCred mcclient.TokenCredential
if auth.IsAuthed() {
userCred = policy.FetchUserCredential(ctx)
if userCred != nil {
log.Infof("getSessionWithUserCred: %v", userCred)
} else {
log.Infof("No userCred in context, will use ak/sk for authentication")
token, err := a.authenticate(ak, sk)
if err != nil {
return nil, err
}
userCred = token
}
a.session = auth.GetSession(ctx, userCred, "")
} else {
token, err := a.authenticate(ak, sk)
if err != nil {
return nil, err
}
a.session = a.client.NewSession(
context.Background(),
"",
"",
api.EndpointInterfaceApigateway,
token,
)
}
return a.session, nil
}

View File

@@ -0,0 +1,58 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapters
import (
"context"
"fmt"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/mcclient/auth"
)
// Context key 类型,用于从 HTTP Header 传入的 AK/SK 存入 context供 Cursor/Claude 等客户端使用)
type headerCredKey string
const (
ContextKeyAK headerCredKey = "mcp_header_ak"
ContextKeySK headerCredKey = "mcp_header_sk"
)
// GetAKSKFromContext 从 context 中读取连接时通过 Header 传入的 AK/SK未设置时返回空字符串
func GetAKSKFromContext(ctx context.Context) (ak, sk string) {
if v := ctx.Value(ContextKeyAK); v != nil {
if s, ok := v.(string); ok {
ak = s
}
}
if v := ctx.Value(ContextKeySK); v != nil {
if s, ok := v.(string); ok {
sk = s
}
}
return ak, sk
}
// HasRequestCredentials 判断请求上下文是否已带用户凭据Token 或 AK/SK
func HasRequestCredentials(ctx context.Context) bool {
if auth.IsAuthed() && policy.FetchUserCredential(ctx) != nil {
return true
}
ak, sk := GetAKSKFromContext(ctx)
return ak != "" && sk != ""
}
// ErrAuthenticationRequired tools/call 无凭据时的明确错误。
var ErrAuthenticationRequired = fmt.Errorf("authentication required: provide X-Auth-Token, AK/SK headers, or X-API-Key on tools/call")

View File

@@ -1,635 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapters
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// StartServer 启动 Cloudpods 中的服务器
func (a *CloudpodsAdapter) StartServer(ctx context.Context, serverId string, req models.ServerStartRequest, ak string, sk string) (*models.ServerOperationResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造启动参数
params := jsonutils.NewDict()
// 如果需要自动续费预付费实例,则设置相应参数
if req.AutoPrepaid {
params.Set("auto_prepaid", jsonutils.NewBool(true))
}
// 如果指定了 QEMU 版本,则设置相应参数
if req.QemuVersion != "" {
params.Set("qemu_version", jsonutils.NewString(req.QemuVersion))
}
// 调用 Cloudpods API 启动服务器
result, err := compute.Servers.PerformAction(session, serverId, "start", params)
if err != nil {
return nil, fmt.Errorf("failed to start server: %w", err)
}
// 构造响应数据
response := &models.ServerOperationResponse{
Operation: "start",
}
// 尝试将结果解析到响应结构体中
if err := result.Unmarshal(response); err != nil {
// 如果解析失败,则尝试获取任务 ID
taskId, _ := result.GetString("task_id")
response.TaskId = taskId
// 如果任务 ID 不为空,则认为操作成功
response.Success = taskId != ""
}
return response, nil
}
// StopServer 停止 Cloudpods 中的服务器
func (a *CloudpodsAdapter) StopServer(ctx context.Context, serverId string, req models.ServerStopRequest, ak string, sk string) (*models.ServerOperationResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造停止参数
params := jsonutils.NewDict()
// 如果需要强制停止,则设置相应参数
if req.IsForce {
params.Set("is_force", jsonutils.NewBool(true))
}
// 如果需要停止计费,则设置相应参数
if req.StopCharging {
params.Set("stop_charging", jsonutils.NewBool(true))
}
// 如果设置了超时时间,则设置相应参数
if req.TimeoutSecs > 0 {
params.Set("timeout_secs", jsonutils.NewInt(req.TimeoutSecs))
}
// 调用 Cloudpods API 停止服务器
result, err := compute.Servers.PerformAction(session, serverId, "stop", params)
if err != nil {
return nil, fmt.Errorf("failed to stop server: %w", err)
}
// 构造响应数据
response := &models.ServerOperationResponse{
Operation: "stop",
}
// 尝试将结果解析到响应结构体中
if err := result.Unmarshal(response); err != nil {
// 如果解析失败,则尝试获取任务 ID
taskId, _ := result.GetString("task_id")
response.TaskId = taskId
// 如果任务 ID 不为空,则认为操作成功
response.Success = taskId != ""
}
return response, nil
}
// RestartServer 重启 Cloudpods 中的服务器
func (a *CloudpodsAdapter) RestartServer(ctx context.Context, serverId string, req models.ServerRestartRequest, ak string, sk string) (*models.ServerOperationResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造重启参数
params := jsonutils.NewDict()
// 如果需要强制重启,则设置相应参数
if req.IsForce {
params.Set("is_force", jsonutils.NewBool(true))
}
// 调用 Cloudpods API 重启服务器
result, err := compute.Servers.PerformAction(session, serverId, "restart", params)
if err != nil {
return nil, fmt.Errorf("failed to restart server: %w", err)
}
// 构造响应数据
response := &models.ServerOperationResponse{
Operation: "restart",
}
// 尝试将结果解析到响应结构体中
if err := result.Unmarshal(response); err != nil {
// 如果解析失败,则尝试获取任务 ID
taskId, _ := result.GetString("task_id")
response.TaskId = taskId
// 如果任务 ID 不为空,则认为操作成功
response.Success = taskId != ""
}
return response, nil
}
// ResetServerPassword 重置 Cloudpods 中服务器的密码
func (a *CloudpodsAdapter) ResetServerPassword(ctx context.Context, serverId string, req models.ServerResetPasswordRequest, ak string, sk string) (*models.ServerOperationResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造密码重置参数
params := jsonutils.NewDict()
// 设置新密码
params.Set("password", jsonutils.NewString(req.Password))
if req.ResetPassword {
params.Set("reset_password", jsonutils.NewBool(true))
}
if req.AutoStart {
params.Set("auto_start", jsonutils.NewBool(true))
}
if req.Username != "" {
params.Set("username", jsonutils.NewString(req.Username))
}
// 调用 Cloudpods API 重置服务器密码
result, err := compute.Servers.PerformAction(session, serverId, "reset-password", params)
if err != nil {
return nil, fmt.Errorf("failed to reset server password: %w", err)
}
// 构造响应数据
response := &models.ServerOperationResponse{
Operation: "reset-password",
}
// 尝试将结果解析到响应结构体中
if err := result.Unmarshal(response); err != nil {
// 如果解析失败,则尝试获取任务 ID
taskId, _ := result.GetString("task_id")
response.TaskId = taskId
// 如果任务 ID 不为空,则认为操作成功
response.Success = taskId != ""
}
return response, nil
}
// DeleteServer 删除 Cloudpods 中的服务器
func (a *CloudpodsAdapter) DeleteServer(ctx context.Context, serverId string, req models.ServerDeleteRequest, ak string, sk string) (*models.ServerOperationResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造删除参数
params := jsonutils.NewDict()
// 如果需要覆盖待删除状态,则设置相应参数
if req.OverridePendingDelete {
params.Set("override_pending_delete", jsonutils.NewBool(true))
}
// 如果需要彻底删除,则设置相应参数
if req.Purge {
params.Set("purge", jsonutils.NewBool(true))
}
// 如果需要删除快照,则设置相应参数
if req.DeleteSnapshots {
params.Set("delete_snapshots", jsonutils.NewBool(true))
}
// 如果需要删除弹性 IP则设置相应参数
if req.DeleteEip {
params.Set("delete_eip", jsonutils.NewBool(true))
}
// 如果需要删除磁盘,则设置相应参数
if req.DeleteDisks {
params.Set("delete_disks", jsonutils.NewBool(true))
}
// 调用 Cloudpods API 删除服务器
result, err := compute.Servers.Delete(session, serverId, params)
if err != nil {
return nil, fmt.Errorf("failed to delete server: %w", err)
}
// 构造响应数据
response := &models.ServerOperationResponse{
Operation: "delete",
}
// 尝试将结果解析到响应结构体中
if err := result.Unmarshal(response); err != nil {
// 如果解析失败,则尝试获取任务 ID
taskId, _ := result.GetString("task_id")
response.TaskId = taskId
// 如果任务 ID 不为空,则认为操作成功
response.Success = taskId != ""
}
return response, nil
}
// CreateServer 在 Cloudpods 中创建服务器
func (a *CloudpodsAdapter) CreateServer(ctx context.Context, req models.CreateServerRequest, ak string, sk string) (*models.CreateServerResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造创建服务器的参数
params := jsonutils.NewDict()
// 设置服务器名称
params.Set("name", jsonutils.NewString(req.Name))
// 设置 CPU 核心数
params.Set("vcpu_count", jsonutils.NewInt(req.VcpuCount))
// 设置内存大小
params.Set("vmem_size", jsonutils.NewInt(req.VmemSize))
// 如果创建数量大于1则设置相应参数
if req.Count > 1 {
params.Set("count", jsonutils.NewInt(int64(req.Count)))
}
// 如果需要自动启动,则设置相应参数
if req.AutoStart {
params.Set("auto_start", jsonutils.NewBool(req.AutoStart))
}
// 如果设置了密码,则设置相应参数
if req.Password != "" {
params.Set("password", jsonutils.NewString(req.Password))
}
// 如果设置了计费类型,则设置相应参数
if req.BillingType != "" {
params.Set("billing_type", jsonutils.NewString(req.BillingType))
}
// 如果设置了计费时长,则设置相应参数
if req.Duration != "" {
params.Set("duration", jsonutils.NewString(req.Duration))
}
// 如果设置了描述,则设置相应参数
if req.Description != "" {
params.Set("description", jsonutils.NewString(req.Description))
}
// 如果设置了主机名,则设置相应参数
if req.Hostname != "" {
params.Set("hostname", jsonutils.NewString(req.Hostname))
}
// 如果设置了虚拟化类型,则设置相应参数
if req.Hypervisor != "" {
params.Set("hypervisor", jsonutils.NewString(req.Hypervisor))
}
// 如果设置了用户数据,则设置相应参数
if req.UserData != "" {
params.Set("user_data", jsonutils.NewString(req.UserData))
}
// 如果设置了密钥对 ID则设置相应参数
if req.KeypairId != "" {
params.Set("keypair_id", jsonutils.NewString(req.KeypairId))
}
// 如果设置了项目 ID则设置相应参数
if req.ProjectId != "" {
params.Set("project_id", jsonutils.NewString(req.ProjectId))
}
// 如果设置了可用区 ID则设置相应参数
if req.ZoneId != "" {
params.Set("prefer_zone_id", jsonutils.NewString(req.ZoneId))
}
// 如果设置了区域 ID则设置相应参数
if req.RegionId != "" {
params.Set("prefer_region_id", jsonutils.NewString(req.RegionId))
}
// 如果需要禁用删除,则设置相应参数
if req.DisableDelete {
params.Set("disable_delete", jsonutils.NewBool(req.DisableDelete))
}
// 如果设置了启动顺序,则设置相应参数
if req.BootOrder != "" {
params.Set("boot_order", jsonutils.NewString(req.BootOrder))
}
// 如果设置了元数据,则设置相应参数
if len(req.Metadata) > 0 {
metaDict := jsonutils.NewDict()
for k, v := range req.Metadata {
metaDict.Set(k, jsonutils.NewString(v))
}
params.Set("__meta__", metaDict)
}
// 构造磁盘参数
disks := jsonutils.NewArray()
// 如果设置了镜像 ID则构造系统磁盘参数
if req.ImageId != "" {
diskDict := jsonutils.NewDict()
diskDict.Set("image_id", jsonutils.NewString(req.ImageId))
diskDict.Set("disk_type", jsonutils.NewString("sys"))
if req.DiskSize > 0 {
diskDict.Set("size", jsonutils.NewInt(req.DiskSize))
}
disks.Add(diskDict)
}
// 构造数据磁盘参数
for _, disk := range req.DataDisks {
diskDict := jsonutils.NewDict()
if disk.ImageId != "" {
diskDict.Set("image_id", jsonutils.NewString(disk.ImageId))
}
if disk.Size > 0 {
diskDict.Set("size", jsonutils.NewInt(disk.Size))
}
diskDict.Set("disk_type", jsonutils.NewString(disk.DiskType))
disks.Add(diskDict)
}
// 如果有磁盘参数,则设置相应参数
if disks.Length() > 0 {
params.Set("disks", disks)
}
// 如果设置了网络 ID则构造网络参数
if req.NetworkId != "" {
networks := jsonutils.NewArray()
netDict := jsonutils.NewDict()
netDict.Set("network", jsonutils.NewString(req.NetworkId))
networks.Add(netDict)
params.Set("nets", networks)
}
// 如果设置了安全组 ID则设置相应参数
if req.SecgroupId != "" {
params.Set("secgrp_id", jsonutils.NewString(req.SecgroupId))
}
// 如果设置了安全组列表,则设置相应参数
if len(req.Secgroups) > 0 {
secgroups := jsonutils.NewArray()
for _, sg := range req.Secgroups {
secgroups.Add(jsonutils.NewString(sg))
}
params.Set("secgroups", secgroups)
}
// 如果设置了服务器规格 ID则设置相应参数
if req.ServerskuId != "" {
params.Set("instance_type", jsonutils.NewString(req.ServerskuId))
}
// 调用 Cloudpods API 创建服务器
result, err := compute.Servers.Create(session, params)
if err != nil {
return nil, fmt.Errorf("failed to create server: %w", err)
}
// 构造响应数据
response := &models.CreateServerResponse{}
if err := result.Unmarshal(response); err != nil {
return nil, fmt.Errorf("failed to unmarshal create server response: %w", err)
}
return response, nil
}
// GetServerMonitor 获取 Cloudpods 中服务器的监控数据
func (a *CloudpodsAdapter) GetServerMonitor(ctx context.Context, serverId string, startTime, endTime int64, metrics []string, ak string, sk string) (*models.MonitorResponse, error) {
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
params := jsonutils.NewDict()
metricQuery := jsonutils.NewArray()
for _, metric := range metrics {
modelDict := jsonutils.NewDict()
modelDict.Set("database", jsonutils.NewString("telegraf"))
modelDict.Set("measurement", jsonutils.NewString("vm_cpu"))
switch metric {
case "cpu_usage":
modelDict.Set("measurement", jsonutils.NewString("vm_cpu"))
case "mem_usage":
modelDict.Set("measurement", jsonutils.NewString("vm_mem"))
case "disk_usage":
modelDict.Set("measurement", jsonutils.NewString("vm_disk"))
case "net_bps_rx", "net_bps_tx":
modelDict.Set("measurement", jsonutils.NewString("vm_netio"))
}
tagsArray := jsonutils.NewArray()
tagDict := jsonutils.NewDict()
tagDict.Set("key", jsonutils.NewString("vm_id"))
tagDict.Set("operator", jsonutils.NewString("="))
tagDict.Set("value", jsonutils.NewString(serverId))
tagsArray.Add(tagDict)
modelDict.Set("tags", tagsArray)
queryDict := jsonutils.NewDict()
queryDict.Set("model", modelDict)
if startTime > 0 {
queryDict.Set("from", jsonutils.NewString(fmt.Sprintf("%d", startTime)))
}
if endTime > 0 {
queryDict.Set("to", jsonutils.NewString(fmt.Sprintf("%d", endTime)))
}
metricQuery.Add(queryDict)
}
params.Set("metric_query", metricQuery)
params.Set("scope", jsonutils.NewString("system"))
params.Set("interval", jsonutils.NewString("60s"))
result, err := monitor.UnifiedMonitorManager.PerformAction(session, "query", "", params)
if err != nil {
return nil, fmt.Errorf("failed to get server monitor data: %w", err)
}
response := &models.MonitorResponse{
Status: 200,
Data: models.MonitorResponseData{
Metrics: []models.MetricData{},
},
}
unifiedmonitor, err := result.Get("unifiedmonitor")
if err != nil {
return nil, fmt.Errorf("failed to get unifiedmonitor data: %w", err)
}
series, err := unifiedmonitor.Get("Series")
if err != nil {
return nil, fmt.Errorf("failed to get series data: %w", err)
}
seriesArray, ok := series.(*jsonutils.JSONArray)
if !ok {
return nil, fmt.Errorf("invalid series data format")
}
for i := 0; i < seriesArray.Length(); i++ {
seriesObj, err := seriesArray.GetAt(i)
if err != nil {
continue
}
name, _ := seriesObj.GetString("name")
metricData := models.MetricData{
Metric: name,
Unit: "%",
Values: []models.MetricValue{},
}
if strings.Contains(name, "net_bps") {
metricData.Unit = "bps"
} else if strings.Contains(name, "disk_io") {
metricData.Unit = "iops"
}
points, err := seriesObj.Get("points")
if err != nil {
continue
}
pointsArray, ok := points.(*jsonutils.JSONArray)
if !ok {
continue
}
for j := 0; j < pointsArray.Length(); j++ {
pointObj, err := pointsArray.GetAt(j)
if err != nil {
continue
}
pointArray, ok := pointObj.(*jsonutils.JSONArray)
if !ok || pointArray.Length() < 2 {
continue
}
timestamp, err := pointArray.GetAt(0)
if err != nil {
continue
}
value, err := pointArray.GetAt(1)
if err != nil {
continue
}
timestampStr, _ := timestamp.GetString()
valueStr, _ := value.GetString()
timestampInt, _ := strconv.ParseInt(timestampStr, 10, 64)
valueFloat, _ := strconv.ParseFloat(valueStr, 64)
metricData.Values = append(metricData.Values, models.MetricValue{
Timestamp: timestampInt,
Value: valueFloat,
})
}
response.Data.Metrics = append(response.Data.Metrics, metricData)
}
return response, nil
}
// GetServerStats 获取 Cloudpods 中服务器的实时统计数据
func (a *CloudpodsAdapter) GetServerStats(ctx context.Context, serverId string, ak string, sk string) (*models.ServerStatsResponse, error) {
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
params := jsonutils.NewDict()
result, err := compute.Servers.GetSpecific(session, serverId, "stats", params)
if err != nil {
return nil, fmt.Errorf("failed to get server stats: %w", err)
}
statsData := models.ServerStatsData{}
cpuUsed, _ := result.Float("cpu_used")
statsData.CPUUsage = cpuUsed * 100
memSize, _ := result.Int("mem_size")
memUsed, _ := result.Int("mem_used")
if memSize > 0 {
statsData.MemUsage = float64(memUsed) / float64(memSize) * 100
}
diskSize, _ := result.Int("disk_size")
diskUsed, _ := result.Int("disk_used")
if diskSize > 0 {
statsData.DiskUsage = float64(diskUsed) / float64(diskSize) * 100
}
netInRate, _ := result.Float("net_in_rate")
netOutRate, _ := result.Float("net_out_rate")
statsData.NetBpsRx = int64(netInRate)
statsData.NetBpsTx = int64(netOutRate)
statsData.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
response := &models.ServerStatsResponse{
Status: 200,
Data: statsData,
}
return response, nil
}

View File

@@ -1,500 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapters
import (
"context"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/mcclient/modules/image"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// ListCloudRegions 查询 Cloudpods 中的区域列表
func (a CloudpodsAdapter) ListCloudRegions(ctx context.Context, limit int, offset int, search string, provider string, ak string, sk string) (*models.CloudregionListResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造查询参数
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("max"))
if limit > 0 {
// 设置查询结果数量限制
params.Set("limit", jsonutils.NewInt(int64(limit)))
}
if offset > 0 {
// 设置查询偏移量
params.Set("offset", jsonutils.NewInt(int64(offset)))
}
if search != "" {
// 设置搜索关键字
params.Set("search", jsonutils.NewString(search))
}
if provider != "" {
// 设置云提供商过滤条件
providers := jsonutils.NewArray()
providers.Add(jsonutils.NewString(provider))
params.Set("providers", providers)
}
// 调用 Cloudpods API 查询区域列表
result, err := compute.Cloudregions.List(session, params)
if err != nil {
return nil, err
}
// 构造响应数据
response := &models.CloudregionListResponse{
Limit: int64(limit),
Offset: int64(offset),
Cloudregions: make([]models.CloudregionDetails, 0),
Total: int64(result.Total),
}
// 遍历查询结果,将数据转换为响应格式
for _, data := range result.Data {
region := models.CloudregionDetails{}
if err := data.Unmarshal(&region); err != nil {
// 如果数据转换失败,记录警告日志并跳过该条数据
log.Warningf("Failed to unmarshal cloudregion details: %s", err)
continue
}
response.Cloudregions = append(response.Cloudregions, region)
}
return response, nil
}
// ListVPCs 查询 Cloudpods 中的 VPC 列表
func (a *CloudpodsAdapter) ListVPCs(ctx context.Context, limit int, offset int, search string, cloudregionId string, ak string, sk string) (*models.VpcListResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造查询参数
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("max"))
if limit > 0 {
// 设置查询结果数量限制
params.Set("limit", jsonutils.NewInt(int64(limit)))
}
if offset > 0 {
// 设置查询偏移量
params.Set("offset", jsonutils.NewInt(int64(offset)))
}
if search != "" {
// 设置搜索关键字
params.Set("search", jsonutils.NewString(search))
}
if cloudregionId != "" {
// 设置云区域 ID 过滤条件
cloudregionIds := jsonutils.NewArray()
cloudregionIds.Add(jsonutils.NewString(cloudregionId))
params.Set("cloudregion_id", cloudregionIds)
}
// 调用 Cloudpods API 查询 VPC 列表
result, err := compute.Vpcs.List(session, params)
if err != nil {
return nil, fmt.Errorf("failed to list vpcs: %w", err)
}
// 构造响应数据
response := &models.VpcListResponse{
Limit: int64(limit),
Offset: int64(offset),
Vpcs: make([]models.VpcDetails, 0),
Total: int64(result.Total),
}
// 遍历查询结果,将数据转换为响应格式
for _, data := range result.Data {
vpc := models.VpcDetails{}
if err := data.Unmarshal(&vpc); err != nil {
// 如果数据转换失败,记录警告日志并跳过该条数据
log.Warningf("Failed to unmarshal vpc details: %s", err)
continue
}
response.Vpcs = append(response.Vpcs, vpc)
}
return response, nil
}
// ListNetworks 查询 Cloudpods 中的网络列表
func (a *CloudpodsAdapter) ListNetworks(ctx context.Context, limit int, offset int, search string, vpcId string, ak string, sk string) (*models.NetworkListResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造查询参数
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("max"))
if limit > 0 {
// 设置查询结果数量限制
params.Set("limit", jsonutils.NewInt(int64(limit)))
}
if offset > 0 {
// 设置查询偏移量
params.Set("offset", jsonutils.NewInt(int64(offset)))
}
if search != "" {
// 设置搜索关键字
params.Set("search", jsonutils.NewString(search))
}
if vpcId != "" {
// 设置 VPC ID 过滤条件
//vpcIds := jsonutils.NewArray()
//vpcIds.Add(jsonutils.NewString(vpcId))
//params.Set("vpc_id", vpcIds)
params.Set("vpc_id", jsonutils.NewString(vpcId))
}
// 调用 Cloudpods API 查询网络列表
result, err := compute.Networks.List(session, params)
if err != nil {
return nil, fmt.Errorf("failed to list networks: %w", err)
}
// 构造响应数据
response := &models.NetworkListResponse{
Limit: int64(limit),
Offset: int64(offset),
Networks: make([]models.NetworkDetails, 0),
Total: int64(result.Total),
}
// 遍历查询结果,将数据转换为响应格式
for _, data := range result.Data {
network := models.NetworkDetails{}
if err := data.Unmarshal(&network); err != nil {
// 如果数据转换失败,记录警告日志并跳过该条数据
log.Warningf("Failed to unmarshal network details: %s", err)
continue
}
response.Networks = append(response.Networks, network)
}
return response, nil
}
// ListImages 查询 Cloudpods 中的镜像列表
func (a *CloudpodsAdapter) ListImages(ctx context.Context, limit int, offset int, search string, osTypes []string, ak string, sk string) (*models.ImageListResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造查询参数
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("max"))
if limit > 0 {
// 设置查询结果数量限制
params.Set("limit", jsonutils.NewInt(int64(limit)))
}
if offset > 0 {
// 设置查询偏移量
params.Set("offset", jsonutils.NewInt(int64(offset)))
}
if search != "" {
// 设置搜索关键字
params.Set("search", jsonutils.NewString(search))
}
if len(osTypes) > 0 {
// 设置操作系统类型过滤条件
osTypesArray := jsonutils.NewArray()
for _, osType := range osTypes {
osTypesArray.Add(jsonutils.NewString(osType))
}
params.Set("os_types", osTypesArray)
}
// 调用 Cloudpods API 查询镜像列表
result, err := image.Images.List(session, params)
if err != nil {
return nil, fmt.Errorf("failed to list images: %w", err)
}
// 构造响应数据
response := &models.ImageListResponse{
Limit: int64(limit),
Offset: int64(offset),
Images: make([]models.ImageDetails, 0),
Total: int64(result.Total),
}
// 遍历查询结果,将数据转换为响应格式
for _, data := range result.Data {
image := models.ImageDetails{}
if err := data.Unmarshal(&image); err != nil {
// 如果数据转换失败,记录警告日志并跳过该条数据
log.Warningf("Failed to unmarshal image details: %s", err)
continue
}
response.Images = append(response.Images, image)
}
return response, nil
}
// ListServerSkus 查询 Cloudpods 中的服务器规格列表
func (a *CloudpodsAdapter) ListServerSkus(ctx context.Context, limit int, offset int, search string, cloudregionIds []string, zoneIds []string, cpuCoreCount []string, memorySizeMB []string, providers []string, cpuArch []string, ak string, sk string) (*models.ServerSkuListResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造查询参数
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("max"))
if limit > 0 {
// 设置查询结果数量限制
params.Set("limit", jsonutils.NewInt(int64(limit)))
}
if offset > 0 {
// 设置查询偏移量
params.Set("offset", jsonutils.NewInt(int64(offset)))
}
if search != "" {
// 设置搜索关键字
params.Set("search", jsonutils.NewString(search))
}
if len(cloudregionIds) > 0 {
// 设置云区域 ID 过滤条件
cloudregionIdArray := jsonutils.NewArray()
for _, id := range cloudregionIds {
cloudregionIdArray.Add(jsonutils.NewString(id))
}
params.Set("cloudregion_id", cloudregionIdArray)
}
if len(zoneIds) > 0 {
// 设置可用区 ID 过滤条件
zoneIdArray := jsonutils.NewArray()
for _, id := range zoneIds {
zoneIdArray.Add(jsonutils.NewString(id))
}
params.Set("zone_ids", zoneIdArray)
}
if len(cpuCoreCount) > 0 {
// 设置 CPU 核心数过滤条件
cpuCoreArray := jsonutils.NewArray()
for _, count := range cpuCoreCount {
cpuCoreArray.Add(jsonutils.NewString(count))
}
params.Set("cpu_core_count", cpuCoreArray)
}
if len(memorySizeMB) > 0 {
// 设置内存大小过滤条件
memoryArray := jsonutils.NewArray()
for _, size := range memorySizeMB {
memoryArray.Add(jsonutils.NewString(size))
}
params.Set("memory_size_mb", memoryArray)
}
if len(providers) > 0 {
// 设置提供商过滤条件
providerArray := jsonutils.NewArray()
for _, provider := range providers {
providerArray.Add(jsonutils.NewString(provider))
}
params.Set("providers", providerArray)
}
if len(cpuArch) > 0 {
// 设置 CPU 架构过滤条件
cpuArchArray := jsonutils.NewArray()
for _, arch := range cpuArch {
cpuArchArray.Add(jsonutils.NewString(arch))
}
params.Set("cpu_arch", cpuArchArray)
}
// 调用 Cloudpods API 查询服务器规格列表
result, err := compute.ServerSkus.List(session, params)
if err != nil {
return nil, fmt.Errorf("failed to list server skus: %w", err)
}
// 构造响应数据
response := &models.ServerSkuListResponse{
Limit: int64(limit),
Offset: int64(offset),
Serverskus: make([]models.ServerSkuDetails, 0),
Total: int64(result.Total),
}
// 遍历查询结果,将数据转换为响应格式
for _, data := range result.Data {
sku := models.ServerSkuDetails{}
if err := data.Unmarshal(&sku); err != nil {
// 如果数据转换失败,记录警告日志并跳过该条数据
log.Warningf("Failed to unmarshal server sku details: %s", err)
continue
}
response.Serverskus = append(response.Serverskus, sku)
}
return response, nil
}
// ListStorages 查询 Cloudpods 中的存储列表
func (a *CloudpodsAdapter) ListStorages(ctx context.Context, limit int, offset int, search string, cloudregionIds []string, zoneIds []string, providers []string, storageTypes []string, hostId string, ak string, sk string) (*models.StorageListResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造查询参数
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("max"))
if limit > 0 {
// 设置查询结果数量限制
params.Set("limit", jsonutils.NewInt(int64(limit)))
}
if offset > 0 {
// 设置查询偏移量
params.Set("offset", jsonutils.NewInt(int64(offset)))
}
if search != "" {
// 设置搜索关键字
params.Set("search", jsonutils.NewString(search))
}
if len(cloudregionIds) > 0 {
// 设置云区域 ID 过滤条件
cloudregionIdArray := jsonutils.NewArray()
for _, id := range cloudregionIds {
cloudregionIdArray.Add(jsonutils.NewString(id))
}
params.Set("cloudregion_id", cloudregionIdArray)
}
if len(zoneIds) > 0 {
// 设置可用区 ID 过滤条件
zoneIdArray := jsonutils.NewArray()
for _, id := range zoneIds {
zoneIdArray.Add(jsonutils.NewString(id))
}
params.Set("zone_ids", zoneIdArray)
}
if len(providers) > 0 {
// 设置提供商过滤条件
providerArray := jsonutils.NewArray()
for _, provider := range providers {
providerArray.Add(jsonutils.NewString(provider))
}
params.Set("providers", providerArray)
}
if len(storageTypes) > 0 {
// 设置存储类型过滤条件
for _, storageType := range storageTypes {
params.Set("storage_type", jsonutils.NewString(storageType))
break
}
}
if hostId != "" {
// 设置主机 ID 过滤条件
params.Set("host_id", jsonutils.NewString(hostId))
}
// 调用 Cloudpods API 查询存储列表
result, err := compute.Storages.List(session, params)
if err != nil {
return nil, fmt.Errorf("failed to list storages: %w", err)
}
// 构造响应数据
response := &models.StorageListResponse{
Limit: int64(limit),
Offset: int64(offset),
Storages: make([]models.StorageDetails, 0),
Total: int64(result.Total),
}
// 遍历查询结果,将数据转换为响应格式
for _, data := range result.Data {
storage := models.StorageDetails{}
if err := data.Unmarshal(&storage); err != nil {
// 如果数据转换失败,记录警告日志并跳过该条数据
log.Warningf("Failed to unmarshal storage details: %s", err)
continue
}
response.Storages = append(response.Storages, storage)
}
return response, nil
}
// ListServers 查询 Cloudpods 中的服务器列表
func (a *CloudpodsAdapter) ListServers(ctx context.Context, limit int, offset int, search string, status string, ak string, sk string) (*models.ServerListResponse, error) {
// 获取 Cloudpods 会话
session, err := a.getSession(ctx, ak, sk)
if err != nil {
return nil, err
}
// 构造查询参数
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("max"))
if limit > 0 {
// 设置查询结果数量限制
params.Set("limit", jsonutils.NewInt(int64(limit)))
}
if offset > 0 {
// 设置查询偏移量
params.Set("offset", jsonutils.NewInt(int64(offset)))
}
if search != "" {
// 设置搜索关键字
params.Set("search", jsonutils.NewString(search))
}
if status != "" {
// 设置服务器状态过滤条件
params.Set("status", jsonutils.NewString(status))
}
// 调用 Cloudpods API 查询服务器列表
result, err := compute.Servers.List(session, params)
if err != nil {
return nil, fmt.Errorf("failed to list servers: %w", err)
}
// 构造响应数据
response := &models.ServerListResponse{
Limit: int64(limit),
Offset: int64(offset),
Servers: make([]models.ServerDetails, 0),
Total: int64(result.Total),
}
// 遍历查询结果,将数据转换为响应格式
for _, data := range result.Data {
server := models.ServerDetails{}
if err := data.Unmarshal(&server); err != nil {
// 如果数据转换失败,记录警告日志并跳过该条数据
log.Warningf("Failed to unmarshal server details: %s", err)
continue
}
response.Servers = append(response.Servers, server)
}
return response, nil
}

View File

@@ -0,0 +1,595 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/util/fileutils"
"yunion.io/x/pkg/util/regutils"
"yunion.io/x/onecloud/cmd/climc/shell"
"yunion.io/x/onecloud/pkg/apis/compute"
schedapi "yunion.io/x/onecloud/pkg/apis/scheduler"
"yunion.io/x/onecloud/pkg/mcclient"
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
schedmodules "yunion.io/x/onecloud/pkg/mcclient/modules/scheduler"
computeoptions "yunion.io/x/onecloud/pkg/mcclient/options/compute"
"yunion.io/x/onecloud/pkg/mcp-server/options"
)
const (
serverCreatePollInterval = 5 * time.Second
)
func cloneArgs(src map[string]interface{}) map[string]interface{} {
dst := make(map[string]interface{}, len(src)+1)
for k, v := range src {
dst[k] = v
}
return dst
}
func argLookup(args map[string]interface{}, keys ...string) (interface{}, bool) {
normalize := func(k string) string {
return strings.ReplaceAll(k, "_", "-")
}
for _, key := range keys {
if v, ok := args[key]; ok {
return v, true
}
alt := strings.ReplaceAll(key, "-", "_")
if v, ok := args[alt]; ok {
return v, true
}
want := normalize(key)
for k, v := range args {
if normalize(k) == want {
return v, true
}
}
}
return nil, false
}
func firstString(v interface{}) string {
parts := valueToArgvParts(v)
if len(parts) == 0 {
return ""
}
return parts[0]
}
// mapCreateArgsToForecastArgs 将 server-create 参数映射为 scheduler-forecast CLI 参数。
func mapCreateArgsToForecastArgs(args map[string]interface{}) map[string]interface{} {
out := map[string]interface{}{}
// SchedulerForecastOptions 嵌入 ServerConfigsCLI token 与 create 侧大多一致
copyKeys := []struct {
from []string
to string
}{
{[]string{"disk"}, "disk"},
{[]string{"net"}, "net"},
{[]string{"region", "prefer-region"}, "region"},
{[]string{"zone", "prefer-zone"}, "zone"},
{[]string{"host", "prefer-host"}, "host"},
{[]string{"manager", "prefer-manager"}, "manager"},
{[]string{"hypervisor"}, "hypervisor"},
{[]string{"project", "tenant"}, "project"},
{[]string{"count"}, "count"},
{[]string{"schedtag"}, "schedtag"},
{[]string{"ncpu"}, "ncpu"},
{[]string{"cdrom", "iso"}, "cdrom"},
{[]string{"sku", "instance-type"}, "sku"},
}
for _, item := range copyKeys {
if v, ok := argLookup(args, item.from...); ok {
out[item.to] = v
}
}
// 无 sku 时,把 mem-spec如 2048M/2G转成 forecast 的 --memMB
if _, hasSku := out["sku"]; !hasSku {
if v, ok := argLookup(args, "mem-spec"); ok {
spec := firstString(v)
if regutils.MatchSize(spec) {
if mb, err := fileutils.GetSizeMb(spec, 'M', 1024); err == nil && mb > 0 {
out["mem"] = strconv.Itoa(mb)
}
} else if n, err := strconv.Atoi(strings.TrimSpace(spec)); err == nil && n > 0 {
out["mem"] = strconv.Itoa(n)
}
}
}
return out
}
func findCommand(name string) (shell.CMD, bool) {
for _, cmd := range shell.CommandTable {
if cmd.Command == name {
return cmd, true
}
}
return shell.CMD{}, false
}
func runSchedulerForecast(session *mcclient.ClientSession, createArgs map[string]interface{}) (jsonutils.JSONObject, error) {
cmd, ok := findCommand("scheduler-forecast")
if !ok {
return nil, fmt.Errorf("scheduler-forecast command not registered; import climc shell/scheduler")
}
forecastArgs := mapCreateArgsToForecastArgs(createArgs)
parser, optPtr, err := newArgumentParser(cmd)
if err != nil {
return nil, err
}
argv := mcpArgsToArgv(parser, forecastArgs)
if err := parser.ParseArgs(argv, false); err != nil {
return nil, fmt.Errorf("parse scheduler-forecast args: %w (argv=%v mapped=%v)", err, argv, forecastArgs)
}
filled := parser.Options()
opts, ok := filled.(*computeoptions.SchedulerForecastOptions)
if !ok {
if o, ok2 := optPtr.(*computeoptions.SchedulerForecastOptions); ok2 {
opts = o
} else {
return nil, fmt.Errorf("unexpected scheduler-forecast options type %T", filled)
}
}
input, err := opts.Params(session)
if err != nil {
return nil, fmt.Errorf("build forecast input: %w", err)
}
prepareForecastInput(input)
return schedmodules.SchedManager.DoForecast(session, input.JSON(input))
}
// prepareForecastInput 修正公有云 forecast 入参,避免被当成 KVM 去跑 host_cpu/host_memory。
// 公有云驱动 DoScheduleCPUFilter/MemoryFilter=false但若 hypervisor/provider 对不上,
// GetHypervisorDriver() 为 nilhost_cpu 仍会执行而阿里云宿主机是虚拟的cpu total/free 常为 0。
func prepareForecastInput(input *schedapi.ScheduleInput) {
if input == nil {
return
}
if input.ServerConfigs == nil {
input.ServerConfigs = &compute.ServerConfigs{}
}
hv := strings.ToLower(strings.TrimSpace(input.Hypervisor))
if !isManagedHypervisor(hv) {
return
}
if prov := providerFromHypervisor(hv); prov != "" {
if input.Provider == "" || input.Provider == compute.CLOUD_PROVIDER_ONECLOUD {
input.Provider = prov
}
}
// 公有云按套餐调度:清掉 climc forecast Options 的默认 ncpu=1/mem=512
// 防止 driver 解析失败时 host_cpu 用默认核数把所有宿主机滤掉。
if strings.TrimSpace(input.InstanceType) != "" {
input.Ncpu = 0
input.Memory = 0
}
}
func isManagedHypervisor(hv string) bool {
switch hv {
case compute.HYPERVISOR_KVM, compute.HYPERVISOR_BAREMETAL, compute.HYPERVISOR_POD,
"hypervisor", "":
return false
default:
return hv != ""
}
}
func providerFromHypervisor(hv string) string {
switch strings.ToLower(hv) {
case compute.HYPERVISOR_ALIYUN:
return compute.CLOUD_PROVIDER_ALIYUN
case compute.HYPERVISOR_AWS:
return compute.CLOUD_PROVIDER_AWS
case compute.HYPERVISOR_AZURE:
return compute.CLOUD_PROVIDER_AZURE
case compute.HYPERVISOR_QCLOUD:
return compute.CLOUD_PROVIDER_QCLOUD
case compute.HYPERVISOR_HUAWEI:
return compute.CLOUD_PROVIDER_HUAWEI
case compute.HYPERVISOR_GOOGLE:
return compute.CLOUD_PROVIDER_GOOGLE
case compute.HYPERVISOR_OPENSTACK:
return compute.CLOUD_PROVIDER_OPENSTACK
default:
// 多数公有云 hypervisor 与 provider 仅大小写不同
if hv == "" {
return ""
}
return strings.ToUpper(hv[:1]) + hv[1:]
}
}
// readyForecastCandidates 返回 forecast 中 error 为空的候选(与调度历史 Result.candidates 成功语义一致)。
func readyForecastCandidates(forecast jsonutils.JSONObject) []jsonutils.JSONObject {
if forecast == nil {
return nil
}
arr, err := forecast.GetArray("candidates")
if err != nil || len(arr) == 0 {
return nil
}
ready := make([]jsonutils.JSONObject, 0, len(arr))
for _, c := range arr {
errStr, _ := c.GetString("error")
if strings.TrimSpace(errStr) != "" {
continue
}
ready = append(ready, c)
}
return ready
}
// forecastSucceeded 判断预调度是否成功:优先看合格 candidates兼容 can_create。
func forecastSucceeded(forecast jsonutils.JSONObject) error {
if forecast == nil {
return fmt.Errorf("scheduler-forecast returned empty result")
}
ready := readyForecastCandidates(forecast)
reqCount, err := forecast.Int("req_count")
if err != nil || reqCount <= 0 {
reqCount = 1
}
if int64(len(ready)) >= reqCount {
return nil
}
if jsonutils.QueryBoolean(forecast, "can_create", false) {
return nil
}
reasons, _ := forecast.Get("not_allow_reasons")
allow, _ := forecast.Int("allow_count")
return fmt.Errorf(
"scheduler-forecast not schedulable: ready_candidates=%d allow_count=%d req_count=%d can_create=false reasons=%s\nforecast=%s",
len(ready), allow, reqCount, reasons, forecast.String(),
)
}
func extractJSONObject(s string) json.RawMessage {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
var obj interface{}
if err := json.Unmarshal([]byte(s), &obj); err == nil {
b, _ := json.Marshal(obj)
return b
}
b, _ := json.Marshal(map[string]string{"raw": s})
return b
}
func extractServerID(createOut string) string {
createOut = strings.TrimSpace(createOut)
var obj map[string]interface{}
if err := json.Unmarshal([]byte(createOut), &obj); err != nil {
return ""
}
if id, ok := obj["id"].(string); ok {
return id
}
return ""
}
func isTerminalSuccessStatus(status string) bool {
switch status {
case compute.VM_RUNNING, compute.VM_READY:
return true
default:
return false
}
}
func isTerminalFailStatus(status string) bool {
switch status {
case compute.VM_CREATE_FAILED,
compute.VM_SCHEDULE_FAILED,
compute.VM_DEPLOY_FAILED,
compute.VM_START_FAILED,
compute.VM_DISK_FAILED,
compute.VM_NETWORK_FAILED,
compute.VM_DEVICE_FAILED,
compute.VM_UNKNOWN:
return true
default:
return strings.Contains(status, "fail")
}
}
func waitServerRunningOrReady(ctx context.Context, session *mcclient.ClientSession, id string) (string, error) {
deadline := time.Now().Add(options.ServerCreateWaitDuration())
var lastStatus string
for time.Now().Before(deadline) {
if err := ctx.Err(); err != nil {
return lastStatus, err
}
obj, err := modules.Servers.Get(session, id, nil)
if err != nil {
log.Warningf("wait server %s: get failed: %s", id, err)
select {
case <-ctx.Done():
return lastStatus, ctx.Err()
case <-time.After(serverCreatePollInterval):
}
continue
}
lastStatus, _ = obj.GetString("status")
if isTerminalSuccessStatus(lastStatus) {
return lastStatus, nil
}
if isTerminalFailStatus(lastStatus) {
return lastStatus, fmt.Errorf("server %s entered failed status %q", id, lastStatus)
}
select {
case <-ctx.Done():
return lastStatus, ctx.Err()
case <-time.After(serverCreatePollInterval):
}
}
return lastStatus, fmt.Errorf("timeout waiting server %s to become running/ready, last status=%q", id, lastStatus)
}
func diskHasBackend(disk string) bool {
for _, part := range strings.Split(disk, ",") {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 {
continue
}
key := strings.ToLower(strings.TrimSpace(kv[0]))
if (key == "backend" || key == "storage_type") && strings.TrimSpace(kv[1]) != "" {
return true
}
}
return false
}
func preferDiskBackend(types []string) string {
if len(types) == 0 {
return ""
}
prefer := []string{"cloud_essd", "cloud_ssd", "cloud_efficiency", "cloud", "local"}
lower := make([]string, len(types))
for i, t := range types {
lower[i] = strings.ToLower(strings.TrimSpace(t))
}
for _, p := range prefer {
for i, t := range lower {
if t == p {
return types[i]
}
}
}
return types[0]
}
func storageTypesFromCapability(capa jsonutils.JSONObject, hypervisor string) []string {
if capa == nil {
return nil
}
hv := strings.ToLower(strings.TrimSpace(hypervisor))
asStrings := func(obj jsonutils.JSONObject) []string {
if obj == nil {
return nil
}
if arr, ok := obj.(*jsonutils.JSONArray); ok {
out := arr.GetStringArray()
if len(out) > 0 {
return out
}
}
if s, err := obj.GetString(); err == nil && strings.TrimSpace(s) != "" {
return []string{s}
}
return nil
}
for _, key := range []string{"storage_types2", "StorageTypes2"} {
m, err := capa.GetMap(key)
if err != nil || len(m) == 0 {
continue
}
if hv != "" {
for k, v := range m {
if strings.ToLower(k) == hv {
if arr := asStrings(v); len(arr) > 0 {
return arr
}
}
}
}
for _, v := range m {
if arr := asStrings(v); len(arr) > 0 {
return arr
}
}
}
return nil
}
// ensureDiskBackend 公有云 disk 缺 backend 时,按 prefer-region 拉 cloud-region-capability 自动补全。
func ensureDiskBackend(session *mcclient.ClientSession, args map[string]interface{}) {
var hv string
if v, ok := argLookup(args, "hypervisor"); ok {
hv = firstString(v)
}
if !isManagedHypervisor(strings.ToLower(hv)) {
return
}
rawDisk, ok := argLookup(args, "disk")
if !ok {
return
}
disks := valueToArgvParts(rawDisk)
if len(disks) == 0 {
return
}
need := false
for _, d := range disks {
if !diskHasBackend(d) {
need = true
break
}
}
if !need {
return
}
var regionID string
if v, ok := argLookup(args, "prefer-region", "region"); ok {
regionID = firstString(v)
}
if regionID == "" {
log.Warningf("disk missing backend but prefer-region empty; skip auto-fill")
return
}
capa, err := modules.Cloudregions.GetSpecific(session, regionID, "capability", nil)
if err != nil {
log.Warningf("cloud-region-capability %s failed: %s", regionID, err)
return
}
backend := preferDiskBackend(storageTypesFromCapability(capa, hv))
if backend == "" {
log.Warningf("cloud-region-capability %s has no storage_types2 for %s", regionID, hv)
return
}
for i, d := range disks {
if !diskHasBackend(d) {
disks[i] = strings.TrimSuffix(d, ",") + ",backend=" + backend
}
}
args["disk"] = disks
log.Infof("auto-filled disk backend=%s from region %s capability", backend, regionID)
}
// ensureNetworkAutoSched未指定网络时注入 CLI "random"ParseNetworkConfig → Exit=false
// 等价 API nets:[{"exit":false}],由调度器自动选网。
func ensureNetworkAutoSched(args map[string]interface{}) {
raw, ok := argLookup(args, "net", "nets")
if ok {
parts := valueToArgvParts(raw)
if len(parts) > 0 {
return
}
}
args["net"] = []string{"random"}
delete(args, "nets")
log.Infof("auto-filled net=random (API equivalent nets:[{exit:false}]) for network auto schedule")
}
// ensureGenerateName默认开启 generate-name用 NAME 作为模板自动去重,避免 DuplicateNameError。
func ensureGenerateName(args map[string]interface{}) {
if _, ok := argLookup(args, "generate-name", "generate_name", "GenerateName"); ok {
return
}
args["generate-name"] = true
log.Infof("auto-enabled generate-name to avoid DuplicateNameError")
}
// handleServerCreate创建前 scheduler-forecast 预调度,创建后等待 running 或 ready关机
// dry-run 仅做参数校验,不是预调度。等待超时不视为失败:返回 server_id 供 agent 继续查询。
func (t *ClimcTool) handleServerCreate(ctx context.Context, session *mcclient.ClientSession, args map[string]interface{}) (string, error) {
ensureDiskBackend(session, args)
ensureNetworkAutoSched(args)
ensureGenerateName(args)
// 1) 预调度scheduler-forecast
forecast, err := runSchedulerForecast(session, args)
if err != nil {
return "", fmt.Errorf("scheduler-forecast failed: %w", err)
}
if err := forecastSucceeded(forecast); err != nil {
return "", err
}
forecastRaw := json.RawMessage(forecast.String())
// 2) 真实创建(去掉 dry-run避免走 suggestion 旁路)
createArgs := cloneArgs(args)
delete(createArgs, "dry-run")
delete(createArgs, "dry_run")
delete(createArgs, "DryRun")
createOut, err := invokeCommand(t.cmd, session, createArgs)
if err != nil {
return "", fmt.Errorf("create failed after scheduler-forecast ok: %w\nforecast:\n%s\noutput:\n%s", err, forecast.String(), createOut)
}
serverID := extractServerID(createOut)
result := map[string]interface{}{
"preschedule": forecastRaw,
"server": json.RawMessage(extractJSONObject(createOut)),
}
if serverID == "" {
result["wait_error"] = "create succeeded but server id missing; skip wait"
b, _ := json.Marshal(result)
return string(b), nil
}
// 3) 等待 running / ready
status, waitErr := waitServerRunningOrReady(ctx, session, serverID)
result["final_status"] = status
result["server_id"] = serverID
if waitErr != nil {
result["wait_error"] = waitErr.Error()
if obj, gerr := modules.Servers.Get(session, serverID, nil); gerr == nil {
result["server"] = json.RawMessage(obj.String())
if status == compute.VM_SCHEDULE_FAILED || status == "sched_fail" {
if progress, _ := obj.GetString("progress"); progress != "" {
result["schedule_hint"] = progress
}
result["hint"] = "调度失败(sched_fail)。若未指定网络,应使用自动调度 net=[\"random\"](等价 nets:[{exit:false}]);也可检查 prefer-region、instance-type、disk.backend 是否与区域能力匹配后重试。"
}
}
// 超时/取消:创建已成功,返回 server_id 让 agent 用 climc_server_show 继续查,不把整次 tool 判失败
if isWaitTimeoutOrCanceled(waitErr) {
result["wait_pending"] = true
if result["hint"] == nil {
result["hint"] = "创建已提交但尚未进入 running/ready。请用 climc_server_show 查询 status勿重复创建。"
}
b, _ := json.Marshal(result)
return string(b), nil
}
b, _ := json.Marshal(result)
return string(b), waitErr
}
if obj, gerr := modules.Servers.Get(session, serverID, nil); gerr == nil {
result["server"] = json.RawMessage(obj.String())
}
b, err := json.Marshal(result)
if err != nil {
return createOut, nil
}
return string(b), nil
}
func isWaitTimeoutOrCanceled(err error) bool {
if err == nil {
return false
}
if err == context.Canceled || err == context.DeadlineExceeded {
return true
}
msg := err.Error()
return strings.Contains(msg, "timeout waiting server") || strings.Contains(msg, "context canceled") || strings.Contains(msg, "context deadline")
}

View File

@@ -0,0 +1,174 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"strings"
"testing"
"yunion.io/x/jsonutils"
_ "yunion.io/x/onecloud/cmd/climc/shell/scheduler"
"yunion.io/x/onecloud/pkg/apis/compute"
schedapi "yunion.io/x/onecloud/pkg/apis/scheduler"
)
func TestMapCreateArgsToForecastArgs(t *testing.T) {
got := mapCreateArgsToForecastArgs(map[string]interface{}{
"name": "mcp-test",
"disk": []interface{}{"size=40g,image=img-1"},
"net": []interface{}{"net-1"},
"prefer-region": "reg-1",
"hypervisor": "aliyun",
"ncpu": 4,
"instance-type": "ecs.c7t.xlarge",
"mem-spec": "8G", // 有 sku 时不应再映射 mem
"dry-run": true,
})
if got["region"] != "reg-1" {
t.Fatalf("region=%v", got["region"])
}
if got["sku"] != "ecs.c7t.xlarge" {
t.Fatalf("sku=%v", got["sku"])
}
if _, ok := got["mem"]; ok {
t.Fatalf("mem should be omitted when sku present, got %v", got["mem"])
}
if _, ok := got["dry-run"]; ok {
t.Fatalf("dry-run must not be forwarded to forecast")
}
got = mapCreateArgsToForecastArgs(map[string]interface{}{
"ncpu": 2,
"mem-spec": "2048M",
"disk": []string{"size=30g,image=x"},
"net": []string{"n1"},
"region": "cn-beijing",
})
if got["mem"] != "2048" {
t.Fatalf("mem=%v want 2048", got["mem"])
}
if got["region"] != "cn-beijing" {
t.Fatalf("region=%v", got["region"])
}
}
func TestSchedulerForecastCommandRegistered(t *testing.T) {
cmd, ok := findCommand("scheduler-forecast")
if !ok {
t.Fatal("scheduler-forecast not in CommandTable; imports.go must blank-import shell/scheduler")
}
if cmd.Options == nil {
t.Fatal("scheduler-forecast Options is nil")
}
}
func TestPrepareForecastInputManagedCloud(t *testing.T) {
input := &schedapi.ScheduleInput{}
input.ServerConfigs = &compute.ServerConfigs{
Hypervisor: compute.HYPERVISOR_ALIYUN,
InstanceType: "ecs.c7t.xlarge",
Provider: compute.CLOUD_PROVIDER_ONECLOUD, // 错误默认
}
input.Ncpu = 2
input.Memory = 2048
prepareForecastInput(input)
if input.Provider != compute.CLOUD_PROVIDER_ALIYUN {
t.Fatalf("provider=%q want Aliyun", input.Provider)
}
if input.Ncpu != 0 || input.Memory != 0 {
t.Fatalf("managed+sku should clear default ncpu/mem, got ncpu=%d mem=%d", input.Ncpu, input.Memory)
}
}
func TestForecastSucceededByCandidates(t *testing.T) {
okForecast := jsonutils.Marshal(map[string]interface{}{
"can_create": true,
"req_count": 1,
"candidates": []map[string]interface{}{
{"host_id": "h1", "name": "qx-aliyun-cn-beijing-i", "error": ""},
},
})
if err := forecastSucceeded(okForecast); err != nil {
t.Fatalf("expected success: %v", err)
}
failForecast := jsonutils.Marshal(map[string]interface{}{
"can_create": false,
"req_count": 1,
"candidates": nil,
"not_allow_reasons": []string{"Out of resource"},
})
if err := forecastSucceeded(failForecast); err == nil {
t.Fatal("expected failure")
}
}
func TestPreferDiskBackend(t *testing.T) {
if got := preferDiskBackend([]string{"cloud", "cloud_essd", "cloud_ssd"}); got != "cloud_essd" {
t.Fatalf("prefer=%q", got)
}
capa := jsonutils.Marshal(map[string]interface{}{
"storage_types2": map[string][]string{
"aliyun": {"cloud_efficiency", "cloud_ssd"},
},
})
types := storageTypesFromCapability(capa, "aliyun")
if preferDiskBackend(types) != "cloud_ssd" {
t.Fatalf("types=%v prefer=%q", types, preferDiskBackend(types))
}
if diskHasBackend("size=30g,image=x") {
t.Fatal("should not have backend")
}
if !diskHasBackend("size=30g,image=x,backend=cloud_essd") {
t.Fatal("should have backend")
}
}
func TestEnsureNetworkAutoSched(t *testing.T) {
args := map[string]interface{}{"name": "t"}
ensureNetworkAutoSched(args)
parts := valueToArgvParts(args["net"])
if len(parts) != 1 || parts[0] != "random" {
t.Fatalf("expected net=[random], got %v", args["net"])
}
args = map[string]interface{}{"net": []interface{}{}}
ensureNetworkAutoSched(args)
parts = valueToArgvParts(args["net"])
if len(parts) != 1 || parts[0] != "random" {
t.Fatalf("empty net should become random, got %v", args["net"])
}
args = map[string]interface{}{"net": []string{"net-abc"}}
ensureNetworkAutoSched(args)
parts = valueToArgvParts(args["net"])
if len(parts) != 1 || parts[0] != "net-abc" {
t.Fatalf("explicit net must be kept, got %v", args["net"])
}
}
func TestServerCreateDescriptionMentionsForecast(t *testing.T) {
cmd, ok := findCommand("server-create")
if !ok {
t.Fatal("server-create not registered")
}
desc := buildDescription(cmd)
if !strings.Contains(desc, "scheduler-forecast") {
t.Fatalf("mcp-desc should mention scheduler-forecast, got %q", desc)
}
if strings.Contains(desc, "dry-run 预调度") {
t.Fatalf("mcp-desc must not claim dry-run is preschedule: %q", desc)
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package climcgen 从 climc 的 shell.CommandTable 与 Options struct tag
自动生成 MCP tool schema并调用对应 callback 执行。
注册范围:只注册 Options 上带 mcp-desc 的命令。
AI/MCP 参数通过 Options 字段上的 mcp tag 标记:
- mcp:"true" 暴露给 MCP schema
- mcp:"required" 暴露且在 MCP schema 中标记为 required
未标记的可选参数不会进入 schemapositional / climc required 仍会保留)。
MCP tool 补充说明通过 Options 上的 mcp-desc tag 写入(常用 `_ struct{}` 承载),
由 buildDescription 拼入 tool description。
*/
package climcgen

View File

@@ -0,0 +1,285 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"yunion.io/x/structarg"
"yunion.io/x/onecloud/cmd/climc/shell"
"yunion.io/x/onecloud/pkg/mcclient"
computeoptions "yunion.io/x/onecloud/pkg/mcclient/options/compute"
)
func mcpArgsToArgv(parser *structarg.ArgumentParser, args map[string]interface{}) []string {
argv := make([]string, 0)
normalize := func(k string) string {
return strings.ReplaceAll(k, "_", "-")
}
lookup := func(token string) (interface{}, bool) {
if v, ok := args[token]; ok {
return v, true
}
alt := strings.ReplaceAll(token, "-", "_")
if v, ok := args[alt]; ok {
return v, true
}
for k, v := range args {
if normalize(k) == token {
return v, true
}
}
return nil, false
}
for _, arg := range parser.GetPosArgs() {
v, ok := lookup(arg.Token())
if !ok {
continue
}
argv = append(argv, valueToArgvParts(v)...)
}
for _, arg := range parser.GetOptArgs() {
token := arg.Token()
if token == "help" {
continue
}
v, ok := lookup(token)
if !ok {
continue
}
if !arg.NeedData() {
if isTruthy(v) {
argv = append(argv, "--"+token)
} else if neg := arg.NegativeToken(); neg != "" && isFalsy(v) {
argv = append(argv, "--"+neg)
}
continue
}
if arg.IsMulti() {
for _, part := range valueToArgvParts(v) {
argv = append(argv, "--"+token, part)
}
continue
}
parts := valueToArgvParts(v)
if len(parts) == 0 {
continue
}
argv = append(argv, "--"+token, parts[0])
}
return argv
}
func valueToArgvParts(v interface{}) []string {
if v == nil {
return nil
}
switch x := v.(type) {
case string:
if x == "" {
return nil
}
return []string{x}
case bool:
return []string{strconv.FormatBool(x)}
case float64:
if x == float64(int64(x)) {
return []string{strconv.FormatInt(int64(x), 10)}
}
return []string{strconv.FormatFloat(x, 'f', -1, 64)}
case int:
return []string{strconv.Itoa(x)}
case int64:
return []string{strconv.FormatInt(x, 10)}
case []interface{}:
out := make([]string, 0, len(x))
for _, item := range x {
out = append(out, valueToArgvParts(item)...)
}
return out
case []string:
out := make([]string, 0, len(x))
for _, item := range x {
if item != "" {
out = append(out, item)
}
}
return out
default:
s := fmt.Sprint(x)
if s == "" || s == "<nil>" {
return nil
}
return []string{s}
}
}
func isTruthy(v interface{}) bool {
switch x := v.(type) {
case bool:
return x
case string:
s := strings.ToLower(strings.TrimSpace(x))
return s == "true" || s == "1" || s == "yes" || s == "on"
case float64:
return x != 0
case int:
return x != 0
default:
return false
}
}
func isFalsy(v interface{}) bool {
switch x := v.(type) {
case bool:
return !x
case string:
s := strings.ToLower(strings.TrimSpace(x))
return s == "false" || s == "0" || s == "no" || s == "off"
default:
return false
}
}
func invokeCommand(cmd shell.CMD, session *mcclient.ClientSession, args map[string]interface{}) (string, error) {
parser, _, err := newArgumentParser(cmd)
if err != nil {
return "", err
}
// list 查询默认 scope=max避免权限范围过窄查不到资源
if strings.HasSuffix(cmd.Command, "-list") {
if _, ok := args["scope"]; !ok {
if _, ok := args["Scope"]; !ok {
args["scope"] = "max"
}
}
}
// 创建虚拟机前查区域:默认 usable=true只返回网络可用的区域
if cmd.Command == "cloud-region-list" {
if _, ok := argLookup(args, "usable"); !ok {
args["usable"] = true
}
}
if cmd.Command == "server-sku-list" {
normalizeServerSkuListArgs(args)
}
argv := mcpArgsToArgv(parser, args)
if err := parser.ParseArgs(argv, false); err != nil {
return "", fmt.Errorf("parse args for %s: %w (argv=%v)", cmd.Command, err, argv)
}
filled := parser.Options()
cbVal := reflect.ValueOf(cmd.Callback)
if cbVal.Kind() != reflect.Func {
return "", fmt.Errorf("callback of %s is not a function", cmd.Command)
}
// 使用 goroutine 本地 writer避免劫持全局 os.Stdout / stdoutMu 串行化所有 tools/call。
var buf bytes.Buffer
restore := shell.PushOutput(&buf, shell.OUTPUT_FORMAT_JSON)
defer restore()
var callErr error
func() {
defer func() {
if rec := recover(); rec != nil {
callErr = fmt.Errorf("panic invoking %s: %v", cmd.Command, rec)
}
}()
outs := cbVal.Call([]reflect.Value{
reflect.ValueOf(session),
reflect.ValueOf(filled),
})
if len(outs) == 1 && !outs[0].IsNil() {
callErr = outs[0].Interface().(error)
}
}()
out := buf.String()
if callErr != nil {
return out, callErr
}
// 压缩 JSON降低模型上下文占用减少“只看完区域就不往下走”
if compact := compactJSON(out); compact != "" {
out = compact
}
return out, nil
}
func compactJSON(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
var v interface{}
if err := json.Unmarshal([]byte(s), &v); err != nil {
return ""
}
b, err := json.Marshal(v)
if err != nil {
return ""
}
return string(b)
}
// normalizeServerSkuListArgs 让 AI 传的 2c2g / cpu-core-count 等别名落到 climc 的 spec/cpu/mem。
func normalizeServerSkuListArgs(args map[string]interface{}) {
aliasCopy := func(from, to string) {
if _, ok := argLookup(args, to); ok {
return
}
if v, ok := argLookup(args, from); ok {
args[to] = v
}
}
aliasCopy("cpu-core-count", "cpu")
aliasCopy("cpu_core_count", "cpu")
aliasCopy("memory-size-mb", "mem")
aliasCopy("memory_size_mb", "mem")
if _, hasSpec := argLookup(args, "spec"); !hasSpec {
// search/name 里如果是口语规格,提升为 spec
for _, key := range []string{"search", "name"} {
v, ok := argLookup(args, key)
if !ok {
continue
}
s := strings.TrimSpace(firstString(v))
if s == "" {
continue
}
if _, _, err := computeoptions.ParseSkuSpec(s); err == nil {
args["spec"] = s
delete(args, key)
delete(args, strings.ReplaceAll(key, "-", "_"))
break
}
}
}
}

View File

@@ -0,0 +1,75 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"strings"
"yunion.io/x/log"
)
// ExtraToolsFunc 通过 RegisterExtraTools 注册,用于追加 MCP tools。
type ExtraToolsFunc func() []Tool
// ExtraInstructionsFunc 通过 RegisterExtraInstructions 注册,追加到 MCP ServerInstructions。
type ExtraInstructionsFunc func() string
var (
extraToolsFuncs []ExtraToolsFunc
extraInstructionsFuncs []ExtraInstructionsFunc
)
// RegisterExtraTools 注册额外工具构建回调。应在 init() 中调用StartService 前完成注册。
func RegisterExtraTools(fn ExtraToolsFunc) {
if fn == nil {
return
}
extraToolsFuncs = append(extraToolsFuncs, fn)
}
// RegisterExtraInstructions 注册额外全局说明回调。
func RegisterExtraInstructions(fn ExtraInstructionsFunc) {
if fn == nil {
return
}
extraInstructionsFuncs = append(extraInstructionsFuncs, fn)
}
// BuildExtraTools 执行已注册的 ExtraTools 回调,合并返回。
func BuildExtraTools() []Tool {
out := make([]Tool, 0)
for _, fn := range extraToolsFuncs {
tools := fn()
if len(tools) == 0 {
continue
}
out = append(out, tools...)
for _, t := range tools {
log.Infof("climcgen: registered extra MCP tool %s", t.GetName())
}
}
return out
}
// BuildExtraInstructions 合并已注册的额外说明。
func BuildExtraInstructions() string {
parts := make([]string, 0, len(extraInstructionsFuncs))
for _, fn := range extraInstructionsFuncs {
if s := strings.TrimSpace(fn()); s != "" {
parts = append(parts, s)
}
}
return strings.Join(parts, "\n\n")
}

View File

@@ -12,20 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
package climcgen
// 导入 climc shell 子包以填充 shell.CommandTable
import (
"context"
"github.com/mark3labs/mcp-go/mcp"
_ "yunion.io/x/onecloud/cmd/climc/shell/compute"
_ "yunion.io/x/onecloud/cmd/climc/shell/identity"
_ "yunion.io/x/onecloud/cmd/climc/shell/image"
_ "yunion.io/x/onecloud/cmd/climc/shell/logger"
_ "yunion.io/x/onecloud/cmd/climc/shell/monitor"
_ "yunion.io/x/onecloud/cmd/climc/shell/scheduler"
)
// Tool 是所有工具的接口,定义了工具的基本方法
// GetTool 返回 MCP 工具定义
// Handle 处理工具调用请求
// GetName 返回工具名称
type Tool interface {
GetTool() mcp.Tool
Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
GetName() string
}

View File

@@ -0,0 +1,56 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"fmt"
"strings"
"yunion.io/x/onecloud/pkg/mcp-server/options"
)
// serverInstructionsTemplate 返回给 MCP 客户端的全局使用说明模板,%s 为 PlatformName。
// 细则写在本说明;各工具 mcp-desc 保持一行摘要,避免重复占 token。
const serverInstructionsTemplate = `%s MCPclimc tools使用规则
认证initialize / tools/list 可匿名tools/call 必须带 HeaderX-Auth-Token或 AK+SK或 X-API-Key=base64(ak:sk)),不要在工具参数传密钥。
0. 严禁空口编造:未实际调用 climc_* 并拿到返回前,禁止声称“已查到/已创建成功”。
1. *-list 只用于准备参数,不等于任务完成;同一轮可并行多个查询。
2. 创建虚拟机(连续到 climc_server_create
a) climc_cloud_region_list公有云须 provider创建时 usable=true
b) climc_cloud_region_capability → 取 storage_types2 作 disk.backend
c) climc_image_listKVM或 climc_cached_image_list公有云须 provider+region=区域 id
d) climc_server_sku_list口语 2c2g 用 spec=\"2c2g\")或直接 ncpu/mem
e) climc_server_createname、disk 含 image+backend、规格公有云 hypervisor+prefer-region。net 可省略自动调度。工具会 forecast 预调度并等待 running/ready若返回 wait_pending用 climc_server_show 继续查,勿重复创建。
3. 启停/重启/删除/重置密码/改配/挂盘/绑 EIPclimc_server_list 定位 id 后立刻调用对应操作工具。
4. 监控指标用 climc_monitor_unifiedmonitor_queryclimc_server_monitor 是 QEMU HMP/QMP不是指标。
5. 缺参只追问真正缺失项;已有 id 直接下一步。
`
// BuildServerInstructions 用 platformNameBaseOptions.PlatformName生成 MCP ServerInstructions。
// platformName 为空时使用 options.ResolvedPlatformName()。
func BuildServerInstructions(platformName string) string {
name := strings.TrimSpace(platformName)
if name == "" {
name = options.ResolvedPlatformName()
}
return fmt.Sprintf(serverInstructionsTemplate, name)
}
// isCreateFlowListCommand创建流程中间查询工具Options mcp-desc 含该标记)。
func isCreateFlowListCommand(opt interface{}) bool {
return strings.Contains(collectMcpDesc(opt), "创建流程中的中间步骤")
}

View File

@@ -0,0 +1,139 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"reflect"
"strings"
"yunion.io/x/pkg/util/reflectutils"
"yunion.io/x/pkg/utils"
"yunion.io/x/structarg"
)
// TagMCP 与 climc Options 字段上的 mcp tag 对应。
// 用法:`mcp:"true"` 表示暴露给 AI/MCP`mcp:"required"` 表示 MCP 调用时建议必填。
const TagMCP = "mcp"
// TagMCPDesc 写在 Options 结构体任意字段上(常用 `_ struct{}`),作为 MCP tool 补充说明。
// 例:`_ struct{} \`mcp-desc:"创建虚拟机最终动作..."\“
const TagMCPDesc = "mcp-desc"
type mcpFieldMeta struct {
Required bool
}
// collectMcpDesc 读取 Options 上的 mcp-desc。
// 只看本结构体的非嵌入字段,不递归匿名嵌入,避免复用 Options如 statistics 嵌入 List被误注册。
func collectMcpDesc(optionsProto interface{}) string {
if optionsProto == nil {
return ""
}
return collectMcpDescFromType(reflect.TypeOf(optionsProto))
}
func collectMcpDescFromType(t reflect.Type) string {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return ""
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.Anonymous {
continue
}
if d := strings.TrimSpace(f.Tag.Get(TagMCPDesc)); d != "" {
return d
}
}
return ""
}
// collectMcpFields 从 Options 结构体读取 mcp tag返回 CLI token -> 元信息。
// token 计算方式与 structarg 一致token 标签或 json/字段名,再 CamelSplit 为 kebab-case
func collectMcpFields(optionsProto interface{}) map[string]mcpFieldMeta {
out := make(map[string]mcpFieldMeta)
if optionsProto == nil {
return out
}
v := reflect.ValueOf(optionsProto)
for v.Kind() == reflect.Ptr {
if v.IsNil() {
v = reflect.New(v.Type().Elem())
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return out
}
sets := reflectutils.FetchAllStructFieldValueSetForWrite(v)
for i := range sets {
info := sets[i].Info
if info == nil {
continue
}
tagMap := info.Tags
mcpVal, ok := tagMap[TagMCP]
if !ok || mcpVal == "" || mcpVal == "false" {
continue
}
// 即便 json:"-"Ignore也保留climc 仍可能用字段名作为 CLI token如 MemSpec -> mem-spec
token, tokOK := tagMap["token"]
if !tokOK {
if jsonName := tagMap["json"]; jsonName != "" && jsonName != "-" {
token = info.MarshalName()
} else if alias := tagMap["alias"]; alias != "" {
token = alias
} else {
// Ignore 字段的 info.Name 为空,必须用 FieldName
token = info.FieldName
}
}
if token == "" {
continue
}
cliToken := utils.CamelSplit(token, "-")
meta := mcpFieldMeta{Required: mcpVal == "required"}
out[cliToken] = meta
// 同时登记字段名 token兼容 structarg 对 json:"-" 使用字段名的行为
if fieldTok := utils.CamelSplit(info.FieldName, "-"); fieldTok != "" && fieldTok != cliToken {
out[fieldTok] = meta
}
// json 名与字段名不一致时(如 Region / prefer_region两边都登记
if jsonName := tagMap["json"]; jsonName != "" && jsonName != "-" {
if jsonTok := utils.CamelSplit(info.MarshalName(), "-"); jsonTok != "" && jsonTok != cliToken {
out[jsonTok] = meta
}
}
}
return out
}
// mcpKeepArg仅保留 positional/required以及带 mcp tag 的可选参数。
// 无 mcp tag 的可选参数一律不暴露,避免 schema 过大。
func mcpKeepArg(fields map[string]mcpFieldMeta, arg structarg.Argument) bool {
name := arg.Token()
if arg.IsRequired() || arg.IsPositional() {
return true
}
if name == "" || name == "help" {
return false
}
_, ok := fields[name]
return ok
}

View File

@@ -0,0 +1,94 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"fmt"
"sort"
"strings"
"yunion.io/x/log"
"yunion.io/x/onecloud/cmd/climc/shell"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
)
// BuildTools 扫描 shell.CommandTable注册 Options 上带 mcp-desc 的命令为 MCP tools。
func BuildTools(adapter *adapters.CloudpodsAdapter) ([]Tool, error) {
byName := make(map[string]shell.CMD, len(shell.CommandTable))
for _, cmd := range shell.CommandTable {
byName[cmd.Command] = cmd
}
commands := discoverMcpDescCommands()
result := make([]Tool, 0, len(commands))
for _, name := range commands {
cmd, ok := byName[name]
if !ok {
continue
}
tool, err := NewClimcTool(cmd, adapter)
if err != nil {
return nil, fmt.Errorf("build tool for %s: %w", name, err)
}
result = append(result, tool)
log.Infof("climcgen: registered MCP tool %s (%s)", tool.GetName(), name)
}
if len(result) == 0 {
return nil, fmt.Errorf("no climc tools registered; add mcp-desc on Options")
}
return result, nil
}
// discoverMcpDescCommands 扫描 CommandTable收集 Options 带 mcp-desc 的命令。
// server-create /「最终动作」优先排前,便于模型在创建意图下选中。
func discoverMcpDescCommands() []string {
type ranked struct {
name string
rank int
order int
}
found := make([]ranked, 0)
for i, cmd := range shell.CommandTable {
desc := collectMcpDesc(cmd.Options)
if desc == "" {
continue
}
r := ranked{name: cmd.Command, rank: 2, order: i}
switch {
case cmd.Command == "server-create" || strings.Contains(desc, "最终动作") || strings.Contains(desc, "优先调用"):
r.rank = 0
case strings.Contains(desc, "创建流程中的中间步骤"):
r.rank = 1
}
found = append(found, r)
}
sort.SliceStable(found, func(i, j int) bool {
if found[i].rank != found[j].rank {
return found[i].rank < found[j].rank
}
return found[i].order < found[j].order
})
out := make([]string, 0, len(found))
seen := make(map[string]bool, len(found))
for _, r := range found {
if seen[r.name] {
continue
}
seen[r.name] = true
out = append(out, r.name)
}
return out
}

View File

@@ -0,0 +1,73 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"testing"
"yunion.io/x/onecloud/cmd/climc/shell"
)
func TestDiscoverMcpDescCommands_UsesTagNotHardcodedList(t *testing.T) {
saved := shell.CommandTable
defer func() { shell.CommandTable = saved }()
type withDesc struct {
_ struct{} `mcp-desc:"【创建虚拟机的最终动作/优先调用】demo"`
Name string `mcp:"required"`
}
type noDesc struct {
Name string
}
type listDesc struct {
_ struct{} `mcp-desc:"【创建流程中的中间步骤】list demo"`
}
shell.CommandTable = []shell.CMD{
{Options: &noDesc{}, Command: "should-skip", Desc: "no mcp-desc"},
{Options: &listDesc{}, Command: "demo-list", Desc: "list"},
{Options: &withDesc{}, Command: "server-create", Desc: "create"},
}
got := discoverMcpDescCommands()
if len(got) != 2 {
t.Fatalf("want 2 commands, got %v", got)
}
if got[0] != "server-create" {
t.Fatalf("server-create should be first, got %v", got)
}
if got[1] != "demo-list" {
t.Fatalf("want demo-list second, got %v", got)
}
}
func TestCollectMcpDesc_DoesNotInheritFromEmbed(t *testing.T) {
type base struct {
_ struct{} `mcp-desc:"should not inherit"`
}
type wrapped struct {
base
}
if d := collectMcpDesc(&wrapped{}); d != "" {
t.Fatalf("embedded mcp-desc must not register, got %q", d)
}
type ownDesc struct {
_ struct{} `mcp-desc:"own"`
base
}
if d := collectMcpDesc(&ownDesc{}); d != "own" {
t.Fatalf("want own desc, got %q", d)
}
}

View File

@@ -0,0 +1,272 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"yunion.io/x/structarg"
"yunion.io/x/onecloud/cmd/climc/shell"
)
// maxSchemaProperties 限制生成的参数数量,避免 BaseListOptions 等过大 schema 淹没模型
const maxSchemaProperties = 48
func newOptionsInstance(cmd shell.CMD) (interface{}, error) {
if cmd.Options == nil {
return nil, fmt.Errorf("command %s has nil Options", cmd.Command)
}
t := reflect.TypeOf(cmd.Options)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("command %s Options is not a struct: %s", cmd.Command, t.Kind())
}
return reflect.New(t).Interface(), nil
}
func newArgumentParser(cmd shell.CMD) (*structarg.ArgumentParser, interface{}, error) {
optPtr, err := newOptionsInstance(cmd)
if err != nil {
return nil, nil, err
}
parser, err := structarg.NewArgumentParserWithHelp(optPtr, cmd.Command, cmd.Desc, "")
if err != nil {
return nil, nil, fmt.Errorf("build argument parser for %s: %w", cmd.Command, err)
}
return parser, optPtr, nil
}
func buildInputSchema(cmd shell.CMD) (json.RawMessage, error) {
parser, optPtr, err := newArgumentParser(cmd)
if err != nil {
return nil, err
}
mcpFields := collectMcpFields(optPtr)
properties := map[string]interface{}{}
required := make([]string, 0)
requiredSet := map[string]bool{}
addRequired := func(name string) {
if name == "" || requiredSet[name] {
return
}
requiredSet[name] = true
required = append(required, name)
}
addArg := func(arg structarg.Argument) {
name := arg.Token()
if name == "" || name == "help" {
return
}
if _, exists := properties[name]; exists {
return
}
prop := map[string]interface{}{
"description": strings.TrimSpace(arg.HelpString("")),
}
switch {
case !arg.NeedData():
prop["type"] = "boolean"
case arg.IsMulti():
prop["type"] = "array"
items := map[string]interface{}{"type": "string"}
if choices := argChoices(arg); len(choices) > 0 {
items["enum"] = choices
}
prop["items"] = items
default:
prop["type"] = "string"
}
if choices := argChoices(arg); len(choices) > 0 && !arg.IsMulti() {
prop["enum"] = choices
}
if arg.IsPositional() {
meta := arg.MetaVar()
if meta != "" {
desc, _ := prop["description"].(string)
if desc != "" {
prop["description"] = fmt.Sprintf("%s (positional: %s)", desc, meta)
} else {
prop["description"] = fmt.Sprintf("positional argument %s", meta)
}
}
}
properties[name] = prop
if arg.IsRequired() || arg.IsPositional() {
addRequired(name)
}
if meta, ok := mcpFields[name]; ok && meta.Required {
addRequired(name)
}
}
for _, arg := range parser.GetPosArgs() {
addArg(arg)
}
for _, arg := range parser.GetOptArgs() {
if arg.IsRequired() {
addArg(arg)
}
}
isList := strings.HasSuffix(cmd.Command, "-list")
isCreate := cmd.Command == "server-create"
for _, arg := range parser.GetOptArgs() {
if arg.IsRequired() || arg.Token() == "help" {
continue
}
if len(properties) >= maxSchemaProperties {
break
}
if mcpKeepArg(mcpFields, arg) {
addArg(arg)
}
}
// 强化 create 关键字段说明,降低调用门槛
if isCreate {
if prop, ok := properties["disk"].(map[string]interface{}); ok {
prop["description"] = "系统盘描述,数组。公有云示例:[\"size=40g,image=<镜像ID>,backend=cloud_essd\"]backend 必须来自 climc_cloud_region_capability 的 storage_types2ISO 请用 cdrom 而不是 disk.image"
properties["disk"] = prop
}
if prop, ok := properties["net"].(map[string]interface{}); ok {
prop["description"] = "网络描述,可省略。省略或 [] 时自动 random等价 API nets:[{exit:false}])由调度器选网;指定时示例:[\"<网络ID>\"]"
properties["net"] = prop
}
if prop, ok := properties["cdrom"].(map[string]interface{}); ok {
prop["description"] = "ISO/光驱镜像 ID系统盘不要挂 ISO 镜像"
properties["cdrom"] = prop
}
}
// list 命令确保 scope 出现在 schema 中,并强化 provider 说明
if isList {
if prop, ok := properties["scope"].(map[string]interface{}); ok {
prop["description"] = "resource scope省略时 MCP 默认注入 max"
properties["scope"] = prop
} else {
properties["scope"] = map[string]interface{}{
"type": "string",
"description": "resource scope省略时 MCP 默认注入 max",
"enum": []string{"system", "domain", "project", "user", "max"},
}
}
if prop, ok := properties["provider"].(map[string]interface{}); ok {
prop["description"] = "云厂商过滤;用户指定阿里云/AWS 等公有云时必须传,例如 [\"Aliyun\"],不要省略"
properties["provider"] = prop
}
}
if cmd.Command == "cloud-region-list" {
if prop, ok := properties["usable"].(map[string]interface{}); ok {
prop["description"] = "创建虚拟机时必须为 true只返回网络可用区域省略时 MCP 默认注入 true"
properties["usable"] = prop
} else {
properties["usable"] = map[string]interface{}{
"type": "boolean",
"description": "创建虚拟机时必须为 true只返回网络可用区域省略时 MCP 默认注入 true",
}
}
}
if cmd.Command == "cached-image-list" {
if prop, ok := properties["provider"].(map[string]interface{}); ok {
prop["description"] = "公有云选镜像必传,例如 [\"Aliyun\"];不传会混入其他云(如 AWS镜像"
properties["provider"] = prop
}
if prop, ok := properties["region"].(map[string]interface{}); ok {
prop["description"] = "必须传 climc_cloud_region_list 返回的 idUUID。禁止传 cn-shanghai / Aliyun/cn-shanghai 这类外部 region code"
properties["region"] = prop
}
if prop, ok := properties["image-type"].(map[string]interface{}); ok {
prop["description"] = "镜像类型;创建系统盘优先 system避免误用 ISO/驱动盘"
properties["image-type"] = prop
}
}
if cmd.Command == "server-sku-list" {
if prop, ok := properties["spec"].(map[string]interface{}); ok {
prop["description"] = "口语规格优先使用。例2c2g、2核2G、4C8G自动转为 cpu + mem(MB)。用户说「2核2G」就传 spec=\"2c2g\""
properties["spec"] = prop
} else {
properties["spec"] = map[string]interface{}{
"type": "string",
"description": "口语规格优先使用。例2c2g、2核2G、4C8G自动转为 cpu + mem(MB)",
}
}
if prop, ok := properties["cpu"].(map[string]interface{}); ok {
prop["description"] = "CPU 核数;与 mem 联用。有口语规格时优先用 spec"
properties["cpu"] = prop
}
if prop, ok := properties["mem"].(map[string]interface{}); ok {
prop["description"] = "内存 MB2G=2048。有口语规格时优先用 spec"
properties["mem"] = prop
}
if prop, ok := properties["postpaid-status"].(map[string]interface{}); ok {
prop["description"] = "按量付费状态;创建时优先 available避免选到 soldout"
properties["postpaid-status"] = prop
}
}
// 认证:请使用连接 HeaderX-Auth-Token / AK+SK / X-API-Key不要在工具参数里传密钥。
schema := map[string]interface{}{
"type": "object",
"properties": properties,
}
if len(required) > 0 {
schema["required"] = required
}
raw, err := json.Marshal(schema)
if err != nil {
return nil, err
}
return raw, nil
}
func argChoices(arg structarg.Argument) []string {
type chooser interface {
Choices() []string
}
if c, ok := arg.(chooser); ok {
return c.Choices()
}
return nil
}
func toolNameFromCommand(command string) string {
return "climc_" + strings.ReplaceAll(command, "-", "_")
}
func buildDescription(cmd shell.CMD) string {
desc := strings.TrimSpace(cmd.Desc)
if desc == "" {
desc = fmt.Sprintf("Execute climc command %s", cmd.Command)
}
desc = fmt.Sprintf("[climc %s] %s", cmd.Command, desc)
if mcp := collectMcpDesc(cmd.Options); mcp != "" {
desc += "。" + mcp
}
return desc
}

View File

@@ -0,0 +1,82 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"encoding/json"
"strings"
"testing"
_ "yunion.io/x/onecloud/cmd/climc/shell/compute"
_ "yunion.io/x/onecloud/cmd/climc/shell/image"
"yunion.io/x/onecloud/cmd/climc/shell"
)
func TestBuildInputSchemaUsesMcpTags(t *testing.T) {
var cmd shell.CMD
found := false
for _, c := range shell.CommandTable {
if c.Command == "server-create" {
cmd = c
found = true
break
}
}
if !found {
t.Fatal("server-create not registered")
}
raw, err := buildInputSchema(cmd)
if err != nil {
t.Fatalf("buildInputSchema: %v", err)
}
var schema map[string]interface{}
if err := json.Unmarshal(raw, &schema); err != nil {
t.Fatal(err)
}
props := schema["properties"].(map[string]interface{})
for _, key := range []string{"name", "net", "disk", "ncpu", "mem-spec", "prefer-region", "hypervisor", "cdrom"} {
if _, ok := props[key]; !ok {
t.Errorf("expected mcp-tagged property %q in schema, got keys=%v", key, propKeys(props))
}
}
// kickstart 等未打 mcp tag 的字段不应出现
for _, key := range []string{"kickstart-os-type", "user-data-file", "fake-create"} {
if _, ok := props[key]; ok {
t.Errorf("unexpected untagged property %q in schema", key)
}
}
req, _ := schema["required"].([]interface{})
reqSet := map[string]bool{}
for _, r := range req {
reqSet[r.(string)] = true
}
if !reqSet["name"] || reqSet["net"] {
t.Errorf("expected name required and net optional, got %v", req)
}
desc := buildDescription(cmd)
if !strings.Contains(desc, "创建虚拟机的最终动作") {
t.Errorf("expected mcp-desc in tool description, got %q", desc)
}
}
func propKeys(props map[string]interface{}) []string {
keys := make([]string, 0, len(props))
for k := range props {
keys = append(keys, k)
}
return keys
}

View File

@@ -0,0 +1,155 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package climcgen
import (
"context"
"fmt"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/cmd/climc/shell"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
)
// Tool MCP 工具接口
type Tool interface {
GetTool() mcp.Tool
Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
GetName() string
}
// ClimcTool 由 climc CommandTable 自动生成的 MCP 工具
type ClimcTool struct {
cmd shell.CMD
name string
tool mcp.Tool
adapter *adapters.CloudpodsAdapter
}
func NewClimcTool(cmd shell.CMD, adapter *adapters.CloudpodsAdapter) (*ClimcTool, error) {
schema, err := buildInputSchema(cmd)
if err != nil {
return nil, err
}
name := toolNameFromCommand(cmd.Command)
desc := buildDescription(cmd)
tool := mcp.NewToolWithRawSchema(name, desc, schema)
return &ClimcTool{
cmd: cmd,
name: name,
tool: tool,
adapter: adapter,
}, nil
}
func (t *ClimcTool) GetTool() mcp.Tool {
return t.tool
}
func (t *ClimcTool) GetName() string {
return t.name
}
func (t *ClimcTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := map[string]interface{}{}
if req.Params.Arguments != nil {
switch a := req.Params.Arguments.(type) {
case map[string]interface{}:
args = a
default:
return nil, fmt.Errorf("unexpected arguments type %T", req.Params.Arguments)
}
}
ak, _ := args["ak"].(string)
sk, _ := args["sk"].(string)
delete(args, "ak")
delete(args, "sk")
if !adapters.HasRequestCredentials(ctx) && (ak == "" || sk == "") {
return mcp.NewToolResultError(adapters.ErrAuthenticationRequired.Error()), nil
}
session, err := t.adapter.GetSession(ctx, ak, sk)
if err != nil {
log.Errorf("climc tool %s get session failed: %s", t.name, err)
return mcp.NewToolResultError(fmt.Sprintf("authentication/session failed: %s", err.Error())), nil
}
var output string
if t.cmd.Command == "server-create" {
output, err = t.handleServerCreate(ctx, session, args)
} else {
if err := ctx.Err(); err != nil {
return nil, err
}
output, err = invokeCommand(t.cmd, session, args)
}
if err != nil {
log.Errorf("climc tool %s failed: %s", t.name, err)
if output != "" {
return mcp.NewToolResultError(fmt.Sprintf("%s\noutput:\n%s", err.Error(), output)), nil
}
return nil, fmt.Errorf("execute %s: %w", t.cmd.Command, err)
}
if strings.TrimSpace(output) == "" {
output = fmt.Sprintf(`{"command":%q,"success":true}`, t.cmd.Command)
}
if hint := nextStepHint(t.cmd.Command, t.cmd.Options); hint != "" {
output = output + "\n\n" + hint
}
return mcp.NewToolResultText(output), nil
}
func nextStepHint(command string, options interface{}) string {
switch {
case command == "cloud-region-list":
return `[MCP下一步] 创建虚拟机尚未完成。请用区域 id 调 climc_cloud_region_capability再 climc_cached_image_list + climc_server_sku_list可并行最后 climc_server_create。net 可省略走自动调度,默认不要 network-list。不要在此处结束。`
case command == "cloud-region-capability":
return `[MCP下一步] 记下 storage_types2 中的系统盘类型(如 cloud_essd。继续 climc_cached_image_list 与 climc_server_sku_list然后 climc_server_createdisk 带 backendnet 可省略)。`
case command == "cached-image-list":
return `[MCP下一步] 确认已带 provider 及 region=区域id。继续 climc_server_sku_list若未查然后立刻 climc_server_createdisk 含 image+backendnet 可省略);禁止用 ISO不要重复查同一镜像列表。`
case command == "image-list":
return `[MCP下一步] KVM 用本结果;公有云请改用 climc_cached_image_list。继续 sku 后立刻 climc_server_create。`
case command == "network-list" || command == "vpc-list":
return `[MCP下一步] 若用户未指定网络,可省略 net直接 climc_server_create 走自动调度;若已选中网络 idcreate 时传入 net=["<id>"]。`
case command == "server-sku-list" || command == "storage-list":
return `[MCP下一步] 请立刻调用 climc_server_createdisk 须含 backendnet 可省略(自动 random / nets:[{exit:false}])。`
case command == "server-list":
return `[MCP下一步] 若用户要启动/停止/重启/删除/改配/重置密码/挂盘/绑定 EIP拿到 id 后立刻调用对应操作工具,不要只查询就结束。`
case command == "cloud-account-list":
return `[MCP下一步] 查详情用 climc_cloud_account_show同步资源用 climc_cloud_account_sync可 force=true`
case command == "cloud-account-sync":
return `[MCP下一步] 同步为异步任务,可用 climc_cloud_account_show 查看 sync_status。`
case command == "eip-list":
return `[MCP下一步] 绑定到虚机用 climc_server_associate_eip需 server id 与 eip id详情用 climc_eip_show。`
case command == "disk-list":
return `[MCP下一步] 挂到虚机用 climc_server_attach_disk扩容 climc_disk_resize详情 climc_disk_show。`
case command == "host-list":
return `[MCP下一步] 查宿主机详情用 climc_host_show。`
case command == "docs_search":
return `[MCP下一步] 对最相关 path 调用 docs_get 阅读正文后再回答用户。`
case command == "action-show":
return `[MCP下一步] 这是操作审计日志。若要继续改资源,回到对应 climc_* 操作工具。`
case isCreateFlowListCommand(options):
return `[MCP下一步] 这只是创建前的资源查询。任务完成条件是成功调用 climc_server_create。`
default:
return ""
}
}

View File

@@ -1 +0,0 @@
package models // import "yunion.io/x/onecloud/pkg/mcp-server/models"

View File

@@ -1,774 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import "time"
type ListRegionsReq struct {
}
type CloudregionDetails struct {
CanDelete bool `json:"can_delete"`
CanUpdate bool `json:"can_update"`
City string `json:"city"`
CloudEnv string `json:"cloud_env"`
CountryCode string `json:"country_code"`
CreatedAt *time.Time `json:"created_at"`
Deleted bool `json:"deleted"`
DeletedAt *time.Time `json:"deleted_at"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
Environment string `json:"environment"`
ExternalId string `json:"external_id"`
GuestCount int64 `json:"guest_count"`
GuestIncrementCount int64 `json:"guest_increment_count"`
Id string `json:"id"`
ImportedAt *time.Time `json:"imported_at"`
IsEmulated bool `json:"is_emulated"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Metadata map[string]string `json:"metadata"`
Name string `json:"name"`
NetworkCount int64 `json:"network_count"`
Progress float64 `json:"progress"`
Provider string `json:"provider"`
Source string `json:"source"`
Status string `json:"status"`
UpdateVersion int64 `json:"update_version"`
UpdatedAt *time.Time `json:"updated_at"`
VpcCount int64 `json:"vpc_count"`
ZoneCount int64 `json:"zone_count"`
}
type CloudregionListResponse struct {
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Cloudregions []CloudregionDetails `json:"cloudregions"`
Total int64 `json:"total"`
}
type SharedDomain struct {
Id string `json:"id"`
Name string `json:"name"`
}
type SharedProject struct {
Domain string `json:"domain"`
DomainId string `json:"domain_id"`
Id string `json:"id"`
Name string `json:"name"`
}
type VpcDetails struct {
Account string `json:"account"`
AccountHealthStatus string `json:"account_health_status"`
AccountId string `json:"account_id"`
AccountReadOnly bool `json:"account_read_only"`
AccountStatus string `json:"account_status"`
AcceptVpcPeerCount int64 `json:"accpet_vpc_peer_count"`
Brand string `json:"brand"`
CanDelete bool `json:"can_delete"`
CanUpdate bool `json:"can_update"`
CidrBlock string `json:"cidr_block"`
CidrBlock6 string `json:"cidr_block6"`
CloudEnv string `json:"cloud_env"`
Cloudregion string `json:"cloudregion"`
CloudregionId string `json:"cloudregion_id"`
CreatedAt *time.Time `json:"created_at"`
Deleted bool `json:"deleted"`
DeletedAt *time.Time `json:"deleted_at"`
Description string `json:"description"`
Direct bool `json:"direct"`
DnsZoneCount int64 `json:"dns_zone_count"`
DomainId string `json:"domain_id"`
DomainSrc string `json:"domain_src"`
Enabled bool `json:"enabled"`
Environment string `json:"environment"`
ExternalAccessMode string `json:"external_access_mode"`
ExternalId string `json:"external_id"`
Globalvpc string `json:"globalvpc"`
GlobalvpcId string `json:"globalvpc_id"`
Id string `json:"id"`
ImportedAt *time.Time `json:"imported_at"`
IsDefault bool `json:"is_default"`
IsEmulated bool `json:"is_emulated"`
IsPublic bool `json:"is_public"`
Manager string `json:"manager"`
ManagerDomain string `json:"manager_domain"`
ManagerDomainId string `json:"manager_domain_id"`
ManagerId string `json:"manager_id"`
ManagerProject string `json:"manager_project"`
ManagerProjectId string `json:"manager_project_id"`
Metadata map[string]string `json:"metadata"`
Name string `json:"name"`
NatgatewayCount int64 `json:"natgateway_count"`
NetworkCount int64 `json:"network_count"`
Progress float64 `json:"progress"`
ProjectDomain string `json:"project_domain"`
Provider string `json:"provider"`
PublicScope string `json:"public_scope"`
PublicSrc string `json:"public_src"`
Region string `json:"region"`
RegionExtId string `json:"region_ext_id"`
RegionExternalId string `json:"region_external_id"`
RegionId string `json:"region_id"`
RequestVpcPeerCount int64 `json:"request_vpc_peer_count"`
RoutetableCount int64 `json:"routetable_count"`
SharedDomains []SharedDomain `json:"shared_domains"`
SharedProjects []SharedProject `json:"shared_projects"`
Source string `json:"source"`
Status string `json:"status"`
UpdateVersion int64 `json:"update_version"`
UpdatedAt *time.Time `json:"updated_at"`
WireCount int64 `json:"wire_count"`
}
type VpcListResponse struct {
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Vpcs []VpcDetails `json:"vpcs"`
Total int64 `json:"total"`
}
type SchedtagShortDescDetails struct {
Default string `json:"default"`
Id string `json:"id"`
Name string `json:"name"`
ResName string `json:"res_name"`
}
type SRoute []string
type SSimpleWire struct {
Wire string `json:"Wire"`
WireId string `json:"WireId"`
}
type NetworkDetails struct {
Account string `json:"account"`
AccountHealthStatus string `json:"account_health_status"`
AccountId string `json:"account_id"`
AccountReadOnly bool `json:"account_read_only"`
AccountStatus string `json:"account_status"`
AdditionalWires []SSimpleWire `json:"additional_wires"`
AllocPolicy string `json:"alloc_policy"`
AllocTimoutSeconds int64 `json:"alloc_timout_seconds"`
BgpType string `json:"bgp_type"`
BmReusedVnics int64 `json:"bm_reused_vnics"`
BmVnics int64 `json:"bm_vnics"`
Brand string `json:"brand"`
CanDelete bool `json:"can_delete"`
CanUpdate bool `json:"can_update"`
CloudEnv string `json:"cloud_env"`
Cloudregion string `json:"cloudregion"`
CloudregionId string `json:"cloudregion_id"`
CreatedAt *time.Time `json:"created_at"`
Deleted bool `json:"deleted"`
DeletedAt *time.Time `json:"deleted_at"`
Description string `json:"description"`
Dns string `json:"dns"`
DomainId string `json:"domain_id"`
EipVnics int64 `json:"eip_vnics"`
Environment string `json:"environment"`
Exit bool `json:"exit"`
ExternalId string `json:"external_id"`
Freezed bool `json:"freezed"`
GroupVnics int64 `json:"group_vnics"`
GuestDhcp string `json:"guest_dhcp"`
GuestDns string `json:"guest_dns"`
GuestDns6 string `json:"guest_dns6"`
GuestDomain string `json:"guest_domain"`
GuestDomain6 string `json:"guest_domain6"`
GuestGateway string `json:"guest_gateway"`
GuestGateway6 string `json:"guest_gateway6"`
GuestIpEnd string `json:"guest_ip_end"`
GuestIpMask uint8 `json:"guest_ip_mask"`
GuestIpStart string `json:"guest_ip_start"`
GuestIp6End string `json:"guest_ip6_end"`
GuestIp6Mask uint8 `json:"guest_ip6_mask"`
GuestIp6Start string `json:"guest_ip6_start"`
GuestNtp string `json:"guest_ntp"`
Id string `json:"id"`
IfnameHint string `json:"ifname_hint"`
ImportedAt *time.Time `json:"imported_at"`
IsAutoAlloc bool `json:"is_auto_alloc"`
IsClassic bool `json:"is_classic"`
IsDefaultVpc bool `json:"is_default_vpc"`
IsEmulated bool `json:"is_emulated"`
IsPublic bool `json:"is_public"`
IsSystem bool `json:"is_system"`
LbVnics int64 `json:"lb_vnics"`
Manager string `json:"manager"`
ManagerDomain string `json:"manager_domain"`
ManagerDomainId string `json:"manager_domain_id"`
ManagerId string `json:"manager_id"`
ManagerProject string `json:"manager_project"`
ManagerProjectId string `json:"manager_project_id"`
Metadata map[string]string `json:"metadata"`
Name string `json:"name"`
NatVnics int64 `json:"nat_vnics"`
NetworkinterfaceVnics int64 `json:"networkinterface_vnics"`
PendingDeleted bool `json:"pending_deleted"`
PendingDeletedAt *time.Time `json:"pending_deleted_at"`
Ports int64 `json:"ports"`
PortsUsed int64 `json:"ports_used"`
Ports6Used int64 `json:"ports6_used"`
Progress float64 `json:"progress"`
Project string `json:"project"`
ProjectDomain string `json:"project_domain"`
ProjectId string `json:"project_id"`
ProjectMetadata map[string]string `json:"project_metadata"`
ProjectSrc string `json:"project_src"`
Provider string `json:"provider"`
PublicScope string `json:"public_scope"`
PublicSrc string `json:"public_src"`
RdsVnics int64 `json:"rds_vnics"`
Region string `json:"region"`
RegionExtId string `json:"region_ext_id"`
RegionExternalId string `json:"region_external_id"`
RegionId string `json:"region_id"`
ReserveVnics4 int64 `json:"reserve_vnics4"`
ReserveVnics6 int64 `json:"reserve_vnics6"`
Routes []SRoute `json:"routes"`
Schedtags []SchedtagShortDescDetails `json:"schedtags"`
ServerType string `json:"server_type"`
SharedDomains []SharedDomain `json:"shared_domains"`
SharedProjects []SharedProject `json:"shared_projects"`
Source string `json:"source"`
Status string `json:"status"`
Tenant string `json:"tenant"`
TenantId string `json:"tenant_id"`
Total int64 `json:"total"`
Total6 int64 `json:"total6"`
UpdateVersion int64 `json:"update_version"`
UpdatedAt *time.Time `json:"updated_at"`
VlanId int64 `json:"vlan_id"`
Vnics int64 `json:"vnics"`
Vnics4 int64 `json:"vnics4"`
Vnics6 int64 `json:"vnics6"`
Vpc string `json:"vpc"`
VpcExtId string `json:"vpc_ext_id"`
VpcId string `json:"vpc_id"`
Wire string `json:"wire"`
WireId string `json:"wire_id"`
Zone string `json:"zone"`
ZoneId string `json:"zone_id"`
}
type NetworkListResponse struct {
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Networks []NetworkDetails `json:"networks"`
Total int64 `json:"total"`
}
type ImageDetails struct {
AutoDeleteAt *time.Time `json:"auto_delete_at"`
CanDelete bool `json:"can_delete"`
CanUpdate bool `json:"can_update"`
Checksum string `json:"checksum"`
CreatedAt *time.Time `json:"created_at"`
Deleted bool `json:"deleted"`
DeletedAt *time.Time `json:"deleted_at"`
Description string `json:"description"`
DisableDelete bool `json:"disable_delete"`
DiskFormat string `json:"disk_format"`
DomainId string `json:"domain_id"`
EncryptAlg string `json:"encrypt_alg"`
EncryptKey string `json:"encrypt_key"`
EncryptKeyId string `json:"encrypt_key_id"`
EncryptKeyUser string `json:"encrypt_key_user"`
EncryptKeyUserDomain string `json:"encrypt_key_user_domain"`
EncryptKeyUserDomainId string `json:"encrypt_key_user_domain_id"`
EncryptKeyUserId string `json:"encrypt_key_user_id"`
EncryptStatus string `json:"encrypt_status"`
FastHash string `json:"fast_hash"`
Freezed bool `json:"freezed"`
Id string `json:"id"`
IsData bool `json:"is_data"`
IsEmulated bool `json:"is_emulated"`
IsGuestImage bool `json:"is_guest_image"`
IsPublic bool `json:"is_public"`
IsStandard bool `json:"is_standard"`
IsSystem bool `json:"is_system"`
Location string `json:"location"`
Metadata map[string]string `json:"metadata"`
MinDisk int32 `json:"min_disk"`
MinRam int32 `json:"min_ram"`
Name string `json:"name"`
OsArch string `json:"os_arch"`
OssChecksum string `json:"oss_checksum"`
Owner string `json:"owner"`
PendingDeleted bool `json:"pending_deleted"`
PendingDeletedAt *time.Time `json:"pending_deleted_at"`
Progress float64 `json:"progress"`
Project string `json:"project"`
ProjectDomain string `json:"project_domain"`
ProjectId string `json:"project_id"`
ProjectMetadata map[string]string `json:"project_metadata"`
ProjectSrc string `json:"project_src"`
Properties map[string]string `json:"properties"`
Protected bool `json:"protected"`
PublicScope string `json:"public_scope"`
PublicSrc string `json:"public_src"`
SharedDomains []SharedDomain `json:"shared_domains"`
SharedProjects []SharedProject `json:"shared_projects"`
Size int64 `json:"size"`
Status string `json:"status"`
Tenant string `json:"tenant"`
TenantId string `json:"tenant_id"`
UpdateVersion int64 `json:"update_version"`
UpdatedAt *time.Time `json:"updated_at"`
}
type ImageListResponse struct {
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Images []ImageDetails `json:"images"`
Total int64 `json:"total"`
}
type ServerSkuDetails struct {
AttachedDiskCount int64 `json:"attached_disk_count"`
AttachedDiskSizeGB int64 `json:"attached_disk_size_gb"`
AttachedDiskType string `json:"attached_disk_type"`
CanDelete bool `json:"can_delete"`
CanUpdate bool `json:"can_update"`
CloudEnv string `json:"cloud_env"`
Cloudregion string `json:"cloudregion"`
CloudregionId string `json:"cloudregion_id"`
CpuArch string `json:"cpu_arch"`
CpuCoreCount int64 `json:"cpu_core_count"`
CreatedAt *time.Time `json:"created_at"`
DataDiskMaxCount int64 `json:"data_disk_max_count"`
DataDiskTypes string `json:"data_disk_types"`
Deleted bool `json:"deleted"`
DeletedAt *time.Time `json:"deleted_at"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
ExternalId string `json:"external_id"`
GpuAttachable bool `json:"gpu_attachable"`
GpuCount string `json:"gpu_count"`
GpuMaxCount int64 `json:"gpu_max_count"`
GpuSpec string `json:"gpu_spec"`
Id string `json:"id"`
ImportedAt *time.Time `json:"imported_at"`
InstanceTypeCategory string `json:"instance_type_category"`
InstanceTypeFamily string `json:"instance_type_family"`
IsEmulated bool `json:"is_emulated"`
LocalCategory string `json:"local_category"`
Md5 string `json:"md5"`
MemorySizeMB int64 `json:"memory_size_mb"`
Metadata map[string]string `json:"metadata"`
Name string `json:"name"`
NicMaxCount int64 `json:"nic_max_count"`
NicType string `json:"nic_type"`
OsName string `json:"os_name"`
PostpaidStatus string `json:"postpaid_status"`
PrepaidStatus string `json:"prepaid_status"`
Progress float64 `json:"progress"`
Provider string `json:"provider"`
Region string `json:"region"`
RegionExtId string `json:"region_ext_id"`
RegionExternalId string `json:"region_external_id"`
RegionId string `json:"region_id"`
Source string `json:"source"`
Status string `json:"status"`
SysDiskMaxSizeGB int64 `json:"sys_disk_max_size_gb"`
SysDiskMinSizeGB int64 `json:"sys_disk_min_size_gb"`
SysDiskResizable bool `json:"sys_disk_resizable"`
SysDiskType string `json:"sys_disk_type"`
TotalGuestCount int64 `json:"total_guest_count"`
UpdateVersion int64 `json:"update_version"`
UpdatedAt *time.Time `json:"updated_at"`
Zone string `json:"zone"`
ZoneExtId string `json:"zone_ext_id"`
ZoneId string `json:"zone_id"`
}
type ServerSkuListResponse struct {
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Serverskus []ServerSkuDetails `json:"serverskus"`
Total int64 `json:"total"`
}
type StorageHost struct {
HostStatus string `json:"HostStatus"`
Id string `json:"Id"`
Name string `json:"Name"`
Status string `json:"Status"`
}
type StorageDetails struct {
DiskCount int64 `json:"DiskCount"`
HostCount int64 `json:"HostCount"`
SnapshotCount int64 `json:"SnapshotCount"`
Used int64 `json:"Used"`
Wasted int64 `json:"Wasted"`
Account string `json:"account"`
AccountHealthStatus string `json:"account_health_status"`
AccountId string `json:"account_id"`
AccountReadOnly bool `json:"account_read_only"`
AccountStatus string `json:"account_status"`
ActualCapacityUsed int64 `json:"actual_capacity_used"`
Brand string `json:"brand"`
CanDelete bool `json:"can_delete"`
CanUpdate bool `json:"can_update"`
Capacity int64 `json:"capacity"`
CloudEnv string `json:"cloud_env"`
Cloudregion string `json:"cloudregion"`
CloudregionId string `json:"cloudregion_id"`
Cmtbound float64 `json:"cmtbound"`
CommitBound float64 `json:"commit_bound"`
CommitRate float64 `json:"commit_rate"`
CreatedAt *time.Time `json:"created_at"`
Deleted bool `json:"deleted"`
DeletedAt *time.Time `json:"deleted_at"`
Description string `json:"description"`
DomainId string `json:"domain_id"`
DomainSrc string `json:"domain_src"`
Enabled bool `json:"enabled"`
Environment string `json:"environment"`
ExternalId string `json:"external_id"`
FreeCapacity int64 `json:"free_capacity"`
Hosts []StorageHost `json:"hosts"`
Id string `json:"id"`
ImportedAt *time.Time `json:"imported_at"`
IsEmulated bool `json:"is_emulated"`
IsPublic bool `json:"is_public"`
IsSysDiskStore bool `json:"is_sys_disk_store"`
Manager string `json:"manager"`
ManagerDomain string `json:"manager_domain"`
ManagerDomainId string `json:"manager_domain_id"`
ManagerId string `json:"manager_id"`
ManagerProject string `json:"manager_project"`
ManagerProjectId string `json:"manager_project_id"`
MasterHost string `json:"master_host"`
MasterHostName string `json:"master_host_name"`
MediumType string `json:"medium_type"`
Metadata map[string]string `json:"metadata"`
Name string `json:"name"`
Progress float64 `json:"progress"`
ProjectDomain string `json:"project_domain"`
Provider string `json:"provider"`
PublicScope string `json:"public_scope"`
PublicSrc string `json:"public_src"`
RealTimeUsedCapacity int64 `json:"real_time_used_capacity"`
Region string `json:"region"`
RegionExtId string `json:"region_ext_id"`
RegionExternalId string `json:"region_external_id"`
RegionId string `json:"region_id"`
Reserved int64 `json:"reserved"`
Schedtags []SchedtagShortDescDetails `json:"schedtags"`
SharedDomains []SharedDomain `json:"shared_domains"`
SharedProjects []SharedProject `json:"shared_projects"`
Source string `json:"source"`
Status string `json:"status"`
StorageConf map[string]interface{} `json:"storage_conf"`
StorageType string `json:"storage_type"`
StoragecacheId string `json:"storagecache_id"`
UpdateVersion int64 `json:"update_version"`
UpdatedAt *time.Time `json:"updated_at"`
UsedCapacity int64 `json:"used_capacity"`
VirtualCapacity int64 `json:"virtual_capacity"`
WasteCapacity int64 `json:"waste_capacity"`
Zone string `json:"zone"`
ZoneExtId string `json:"zone_ext_id"`
ZoneId string `json:"zone_id"`
}
type StorageListResponse struct {
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Storages []StorageDetails `json:"storages"`
Total int64 `json:"total"`
}
type ServerDetails struct {
Account string `json:"account"`
AccountHealthStatus string `json:"account_health_status"`
AccountId string `json:"account_id"`
AccountReadOnly bool `json:"account_read_only"`
AccountStatus string `json:"account_status"`
BackupGuestSync string `json:"backup_guest_sync"`
BackupGuestSyncStatus string `json:"backup_guest_sync_status"`
BackupHostId string `json:"backup_host_id"`
BackupHostName string `json:"backup_host_name"`
BackupHostStatus string `json:"backup_host_status"`
BillingCycle string `json:"billing_cycle"`
BillingType string `json:"billing_type"`
Bios string `json:"bios"`
BootOrder string `json:"boot_order"`
Brand string `json:"brand"`
CanDelete bool `json:"can_delete"`
CanRecycle bool `json:"can_recycle"`
CanUpdate bool `json:"can_update"`
Cdrom interface{} `json:"cdrom"`
CdromSupport bool `json:"cdrom_support"`
CloudEnv string `json:"cloud_env"`
Cloudregion string `json:"cloudregion"`
CloudregionId string `json:"cloudregion_id"`
Containers interface{} `json:"containers"`
CpuNumaPin map[string]interface{} `json:"cpu_numa_pin"`
CpuSockets int64 `json:"cpu_sockets"`
CreatedAt *time.Time `json:"created_at"`
DeleteFailReason interface{} `json:"delete_fail_reason"`
Deleted bool `json:"deleted"`
DeletedAt *time.Time `json:"deleted_at"`
Description string `json:"description"`
DisableDelete bool `json:"disable_delete"`
DiskSizeMb int64 `json:"disk"`
DiskCount int64 `json:"disk_count"`
Disks string `json:"disks"`
DisksInfo interface{} `json:"disks_info"`
DomainId string `json:"domain_id"`
Eip string `json:"eip"`
EipMode string `json:"eip_mode"`
EncryptAlg string `json:"encrypt_alg"`
EncryptKey string `json:"encrypt_key"`
EncryptKeyId string `json:"encrypt_key_id"`
EncryptKeyUser string `json:"encrypt_key_user"`
EncryptKeyUserDomain string `json:"encrypt_key_user_domain"`
EncryptKeyUserDomainId string `json:"encrypt_key_user_domain_id"`
EncryptKeyUserId string `json:"encrypt_key_user_id"`
Environment string `json:"environment"`
ExpiredAt *time.Time `json:"expired_at"`
ExternalId string `json:"external_id"`
ExtraCpuCount int64 `json:"extra_cpu_count"`
FlavorId string `json:"flavor_id"`
Floppy interface{} `json:"floppy"`
FloppySupport bool `json:"floppy_support"`
Freezed bool `json:"freezed"`
GpuCount string `json:"gpu_count"`
GpuModel string `json:"gpu_model"`
Host string `json:"host"`
HostAccessIp string `json:"host_access_ip"`
HostAccessMac string `json:"host_access_mac"`
HostBillingType string `json:"host_billing_type"`
HostEIP string `json:"host_eip"`
HostEnabled bool `json:"host_enabled"`
HostId string `json:"host_id"`
HostStatus string `json:"host_status"`
Hostname string `json:"hostname"`
Hypervisor string `json:"hypervisor"`
Id string `json:"id"`
ImportedAt *time.Time `json:"imported_at"`
Ips []string `json:"ips"`
IsBaremetal bool `json:"is_baremetal"`
IsDefer bool `json:"is_defer"`
IsEmulated bool `json:"is_emulated"`
IsMerge bool `json:"is_merge"`
IsMirror bool `json:"is_mirror"`
IsPublic bool `json:"is_public"`
IsSystem bool `json:"is_system"`
KeypairId string `json:"keypair_id"`
Manager string `json:"manager"`
ManagerDomain string `json:"manager_domain"`
ManagerDomainId string `json:"manager_domain_id"`
ManagerId string `json:"manager_id"`
ManagerProject string `json:"manager_project"`
ManagerProjectId string `json:"manager_project_id"`
MemoryPinned bool `json:"memory_pinned"`
Metadata map[string]string `json:"metadata"`
Mmemc interface{} `json:"mmemc"`
Name string `json:"name"`
NicType string `json:"nic_type"`
Nics interface{} `json:"nics"`
NSPSConfig map[string]interface{} `json:"nsps_config"`
OsArch string `json:"os_arch"`
OsFullName string `json:"os_full_name"`
OsName string `json:"os_name"`
OsType string `json:"os_type"`
PendingDeleted bool `json:"pending_deleted"`
PendingDeletedAt *time.Time `json:"pending_deleted_at"`
PowerStates string `json:"power_states"`
Progress float64 `json:"progress"`
Project string `json:"project"`
ProjectDomain string `json:"project_domain"`
ProjectId string `json:"project_id"`
ProjectMetadata map[string]string `json:"project_metadata"`
ProjectSrc string `json:"project_src"`
Provider string `json:"provider"`
PublicIp string `json:"public_ip"`
PublicScope string `json:"public_scope"`
PublicSrc string `json:"public_src"`
Rds bool `json:"rds"`
RecoveryMode string `json:"recovery_mode"`
ReorderMaster bool `json:"reorder_master"`
Schedtags []SchedtagShortDescDetails `json:"schedtags"`
SecurityGroup string `json:"security_group"`
SecurityGroupId string `json:"security_group_id"`
SecurityGroups interface{} `json:"security_groups"`
SharedDomains []SharedDomain `json:"shared_domains"`
SharedProjects []SharedProject `json:"shared_projects"`
ShutdownBehavior string `json:"shutdown_behavior"`
SourceOsDist string `json:"source_os_dist"`
Source string `json:"source"`
Status string `json:"status"`
StorageId string `json:"storage_id"`
StorageType string `json:"storage_type"`
SystemVmtypeName string `json:"system_vmtype_name"`
Tenant string `json:"tenant"`
TenantId string `json:"tenant_id"`
UpdateVersion int64 `json:"update_version"`
UpdatedAt *time.Time `json:"updated_at"`
UpgradeStatus string `json:"upgrade_status"`
UpdateFailReason interface{} `json:"update_fail_reason"`
UserData string `json:"user_data"`
VcpuCount int64 `json:"vcpu_count"`
VdiBrokerStuff map[string]interface{} `json:"vdi_broker_stuff"`
VdiConfig map[string]interface{} `json:"vdi_config"`
VditConfig map[string]interface{} `json:"vdit_config"`
VmemSize int64 `json:"vmem_size"`
VMEMSizeMb int64 `json:"vmem_size_mb"`
Vpc string `json:"vpc"`
VpcId string `json:"vpc_id"`
Zone string `json:"zone"`
ZoneId string `json:"zone_id"`
}
type ServerListResponse struct {
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Servers []ServerDetails `json:"servers"`
Total int64 `json:"total"`
}
type ServerStartRequest struct {
AutoPrepaid bool
QemuVersion string
}
type ServerStopRequest struct {
IsForce bool
StopCharging bool
TimeoutSecs int64
}
type ServerRestartRequest struct {
IsForce bool
}
type ServerOperationResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
TaskId string `json:"task_id"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
Operation string `json:"operation,omitempty"`
}
type ServerResetPasswordRequest struct {
Password string
ResetPassword bool
AutoStart bool
Username string
}
type ServerDeleteRequest struct {
OverridePendingDelete bool
Purge bool
DeleteSnapshots bool
DeleteEip bool
DeleteDisks bool
}
type CreateServerRequest struct {
Name string
VcpuCount int64
VmemSize int64
ImageId string
DiskSize int64
NetworkId string
ServerskuId string
Count int
Password string
AutoStart bool
BillingType string
Duration string
Description string
Hostname string
Hypervisor string
Metadata map[string]string
SecgroupId string
Secgroups []string
UserData string
KeypairId string
ProjectId string
ZoneId string
RegionId string
DisableDelete bool
BootOrder string
DataDisks []DiskConfig
}
type DiskConfig struct {
ImageId string
Size int64
DiskType string
}
type ServerCreateResponseData struct {
Servers []ServerCreateInfo `json:"servers"`
}
type ServerCreateInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
TaskID string `json:"task_id"`
}
type CreateServerResponse struct {
Status int `json:"status"`
Message string `json:"msg"`
Data ServerCreateResponseData `json:"data"`
}
type MonitorResponse struct {
Status int `json:"status"`
Data MonitorResponseData `json:"data"`
}
type MonitorResponseData struct {
Metrics []MetricData `json:"metrics"`
}
type MetricData struct {
Metric string `json:"metric"`
Unit string `json:"unit"`
Values []MetricValue `json:"values"`
}
type MetricValue struct {
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
}
type ServerStatsResponse struct {
Status int `json:"status"`
Data ServerStatsData `json:"data"`
}
type ServerStatsData struct {
CPUUsage float64 `json:"cpu_usage"`
MemUsage float64 `json:"mem_usage"`
DiskUsage float64 `json:"disk_usage"`
NetBpsRx int64 `json:"net_bps_rx"`
NetBpsTx int64 `json:"net_bps_tx"`
UpdatedAt string `json:"updated_at"`
}

View File

@@ -15,9 +15,14 @@
package options
import (
"strings"
"time"
common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
)
const DefaultPlatformName = "Cloudpods"
type MCPServerOptions struct {
common_options.CommonOptions
// 服务基础信息
@@ -26,9 +31,45 @@ type MCPServerOptions struct {
MCPServerDescription string `help:"MCP service description"`
// 连接超时配置
Timeout int `help:"SDK connection timeout to cloudpods service (seconds)" default:"30"`
Timeout int `help:"SDK connection timeout to platform API (seconds)" default:"30"`
// ServerCreateWaitSeconds 创建后等待 running/ready 的最长时间;超时仍返回 server_id便于 agent 用 server-show 继续轮询(宜小于 LLM MCPAgentTimeout
ServerCreateWaitSeconds int `help:"max seconds to wait for server running/ready after create; on timeout still return server_id" default:"90"`
}
var (
Options MCPServerOptions
)
// ResolvedPlatformName 返回配置中的平台展示名,空则回退 DefaultPlatformName。
func ResolvedPlatformName() string {
name := strings.TrimSpace(Options.PlatformName)
if name == "" {
return DefaultPlatformName
}
return name
}
// ServerCreateWaitDuration 创建等待时长。
func ServerCreateWaitDuration() time.Duration {
sec := Options.ServerCreateWaitSeconds
if sec <= 0 {
sec = 90
}
return time.Duration(sec) * time.Second
}
func OnOptionsChange(oldO, newO interface{}) bool {
oldOpts := oldO.(*MCPServerOptions)
newOpts := newO.(*MCPServerOptions)
changed := false
if common_options.OnCommonOptionsChange(&oldOpts.CommonOptions, &newOpts.CommonOptions) {
changed = true
}
// PlatformName 写入 ServerInstructions 需重启进程
if oldOpts.PlatformName != newOpts.PlatformName {
changed = true
}
// ServerCreateWaitSeconds / Timeout 热更新即可OptionManager 会拷贝到 Options
return changed
}

View File

@@ -17,9 +17,14 @@ package server
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/mark3labs/mcp-go/server"
@@ -30,77 +35,63 @@ import (
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/climcgen"
"yunion.io/x/onecloud/pkg/mcp-server/options"
"yunion.io/x/onecloud/pkg/mcp-server/registry"
"yunion.io/x/onecloud/pkg/mcp-server/tools"
)
// CloudpodsMCPServer 是 MCP 服务器的核心结构体包含配置、日志、MCP 实例、注册中心和工具列表
type CloudpodsMCPServer struct {
mcpServer *server.MCPServer
registry *registry.Registry
tools []tools.Tool
tools []climcgen.Tool
}
// NewServer 创建一个新的 Cloudpods MCP 服务器实例,初始化 MCP 服务器和注册中心,并创建所有工具
func NewServer() *CloudpodsMCPServer {
// NewServerOptions 创建 MCP 服务器时可覆盖的选项。
type NewServerOptions struct {
// Instructions MCP 全局说明;空则使用 climcgen.BuildServerInstructions(PlatformName)(再叠加 RegisterExtraInstructions
Instructions string
}
// 创建mcp server对象
// NewServer 创建一个新的 Cloudpods MCP 服务器实例。
// 工具从 climc shell.CommandTable + Options struct tagmcp-desc自动生成
// 可通过 climcgen.RegisterExtraTools 追加工具。
func NewServer() *CloudpodsMCPServer {
return NewServerWithOptions(nil)
}
// NewServerWithOptions 同 NewServer允许覆盖 Instructions 等。
func NewServerWithOptions(opt *NewServerOptions) *CloudpodsMCPServer {
instructions := climcgen.BuildServerInstructions(options.ResolvedPlatformName())
if opt != nil && strings.TrimSpace(opt.Instructions) != "" {
instructions = opt.Instructions
}
if extra := climcgen.BuildExtraInstructions(); extra != "" {
instructions = strings.TrimSpace(instructions) + "\n\n" + extra
}
serverName := strings.TrimSpace(options.Options.MCPServerName)
if serverName == "" {
serverName = options.ResolvedPlatformName()
}
mcpServer := server.NewMCPServer(
options.Options.MCPServerName,
serverName,
options.Options.MCPServerVersion,
server.WithToolCapabilities(false),
server.WithRecovery(),
server.WithInstructions(instructions),
)
// 创建注册中心对象
reg := registry.NewRegistry()
var allTools []tools.Tool
// 创建mcclient sdk的适配器对象
adapter := adapters.NewCloudpodsAdapter()
// 创建具体的工具函数对象
// 用于查询资源的工具函数
regionsTool := tools.NewCloudpodsRegionsTool(adapter)
vpcsTool := tools.NewCloudpodsVPCsTool(adapter)
networksTool := tools.NewCloudpodsNetworksTool(adapter)
imagesTool := tools.NewCloudpodsImagesTool(adapter)
skusTool := tools.NewCloudpodsServerSkusTool(adapter)
storagesTool := tools.NewCloudpodsStoragesTool(adapter)
serversTool := tools.NewCloudpodsServersTool(adapter)
// 用于操作资源的工具函数
serverStartTool := tools.NewCloudpodsServerStartTool(adapter)
serverStopTool := tools.NewCloudpodsServerStopTool(adapter)
serverRestartTool := tools.NewCloudpodsServerRestartTool(adapter)
serverResetPasswordTool := tools.NewCloudpodsServerResetPasswordTool(adapter)
serverDeleteTool := tools.NewCloudpodsServerDeleteTool(adapter)
serverCreateTool := tools.NewCloudpodsServerCreateTool(adapter)
serverMonitorTool := tools.NewCloudpodsServerMonitorTool(adapter)
serverStatsTool := tools.NewCloudpodsServerStatsTool(adapter)
// 将所有的工具函数存储到一个切片中
allTools = append(
allTools,
regionsTool,
vpcsTool,
networksTool,
imagesTool,
skusTool,
storagesTool,
serversTool,
serverStartTool,
serverStopTool,
serverRestartTool,
serverResetPasswordTool,
serverDeleteTool,
serverCreateTool,
serverMonitorTool,
serverStatsTool,
)
allTools, err := climcgen.BuildTools(adapter)
if err != nil {
log.Fatalf("build climc MCP tools failed: %s", err)
}
if extra := climcgen.BuildExtraTools(); len(extra) > 0 {
allTools = append(extra, allTools...)
}
return &CloudpodsMCPServer{
mcpServer: mcpServer,
@@ -111,24 +102,17 @@ func NewServer() *CloudpodsMCPServer {
// Initialize 初始化注册中心和所有工具
func (s *CloudpodsMCPServer) Initialize() error {
// 初始化工具注册中心
if err := s.registry.Initialize(s.mcpServer); err != nil {
return fmt.Errorf("初始化工具注册中心失败: %w", err)
}
// 注册内置工具
if err := s.registerAllTools(); err != nil {
return fmt.Errorf("注册内置工具失败: %w", err)
}
return nil
}
// registerAllTools 将所有工具注册到注册中心
func (s *CloudpodsMCPServer) registerAllTools() error {
for _, tool := range s.tools {
// 注册距离查询工具
if err := s.registry.RegisterTool(
tool.GetName(),
tool.GetTool(),
@@ -137,22 +121,18 @@ func (s *CloudpodsMCPServer) registerAllTools() error {
return fmt.Errorf("注册工具失败: %w", err)
}
}
log.Infof("All tools register completed")
log.Infof("All tools register completed, count=%d", len(s.tools))
return nil
}
// Start 以sse模式启动 mcp 服务
func (s *CloudpodsMCPServer) Start() error {
// 设置 contextFunc 来从 HTTP header 中提取认证信息并放入 context
// 支持X-Auth-Tokentoken、AK/SKCursor 双 header、X-API-KeyClaude 单 headertoken 或 base64(ak:sk)
contextFunc := func(ctx context.Context, r *http.Request) context.Context {
// authenticateRequest 从 Header 注入会话凭据(可选);无凭据时 ok=false仍返回原 ctx。
// /sse 与 tools/list 允许匿名tools/call 在工具 Handler 内强制鉴权。
func authenticateRequest(ctx context.Context, r *http.Request) (context.Context, bool) {
tokenStr := r.Header.Get(api.AUTH_TOKEN_HEADER)
akStr := r.Header.Get("AK")
skStr := r.Header.Get("SK")
apiKey := r.Header.Get("X-API-Key")
// 1) 优先使用 X-Auth-Token
if tokenStr != "" {
if auth.IsAuthed() {
userCred, err := auth.Verify(ctx, tokenStr)
@@ -160,27 +140,22 @@ func (s *CloudpodsMCPServer) Start() error {
log.Errorf("Verify token failed: %s", err)
} else {
ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred)
log.Debugf("UserCred set in context from HTTP header token")
return ctx
return ctx, true
}
}
}
// 2) Cursor 方式:直接使用 AK、SK 两个 Header
if akStr != "" && skStr != "" {
ctx = context.WithValue(ctx, adapters.ContextKeyAK, akStr)
ctx = context.WithValue(ctx, adapters.ContextKeySK, skStr)
log.Debugf("AK/SK set in context from headers")
return ctx
return ctx, true
}
// 3) Claude 方式X-API-Key 可为 token或 base64(ak:sk)
if apiKey != "" {
if auth.IsAuthed() {
if userCred, err := auth.Verify(ctx, apiKey); err == nil {
ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred)
log.Debugf("UserCred set in context from X-API-Key token")
return ctx
return ctx, true
}
}
decoded, err := base64.StdEncoding.DecodeString(apiKey)
@@ -189,13 +164,19 @@ func (s *CloudpodsMCPServer) Start() error {
if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
ctx = context.WithValue(ctx, adapters.ContextKeyAK, parts[0])
ctx = context.WithValue(ctx, adapters.ContextKeySK, parts[1])
log.Debugf("AK/SK set in context from X-API-Key base64(ak:sk)")
return ctx
return ctx, true
}
}
}
return ctx
return ctx, false
}
// Start 以sse模式启动 mcp 服务
func (s *CloudpodsMCPServer) Start() error {
contextFunc := func(ctx context.Context, r *http.Request) context.Context {
ctx2, _ := authenticateRequest(ctx, r)
return ctx2
}
mux := http.NewServeMux()
@@ -223,17 +204,37 @@ func (s *CloudpodsMCPServer) Start() error {
mux.Handle(sseServer.CompleteSsePath(), sseServer.SSEHandler())
mux.Handle(sseServer.CompleteMessagePath(), sseServer.MessageHandler())
if err := sseServer.Start(fmt.Sprintf("%s:%d", options.Options.Address, options.Options.Port)); err != nil {
addr := fmt.Sprintf("%s:%d", options.Options.Address, options.Options.Port)
errCh := make(chan error, 1)
go func() {
errCh <- sseServer.Start(addr)
}()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
log.Infof("Start mcp server successfully on %s", addr)
select {
case err := <-errCh:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
log.Infof("Start mcp server successfully")
return nil
case sig := <-sigCh:
log.Infof("Received signal %v, shutting down mcp server...", sig)
signal.Stop(sigCh)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := sseServer.Shutdown(ctx); err != nil {
return fmt.Errorf("graceful shutdown failed: %w", err)
}
log.Infof("Mcp server stopped")
return nil
}
}
// StartStdio 以stdio模式启动 mcp 服务
func (s *CloudpodsMCPServer) StartStdio() error {
err := server.ServeStdio(s.mcpServer)
if err != nil {
return err

View File

@@ -16,42 +16,74 @@ package service
import (
"os"
"strings"
"yunion.io/x/log"
app_common "yunion.io/x/onecloud/pkg/cloudcommon/app"
common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
"yunion.io/x/onecloud/pkg/mcp-server/climcgen"
"yunion.io/x/onecloud/pkg/mcp-server/options"
"yunion.io/x/onecloud/pkg/mcp-server/server"
)
const (
serviceType = "mcpserver"
serviceVersion = ""
)
// StartOptions 启动时可覆盖的选项。
type StartOptions struct {
// Instructions 若非空则完全替换默认 ServerInstructions已按 PlatformName 生成)
Instructions string
// ExtraInstructions 追加到默认(或 Instructions说明之后
ExtraInstructions string
}
func StartService() {
StartServiceWithOptions(nil)
}
func StartServiceWithOptions(startOpt *StartOptions) {
opts := &options.Options
common_options.ParseOptions(opts, os.Args, "mcpserver.conf", "mcpserver")
common_options.ParseOptions(opts, os.Args, "mcpserver.conf", serviceType)
// 如果配置了认证信息,初始化 auth manager
commonOpts := &opts.CommonOptions
// 只有当所有必需的认证配置都存在时,才初始化 auth manager
if len(commonOpts.AuthURL) > 0 && len(commonOpts.AdminUser) > 0 &&
len(commonOpts.AdminPassword) > 0 && len(commonOpts.AdminProject) > 0 {
app_common.InitAuth(commonOpts, func() {
log.Infof("Auth complete!!")
})
} else {
log.Infof("Auth configuration incomplete, skipping auth initialization. AuthURL: %s, AdminUser: %s, AdminPassword: %s, AdminProject: %s", commonOpts.AuthURL, commonOpts.AdminUser, commonOpts.AdminPassword, commonOpts.AdminProject)
log.Infof("Auth configuration incomplete, skipping auth initialization. AuthURL: %s, AdminUser: %s, AdminPasswordSet: %v, AdminProject: %s",
commonOpts.AuthURL, commonOpts.AdminUser, len(commonOpts.AdminPassword) > 0, commonOpts.AdminProject)
}
// 创建服务器
srv := server.NewServer()
common_options.StartOptionManager(opts, opts.ConfigSyncPeriodSeconds, serviceType, serviceVersion, options.OnOptionsChange)
srvOpt := &server.NewServerOptions{
Instructions: resolveInstructions(startOpt),
}
srv := server.NewServerWithOptions(srvOpt)
// 初始化服务器
if err := srv.Initialize(); err != nil {
log.Fatalf("Fail to init mcp server: %s", err)
}
// 启动服务器
if err := srv.Start(); err != nil {
log.Fatalf("Fail to start mcp server: %s", err)
}
}
func resolveInstructions(startOpt *StartOptions) string {
base := climcgen.BuildServerInstructions(options.ResolvedPlatformName())
if startOpt != nil {
if strings.TrimSpace(startOpt.Instructions) != "" {
base = startOpt.Instructions
}
if extra := strings.TrimSpace(startOpt.ExtraInstructions); extra != "" {
base = strings.TrimSpace(base) + "\n\n" + extra
}
}
return base
}

View File

@@ -1,240 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsImagesTool 是一个用于查询 Cloudpods 镜像列表的工具
// 它封装了 Cloudpods 适配器和日志记录器
type CloudpodsImagesTool struct {
// adapter 用于与 Cloudpods API 进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsImagesTool 创建一个新的 CloudpodsImagesTool 实例
// 参数:
// - adapter: Cloudpods 适配器实例,用于与 Cloudpods API 交互
//
// 返回值:
// - *CloudpodsImagesTool: 新创建的 CloudpodsImagesTool 实例
func NewCloudpodsImagesTool(adapter *adapters.CloudpodsAdapter) *CloudpodsImagesTool {
return &CloudpodsImagesTool{
adapter: adapter,
}
}
// GetTool 定义并返回 Cloudpods 镜像列表查询工具的元数据
// 该工具允许用户查询 Cloudpods 中的磁盘镜像列表,并支持多种查询参数
// 返回值:
// - mcp.Tool: 定义了工具名称、描述和参数的工具对象
func (c *CloudpodsImagesTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_list_images",
mcp.WithDescription("查询Cloudpods磁盘镜像列表获取系统镜像信息"),
mcp.WithString("limit", mcp.Description("返回结果数量限制默认为20")),
mcp.WithString("offset", mcp.Description("返回结果偏移量默认为0")),
mcp.WithString("search", mcp.Description("搜索关键词,可以按镜像名称搜索")),
mcp.WithString("os_types", mcp.Description("操作系统类型多个用逗号分隔Linux,Windows,FreeBSD")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理 Cloudpods 镜像列表查询请求
// 该方法解析请求参数,调用适配器查询镜像列表,并格式化返回结果
// 参数:
// - ctx: 上下文对象,用于控制请求生命周期
// - req: 工具调用请求对象,包含查询参数
//
// 返回值:
// - *mcp.CallToolResult: 格式化后的镜像列表查询结果
// - error: 如果查询过程中发生错误,则返回相应的错误信息
func (c *CloudpodsImagesTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 设置默认的查询结果数量限制为20
limit := 20
// 如果请求中包含limit参数且为有效正整数则使用该值
if limitStr := req.GetString("limit", ""); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
// 设置默认的查询偏移量为0
offset := 0
// 如果请求中包含offset参数且为有效非负整数则使用该值
if offsetStr := req.GetString("offset", ""); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// 获取搜索关键词参数
search := req.GetString("search", "")
// 解析操作系统类型参数,支持多个类型用逗号分隔
var osTypes []string
if osTypesStr := req.GetString("os_types", ""); osTypesStr != "" {
osTypes = strings.Split(osTypesStr, ",")
for i, osType := range osTypes {
osTypes[i] = strings.TrimSpace(osType)
}
}
// 获取访问凭证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器查询镜像列表
imagesResponse, err := c.adapter.ListImages(ctx, limit, offset, search, osTypes, ak, sk)
if err != nil {
log.Errorf("Fail to query image: %s", err)
return nil, fmt.Errorf("fail to query image: %w", err)
}
// 格式化查询结果
formattedResult := c.formatImagesResult(imagesResponse, limit, offset, search, osTypes)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
// 返回格式化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
// 返回值:
// - string: 工具名称,用于唯一标识该工具
func (c *CloudpodsImagesTool) GetName() string {
return "cloudpods_list_images"
}
// formatImagesResult 格式化镜像列表查询结果
// 该方法将从适配器获取的原始镜像数据转换为结构化的响应格式,包含查询信息、镜像详情和摘要信息
// 参数:
// - response: 从适配器获取的原始镜像列表响应数据
// - limit: 查询结果数量限制
// - offset: 查询偏移量
// - search: 搜索关键词
// - osTypes: 操作系统类型过滤条件
//
// 返回值:
// - map[string]interface{}: 格式化后的镜像列表数据,包含查询信息、镜像详情和摘要
func (c *CloudpodsImagesTool) formatImagesResult(response *models.ImageListResponse, limit, offset int, search string, osTypes []string) map[string]interface{} {
// 初始化格式化结果结构
formatted := map[string]interface{}{
// 查询信息部分,包含查询参数和结果统计
"query_info": map[string]interface{}{
"limit": limit,
"offset": offset,
"search": search,
"os_types": osTypes,
"total": response.Total,
"count": len(response.Images),
},
// 镜像列表部分,初始化为空数组
"images": make([]map[string]interface{}, 0, len(response.Images)),
}
// 遍历原始镜像数据,提取每个镜像的详细信息
for _, image := range response.Images {
// 构造单个镜像的详细信息
imageInfo := map[string]interface{}{
"id": image.Id,
"name": image.Name,
"description": image.Description,
"status": image.Status,
"disk_format": image.DiskFormat,
"size": image.Size,
"checksum": image.Checksum,
"oss_checksum": image.OssChecksum,
"fast_hash": image.FastHash,
"location": image.Location,
"os_arch": image.OsArch,
"min_disk": image.MinDisk,
"min_ram": image.MinRam,
"is_data": image.IsData,
"is_guest_image": image.IsGuestImage,
"is_public": image.IsPublic,
"is_standard": image.IsStandard,
"is_system": image.IsSystem,
"is_emulated": image.IsEmulated,
"protected": image.Protected,
"disable_delete": image.DisableDelete,
"freezed": image.Freezed,
"pending_deleted": image.PendingDeleted,
"pending_deleted_at": image.PendingDeletedAt,
"auto_delete_at": image.AutoDeleteAt,
"encrypt_alg": image.EncryptAlg,
"encrypt_key": image.EncryptKey,
"encrypt_key_id": image.EncryptKeyId,
"encrypt_key_user": image.EncryptKeyUser,
"encrypt_key_user_domain": image.EncryptKeyUserDomain,
"encrypt_key_user_domain_id": image.EncryptKeyUserDomainId,
"encrypt_key_user_id": image.EncryptKeyUserId,
"encrypt_status": image.EncryptStatus,
"owner": image.Owner,
"project": image.Project,
"project_id": image.ProjectId,
"project_domain": image.ProjectDomain,
"project_metadata": image.ProjectMetadata,
"project_src": image.ProjectSrc,
"tenant": image.Tenant,
"tenant_id": image.TenantId,
"domain_id": image.DomainId,
"public_scope": image.PublicScope,
"public_src": image.PublicSrc,
"shared_domains": image.SharedDomains,
"shared_projects": image.SharedProjects,
"properties": image.Properties,
"metadata": image.Metadata,
"progress": image.Progress,
"can_delete": image.CanDelete,
"can_update": image.CanUpdate,
"update_version": image.UpdateVersion,
"created_at": image.CreatedAt,
"updated_at": image.UpdatedAt,
}
// 将镜像信息添加到结果数组中
formatted["images"] = append(formatted["images"].([]map[string]interface{}), imageInfo)
}
// 构造结果摘要信息
formatted["summary"] = map[string]interface{}{
"total_images": response.Total,
"returned_count": len(response.Images),
"has_more": response.Total > int64(offset+len(response.Images)),
"next_offset": offset + len(response.Images),
}
// 返回格式化后的完整结果
return formatted
}

View File

@@ -1,265 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsNetworksTool 是一个用于查询 Cloudpods 网络列表的工具
type CloudpodsNetworksTool struct {
// adapter 用于与 Cloudpods API 进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsNetworksTool 创建一个新的 CloudpodsNetworksTool 实例
// adapter: 用于与 Cloudpods API 进行交互的适配器
// 返回值: 指向新创建的 CloudpodsNetworksTool 实例的指针
func NewCloudpodsNetworksTool(adapter *adapters.CloudpodsAdapter) *CloudpodsNetworksTool {
return &CloudpodsNetworksTool{
adapter: adapter,
}
}
// GetTool 定义并返回网络列表查询工具的元数据
// 该工具用于查询Cloudpods中的IP子网列表获取网络配置信息
// 支持的参数包括:
// - limit: 返回结果数量限制默认为20
// - offset: 返回结果偏移量默认为0
// - search: 搜索关键词,可以按网络名称搜索
// - vpc_id: 过滤指定VPC的网络资源
// - ak: 用户登录cloudpods后获取的access key
// - sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsNetworksTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_list_networks",
mcp.WithDescription("查询Cloudpods IP子网列表获取网络配置信息"),
mcp.WithString("limit", mcp.Description("返回结果数量限制默认为20")),
mcp.WithString("offset", mcp.Description("返回结果偏移量默认为0")),
mcp.WithString("search", mcp.Description("搜索关键词,可以按网络名称搜索")),
mcp.WithString("vpc_id", mcp.Description("过滤指定VPC的网络资源")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理网络列表查询请求
// ctx: 控制请求生命周期的上下文
// req: 包含查询参数的请求对象
// 返回值: 包含查询结果的工具结果对象或错误信息
func (c *CloudpodsNetworksTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 设置默认查询限制为20
limit := 20
if limitStr := req.GetString("limit", ""); limitStr != "" {
// 解析limit参数如果解析成功且大于0则使用解析后的值
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
// 设置默认偏移量为0
offset := 0
if offsetStr := req.GetString("offset", ""); offsetStr != "" {
// 解析offset参数如果解析成功且大于等于0则使用解析后的值
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// 获取搜索关键词和VPC ID参数
search := req.GetString("search", "")
vpcId := req.GetString("vpc_id", "")
// 获取访问凭证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器获取网络列表
networksResponse, err := c.adapter.ListNetworks(ctx, limit, offset, search, vpcId, ak, sk)
if err != nil {
log.Errorf("Fail to query network: %s", err)
return nil, fmt.Errorf("fail to query network: %w", err)
}
// 格式化查询结果
formattedResult := c.formatNetworksResult(networksResponse, limit, offset, search, vpcId)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
// 返回格式化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
// 返回值: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsNetworksTool) GetName() string {
return "cloudpods_list_networks"
}
// formatNetworksResult 格式化网络列表查询结果
// response: 从适配器获取的原始网络数据
// limit: 查询限制数量
// offset: 查询偏移量
// search: 搜索关键词
// vpcId: VPC ID过滤条件
// 返回值: 格式化后的网络列表数据,包含查询信息、网络列表和摘要信息
func (c *CloudpodsNetworksTool) formatNetworksResult(response *models.NetworkListResponse, limit, offset int, search, vpcId string) map[string]interface{} {
// 初始化结果结构,包含查询信息和网络列表
formatted := map[string]interface{}{
"query_info": map[string]interface{}{
"limit": limit,
"offset": offset,
"search": search,
"vpc_id": vpcId,
"total": response.Total,
"count": len(response.Networks),
},
"networks": make([]map[string]interface{}, 0, len(response.Networks)),
}
// 遍历原始网络数据,构造每个网络的详细信息
for _, network := range response.Networks {
// 构造单个网络信息
networkInfo := map[string]interface{}{
"id": network.Id,
"name": network.Name,
"description": network.Description,
"status": network.Status,
"guest_ip_start": network.GuestIpStart,
"guest_ip_end": network.GuestIpEnd,
"guest_ip_mask": network.GuestIpMask,
"guest_gateway": network.GuestGateway,
"guest_dns": network.GuestDns,
"guest_dhcp": network.GuestDhcp,
"guest_ntp": network.GuestNtp,
"guest_domain": network.GuestDomain,
"guest_ip6_start": network.GuestIp6Start,
"guest_ip6_end": network.GuestIp6End,
"guest_ip6_mask": network.GuestIp6Mask,
"guest_gateway6": network.GuestGateway6,
"guest_dns6": network.GuestDns6,
"guest_domain6": network.GuestDomain6,
"vpc": network.Vpc,
"vpc_id": network.VpcId,
"vpc_ext_id": network.VpcExtId,
"wire": network.Wire,
"wire_id": network.WireId,
"zone": network.Zone,
"zone_id": network.ZoneId,
"cloudregion": network.Cloudregion,
"cloudregion_id": network.CloudregionId,
"region": network.Region,
"region_id": network.RegionId,
"provider": network.Provider,
"brand": network.Brand,
"cloud_env": network.CloudEnv,
"environment": network.Environment,
"external_id": network.ExternalId,
"account": network.Account,
"account_id": network.AccountId,
"account_status": network.AccountStatus,
"account_health_status": network.AccountHealthStatus,
"manager": network.Manager,
"manager_id": network.ManagerId,
"manager_domain": network.ManagerDomain,
"manager_domain_id": network.ManagerDomainId,
"manager_project": network.ManagerProject,
"manager_project_id": network.ManagerProjectId,
"server_type": network.ServerType,
"alloc_policy": network.AllocPolicy,
"vlan_id": network.VlanId,
"bgp_type": network.BgpType,
"is_auto_alloc": network.IsAutoAlloc,
"is_classic": network.IsClassic,
"is_default_vpc": network.IsDefaultVpc,
"is_public": network.IsPublic,
"is_system": network.IsSystem,
"is_emulated": network.IsEmulated,
"exit": network.Exit,
"freezed": network.Freezed,
"pending_deleted": network.PendingDeleted,
"pending_deleted_at": network.PendingDeletedAt,
"ports": network.Ports,
"ports_used": network.PortsUsed,
"ports6_used": network.Ports6Used,
"total": network.Total,
"total6": network.Total6,
"vnics": network.Vnics,
"vnics4": network.Vnics4,
"vnics6": network.Vnics6,
"bm_vnics": network.BmVnics,
"bm_reused_vnics": network.BmReusedVnics,
"eip_vnics": network.EipVnics,
"group_vnics": network.GroupVnics,
"lb_vnics": network.LbVnics,
"nat_vnics": network.NatVnics,
"networkinterface_vnics": network.NetworkinterfaceVnics,
"rds_vnics": network.RdsVnics,
"reserve_vnics4": network.ReserveVnics4,
"reserve_vnics6": network.ReserveVnics6,
"routes": network.Routes,
"schedtags": network.Schedtags,
"additional_wires": network.AdditionalWires,
"shared_domains": network.SharedDomains,
"shared_projects": network.SharedProjects,
"project": network.Project,
"project_id": network.ProjectId,
"project_domain": network.ProjectDomain,
"project_metadata": network.ProjectMetadata,
"project_src": network.ProjectSrc,
"tenant": network.Tenant,
"tenant_id": network.TenantId,
"domain_id": network.DomainId,
"public_scope": network.PublicScope,
"public_src": network.PublicSrc,
"source": network.Source,
"progress": network.Progress,
"can_delete": network.CanDelete,
"can_update": network.CanUpdate,
"metadata": network.Metadata,
"created_at": network.CreatedAt,
"updated_at": network.UpdatedAt,
"imported_at": network.ImportedAt,
}
// 将网络信息添加到结果数组中
formatted["networks"] = append(formatted["networks"].([]map[string]interface{}), networkInfo)
}
// 构造摘要信息
formatted["summary"] = map[string]interface{}{
"total_networks": response.Total,
"returned_count": len(response.Networks),
"has_more": response.Total > int64(offset+len(response.Networks)),
"next_offset": offset + len(response.Networks),
}
// 返回格式化后的结果
return formatted
}

View File

@@ -1,192 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsRegionsTool 是用于查询 Cloudpods 区域列表的工具
type CloudpodsRegionsTool struct {
// adapter 用于与 Cloudpods API 进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsRegionsTool 创建一个新的 Cloudpods 区域查询工具
// adapter: 用于与 Cloudpods API 进行交互的适配器
// 返回值: 指向新创建的 CloudpodsRegionsTool 实例的指针
func NewCloudpodsRegionsTool(adapter *adapters.CloudpodsAdapter) *CloudpodsRegionsTool {
return &CloudpodsRegionsTool{
adapter: adapter,
}
}
// GetTool 返回 MCP 工具定义,用于查询 Cloudpods 区域列表
// 该工具用于查询Cloudpods中的区域列表获取所有可用的云区域信息
// 支持的参数包括:
// - limit: 返回结果数量限制默认为50
// - offset: 返回结果偏移量默认为0
// - search: 搜索关键词,可以按区域名称搜索
// - provider: 云平台提供商例如aws、azure、aliyun等
// - ak: 用户登录cloudpods后获取的access key
// - sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsRegionsTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_list_regions",
mcp.WithDescription("查询Cloudpods区域列表获取所有可用的云区域信息"),
mcp.WithString("limit", mcp.Description("返回结果数量限制默认为50")),
mcp.WithString("offset", mcp.Description("返回结果偏移量默认为0")),
mcp.WithString("search", mcp.Description("搜索关键词,可以按区域名称搜索")),
mcp.WithString("provider", mcp.Description("云平台提供商例如aws、azure、aliyun等")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理查询 Cloudpods 区域列表的请求
// ctx: 控制请求生命周期的上下文
// req: 包含查询参数的请求对象
// 返回值: 包含查询结果的工具结果对象或错误信息
func (c *CloudpodsRegionsTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 设置默认查询限制为50
limit := 50
if limitStr := req.GetString("limit", ""); limitStr != "" {
// 解析limit参数如果解析成功且大于0则使用解析后的值
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
// 设置默认偏移量为0
offset := 0
if offsetStr := req.GetString("offset", ""); offsetStr != "" {
// 解析offset参数如果解析成功且大于等于0则使用解析后的值
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// 获取搜索关键词和提供商参数
search := req.GetString("search", "")
provider := req.GetString("provider", "")
// 获取访问凭证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器获取区域列表
regionsResponse, err := c.adapter.ListCloudRegions(ctx, limit, offset, search, provider, ak, sk)
if err != nil {
log.Errorf("Fail to query region: %s", err)
return nil, fmt.Errorf("fail to query region: %w", err)
}
// 格式化查询结果
formattedResult := c.formatRegionsResult(regionsResponse, limit, offset, search, provider)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
// 返回格式化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// formatRegionsResult 格式化区域列表查询结果
// response: 从适配器获取的原始区域数据
// limit: 查询限制数量
// offset: 查询偏移量
// search: 搜索关键词
// provider: 云平台提供商
// 返回值: 格式化后的区域列表数据,包含查询信息、区域列表和摘要信息
func (c *CloudpodsRegionsTool) formatRegionsResult(response *models.CloudregionListResponse, limit, offset int, search, provider string) map[string]interface{} {
// 初始化结果结构,包含查询信息和区域列表
formatted := map[string]interface{}{
"query_info": map[string]interface{}{
"limit": limit,
"offset": offset,
"search": search,
"provider": provider,
"total": response.Total,
"count": len(response.Cloudregions),
},
"cloudregions": make([]map[string]interface{}, 0, len(response.Cloudregions)),
}
// 遍历原始区域数据,构造每个区域的详细信息
for _, region := range response.Cloudregions {
// 构造单个区域信息
regionInfo := map[string]interface{}{
"id": region.Id,
"name": region.Name,
"description": region.Description,
"provider": region.Provider,
"cloud_env": region.CloudEnv,
"environment": region.Environment,
"city": region.City,
"country_code": region.CountryCode,
"latitude": region.Latitude,
"longitude": region.Longitude,
"status": region.Status,
"enabled": region.Enabled,
"external_id": region.ExternalId,
"guest_count": region.GuestCount,
"guest_increment_count": region.GuestIncrementCount,
"network_count": region.NetworkCount,
"vpc_count": region.VpcCount,
"zone_count": region.ZoneCount,
"progress": region.Progress,
"source": region.Source,
"can_delete": region.CanDelete,
"can_update": region.CanUpdate,
"is_emulated": region.IsEmulated,
"metadata": region.Metadata,
"created_at": region.CreatedAt,
"updated_at": region.UpdatedAt,
"imported_at": region.ImportedAt,
}
// 将区域信息添加到结果数组中
formatted["cloudregions"] = append(formatted["cloudregions"].([]map[string]interface{}), regionInfo)
}
// 构造摘要信息
formatted["summary"] = map[string]interface{}{
"total_cloudregions": response.Total,
"returned_count": len(response.Cloudregions),
"has_more": response.Total > int64(offset+len(response.Cloudregions)),
"next_offset": offset + len(response.Cloudregions),
}
// 返回格式化后的结果
return formatted
}
// GetName 返回工具名称
// 返回值: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsRegionsTool) GetName() string {
return "cloudpods_list_regions"
}

View File

@@ -1,350 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsServerCreateTool 用于创建Cloudpods虚拟机实例
type CloudpodsServerCreateTool struct {
// adapter 用于与 Cloudpods API 进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerCreateTool 创建一个新的CloudpodsServerCreateTool实例
// adapter: 用于与Cloudpods API交互的适配器
// 返回值: CloudpodsServerCreateTool实例指针
func NewCloudpodsServerCreateTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerCreateTool {
return &CloudpodsServerCreateTool{
adapter: adapter,
}
}
// GetTool 定义并返回创建虚拟机工具的元数据
// 该工具用于创建Cloudpods虚拟机实例支持指定各种配置参数
// name: 虚拟机名称 (必填)
// vcpu_count: CPU核心数 (必填)
// vmem_size: 内存大小(MB) (必填)
// image_id: 镜像ID (必填)
// disk_size: 系统盘大小(GB),不指定则使用镜像默认大小
// network_id: 网络ID (必填)
// serversku_id: 套餐ID指定后将忽略vcpu_count和vmem_size参数
// password: 虚拟机密码长度8-30个字符
// count: 创建数量默认为1
// auto_start: 是否自动启动默认为true
// billing_type: 计费类型例如postpaid、prepaid
// duration: 包年包月时长例如1M、1Y
// description: 描述信息
// hostname: 主机名
// hypervisor: 虚拟化技术如kvm, esxi等默认为kvm
// metadata: 标签列表格式为JSON字符串例如{"key1":"value1","key2":"value2"}
// secgroup_id: 安全组ID
// secgroups: 安全组ID列表多个ID用逗号分隔
// user_data: 用户自定义启动脚本
// keypair_id: 秘钥对ID
// project_id: 项目ID
// zone_id: 可用区ID
// region_id: 区域ID
// disable_delete: 是否开启删除保护默认为true
// boot_order: 启动顺序如cdn
// data_disks: 数据盘配置格式为JSON字符串数组例如[{"size":100,"disk_type":"data"}]
// ak: 用户登录cloudpods后获取的access key
// sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsServerCreateTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_create_server",
mcp.WithDescription("创建Cloudpods虚拟机实例"),
mcp.WithString("name", mcp.Required(), mcp.Description("虚拟机名称")),
mcp.WithString("vcpu_count", mcp.Required(), mcp.Description("CPU核心数")),
mcp.WithString("vmem_size", mcp.Required(), mcp.Description("内存大小(MB)")),
mcp.WithString("image_id", mcp.Required(), mcp.Description("镜像ID")),
mcp.WithString("disk_size", mcp.Description("系统盘大小(GB),不指定则使用镜像默认大小")),
mcp.WithString("network_id", mcp.Required(), mcp.Description("网络ID")),
mcp.WithString("serversku_id", mcp.Description("套餐ID指定后将忽略vcpu_count和vmem_size参数")),
mcp.WithString("password", mcp.Description("虚拟机密码长度8-30个字符")),
mcp.WithString("count", mcp.Description("创建数量默认为1")),
mcp.WithString("auto_start", mcp.Description("是否自动启动默认为true")),
mcp.WithString("billing_type", mcp.Description("计费类型例如postpaid、prepaid")),
mcp.WithString("duration", mcp.Description("包年包月时长例如1M、1Y")),
mcp.WithString("description", mcp.Description("描述信息")),
mcp.WithString("hostname", mcp.Description("主机名")),
mcp.WithString("hypervisor", mcp.Description("虚拟化技术如kvm, esxi等默认为kvm")),
mcp.WithString("metadata", mcp.Description("标签列表格式为JSON字符串例如{\"key1\":\"value1\",\"key2\":\"value2\"}")),
mcp.WithString("secgroup_id", mcp.Description("安全组ID")),
mcp.WithString("secgroups", mcp.Description("安全组ID列表多个ID用逗号分隔")),
mcp.WithString("user_data", mcp.Description("用户自定义启动脚本")),
mcp.WithString("keypair_id", mcp.Description("秘钥对ID")),
mcp.WithString("project_id", mcp.Description("项目ID")),
mcp.WithString("zone_id", mcp.Description("可用区ID")),
mcp.WithString("region_id", mcp.Description("区域ID")),
mcp.WithString("disable_delete", mcp.Description("是否开启删除保护默认为true")),
mcp.WithString("boot_order", mcp.Description("启动顺序如cdn")),
mcp.WithString("data_disks", mcp.Description("数据盘配置格式为JSON字符串数组例如[{\"size\":100,\"disk_type\":\"data\"}]")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理创建虚拟机的请求
// ctx: 上下文,用于控制请求的生命周期
// req: 包含创建虚拟机所需参数的请求对象
// 返回值: 包含创建结果的工具结果对象或错误信息
func (c *CloudpodsServerCreateTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取必填参数:虚拟机名称
name, err := req.RequireString("name")
if err != nil {
return nil, err
}
// 获取必填参数镜像ID
imageID, err := req.RequireString("image_id")
if err != nil {
return nil, err
}
// 获取必填参数网络ID
networkID, err := req.RequireString("network_id")
if err != nil {
return nil, err
}
// 获取必填参数CPU核心数并转换为整数
vcpuCountStr, err := req.RequireString("vcpu_count")
if err != nil {
return nil, err
}
vcpuCount, err := strconv.ParseInt(vcpuCountStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("无效的CPU核心数: %s", vcpuCountStr)
}
// 获取必填参数:内存大小并转换为整数
vmemSizeStr, err := req.RequireString("vmem_size")
if err != nil {
return nil, err
}
vmemSize, err := strconv.ParseInt(vmemSizeStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("无效的内存大小: %s", vmemSizeStr)
}
// 获取可选参数套餐ID
serverSkuID := req.GetString("serversku_id", "")
// 获取可选参数:磁盘大小,如果指定则转换为整数
diskSize := int64(0)
if diskSizeStr := req.GetString("disk_size", ""); diskSizeStr != "" {
if parsedSize, err := strconv.ParseInt(diskSizeStr, 10, 64); err == nil && parsedSize > 0 {
diskSize = parsedSize
}
}
// 获取可选参数:虚拟机密码,并验证长度
password := req.GetString("password", "")
if password != "" && (len(password) < 8 || len(password) > 30) {
return nil, fmt.Errorf("密码长度必须在8-30个字符之间")
}
// 获取可选参数创建数量默认为1
count := 1
if countStr := req.GetString("count", "1"); countStr != "1" {
if parsedCount, err := strconv.Atoi(countStr); err == nil && parsedCount > 0 {
count = parsedCount
}
}
// 获取可选参数是否自动启动默认为true
autoStart := true
if autoStartStr := req.GetString("auto_start", "true"); autoStartStr == "false" {
autoStart = false
}
// 获取可选参数是否开启删除保护默认为true
disableDelete := true
if disableDeleteStr := req.GetString("disable_delete", "true"); disableDeleteStr == "false" {
disableDelete = false
}
// 获取其他可选参数
billingType := req.GetString("billing_type", "")
duration := req.GetString("duration", "")
description := req.GetString("description", "")
hostname := req.GetString("hostname", "")
hypervisor := req.GetString("hypervisor", "")
secgroupID := req.GetString("secgroup_id", "")
userData := req.GetString("user_data", "")
keypairID := req.GetString("keypair_id", "")
projectID := req.GetString("project_id", "")
zoneID := req.GetString("zone_id", "")
regionID := req.GetString("region_id", "")
bootOrder := req.GetString("boot_order", "")
// 获取安全组ID列表并按逗号分割
var secgroups []string
if secgroupsStr := req.GetString("secgroups", ""); secgroupsStr != "" {
secgroups = strings.Split(secgroupsStr, ",")
}
// 解析元数据JSON字符串
metadata := make(map[string]string)
if metadataStr := req.GetString("metadata", ""); metadataStr != "" {
if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {
return nil, fmt.Errorf("无效的元数据JSON格式: %w", err)
}
}
// 解析数据盘配置JSON数组
var dataDisks []models.DiskConfig
if dataDisksStr := req.GetString("data_disks", ""); dataDisksStr != "" {
if err := json.Unmarshal([]byte(dataDisksStr), &dataDisks); err != nil {
return nil, fmt.Errorf("无效的数据盘配置JSON格式: %w", err)
}
}
// 构造创建虚拟机的请求对象
createRequest := models.CreateServerRequest{
Name: name,
VcpuCount: vcpuCount,
VmemSize: vmemSize,
ImageId: imageID,
DiskSize: diskSize,
NetworkId: networkID,
ServerskuId: serverSkuID,
Count: count,
Password: password,
AutoStart: autoStart,
BillingType: billingType,
Duration: duration,
Description: description,
Hostname: hostname,
Hypervisor: hypervisor,
Metadata: metadata,
SecgroupId: secgroupID,
Secgroups: secgroups,
UserData: userData,
KeypairId: keypairID,
ProjectId: projectID,
ZoneId: zoneID,
RegionId: regionID,
DisableDelete: disableDelete,
BootOrder: bootOrder,
DataDisks: dataDisks,
}
// 获取访问凭证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器创建虚拟机
response, err := c.adapter.CreateServer(ctx, createRequest, ak, sk)
if err != nil {
log.Errorf("Fail to create server: %s", err)
return nil, fmt.Errorf("fail to create server: %w", err)
}
// 格式化创建结果
formattedResult := c.formatCreateResult(response, &createRequest)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
// 返回格式化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
// 返回值: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsServerCreateTool) GetName() string {
return "cloudpods_create_server"
}
// formatCreateResult 格式化创建虚拟机的响应结果
// response: 原始的创建虚拟机响应数据
// request: 原始的创建虚拟机请求数据
// 返回值: 格式化后的结果,包含创建信息、结果详情和摘要
func (c *CloudpodsServerCreateTool) formatCreateResult(response *models.CreateServerResponse, request *models.CreateServerRequest) map[string]interface{} {
// 初始化格式化结果结构
formatted := map[string]interface{}{
// 创建请求的基本信息
"create_info": map[string]interface{}{
"name": request.Name,
"vcpu_count": request.VcpuCount,
"vmem_size": request.VmemSize,
"image_id": request.ImageId,
"disk_size": request.DiskSize,
"network_id": request.NetworkId,
"serversku_id": request.ServerskuId,
"count": request.Count,
"auto_start": request.AutoStart,
"billing_type": request.BillingType,
"duration": request.Duration,
"description": request.Description,
"hostname": request.Hostname,
"hypervisor": request.Hypervisor,
"secgroup_id": request.SecgroupId,
"keypair_id": request.KeypairId,
"project_id": request.ProjectId,
"zone_id": request.ZoneId,
"region_id": request.RegionId,
"disable_delete": request.DisableDelete,
"boot_order": request.BootOrder,
},
// 创建响应的结果信息
"result": map[string]interface{}{
"status": response.Status,
"message": response.Message,
"servers": make([]map[string]interface{}, 0, len(response.Data.Servers)),
},
}
// 遍历创建的虚拟机列表,构造每个虚拟机的详细信息
for _, server := range response.Data.Servers {
serverInfo := map[string]interface{}{
"id": server.ID,
"name": server.Name,
"status": server.Status,
"task_id": server.TaskID,
}
formatted["result"].(map[string]interface{})["servers"] = append(
formatted["result"].(map[string]interface{})["servers"].([]map[string]interface{}),
serverInfo,
)
}
// 构造摘要信息
formatted["summary"] = map[string]interface{}{
"requested_count": request.Count, // 请求创建的虚拟机数量
"created_count": len(response.Data.Servers), // 实际创建的虚拟机数量
"success": response.Status == 200, // 创建是否成功
}
return formatted
}

View File

@@ -1,366 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsServerMonitorTool 用于获取Cloudpods虚拟机监控信息
type CloudpodsServerMonitorTool struct {
// adapter 用于与 Cloudpods API 进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerMonitorTool 创建一个新的CloudpodsServerMonitorTool实例
// adapter: 用于与Cloudpods API交互的适配器
// 返回值: CloudpodsServerMonitorTool实例指针
func NewCloudpodsServerMonitorTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerMonitorTool {
return &CloudpodsServerMonitorTool{
adapter: adapter,
}
}
// GetTool 定义并返回获取虚拟机监控信息工具的元数据
// 该工具用于获取Cloudpods虚拟机的监控信息包括CPU、内存、磁盘、网络等指标
// server_id: 虚拟机ID (必填)
// start_time: 开始时间戳默认为1小时前
// end_time: 结束时间戳(秒),默认为当前时间
// metrics: 监控指标多个用逗号分隔例如cpu_usage,mem_usage,disk_usage,net_bps_rx,net_bps_tx
// ak: 用户登录cloudpods后获取的access key
// sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsServerMonitorTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_get_server_monitor",
mcp.WithDescription("获取Cloudpods虚拟机监控信息包括CPU、内存、磁盘、网络等指标"),
mcp.WithString("server_id", mcp.Required(), mcp.Description("虚拟机ID")),
mcp.WithString("start_time", mcp.Description("开始时间戳默认为1小时前")),
mcp.WithString("end_time", mcp.Description("结束时间戳(秒),默认为当前时间")),
mcp.WithString("metrics", mcp.Description("监控指标多个用逗号分隔例如cpu_usage,mem_usage,disk_usage,net_bps_rx,net_bps_tx")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理获取虚拟机监控信息的请求
// ctx: 控制生命周期的上下文
// req: 包含获取监控信息所需参数的请求对象
// 返回值: 包含监控信息的响应对象和可能的错误
func (c *CloudpodsServerMonitorTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取必填参数虚拟机ID
serverID, err := req.RequireString("server_id")
if err != nil {
return nil, err
}
// 设置默认时间范围结束时间为当前时间开始时间为1小时前
now := time.Now().Unix()
startTime := now - 3600
// 解析开始时间参数,如果指定则使用指定值
if startTimeStr := req.GetString("start_time", ""); startTimeStr != "" {
if parsedStartTime, err := strconv.ParseInt(startTimeStr, 10, 64); err == nil {
startTime = parsedStartTime
}
}
// 解析结束时间参数,如果指定则使用指定值
endTime := now
if endTimeStr := req.GetString("end_time", ""); endTimeStr != "" {
if parsedEndTime, err := strconv.ParseInt(endTimeStr, 10, 64); err == nil {
endTime = parsedEndTime
}
}
// 获取可选参数:监控指标
var metrics []string
if metricsStr := req.GetString("metrics", ""); metricsStr != "" {
metrics = strings.Split(metricsStr, ",")
for i, metric := range metrics {
metrics[i] = strings.TrimSpace(metric)
}
} else {
metrics = []string{"cpu_usage", "mem_usage", "disk_usage", "net_bps_rx", "net_bps_tx"}
}
// 获取ak和sk参数用于认证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器获取虚拟机监控信息
monitorResponse, err := c.adapter.GetServerMonitor(ctx, serverID, startTime, endTime, metrics, ak, sk)
if err != nil {
log.Errorf("Fail to get server monitor: %s", err)
return nil, fmt.Errorf("fail to get server monitor: %w", err)
}
// 格式化监控结果
formattedResult := c.formatMonitorResult(monitorResponse, serverID, startTime, endTime, metrics)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
// 返回格式化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
// 返回值: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsServerMonitorTool) GetName() string {
return "cloudpods_get_server_monitor"
}
// formatMonitorResult 格式化虚拟机监控信息的响应结果
// response: 原始监控响应数据
// serverID: 虚拟机ID
// startTime: 监控开始时间
// endTime: 监控结束时间
// requestedMetrics: 请求的监控指标
// 返回值: 包含监控信息的格式化结果
func (c *CloudpodsServerMonitorTool) formatMonitorResult(response *models.MonitorResponse, serverID string, startTime, endTime int64, requestedMetrics []string) map[string]interface{} {
// 初始化格式化结果结构
formatted := map[string]interface{}{
// 添加请求的基本信息
"query_info": map[string]interface{}{
"server_id": serverID,
"start_time": startTime,
"end_time": endTime,
"start_time_human": time.Unix(startTime, 0).Format("2006-01-02 15:04:05"),
"end_time_human": time.Unix(endTime, 0).Format("2006-01-02 15:04:05"),
"requested_metrics": requestedMetrics,
"duration_seconds": endTime - startTime,
},
"status": response.Status,
"metrics": make([]map[string]interface{}, 0, len(response.Data.Metrics)),
}
for _, metric := range response.Data.Metrics {
metricInfo := map[string]interface{}{
"metric": metric.Metric,
"unit": metric.Unit,
"data_points": len(metric.Values),
"values": make([]map[string]interface{}, 0, len(metric.Values)),
}
var totalValue float64
var minValue, maxValue float64
var latestValue float64
var latestTime int64
for i, value := range metric.Values {
valueInfo := map[string]interface{}{
"timestamp": value.Timestamp,
"time_human": time.Unix(value.Timestamp, 0).Format("2006-01-02 15:04:05"),
"value": value.Value,
}
metricInfo["values"] = append(metricInfo["values"].([]map[string]interface{}), valueInfo)
totalValue += value.Value
if i == 0 {
minValue = value.Value
maxValue = value.Value
} else {
if value.Value < minValue {
minValue = value.Value
}
if value.Value > maxValue {
maxValue = value.Value
}
}
if value.Timestamp > latestTime {
latestTime = value.Timestamp
latestValue = value.Value
}
}
if len(metric.Values) > 0 {
metricInfo["statistics"] = map[string]interface{}{
"min": minValue,
"max": maxValue,
"average": totalValue / float64(len(metric.Values)),
"latest": latestValue,
}
}
formatted["metrics"] = append(formatted["metrics"].([]map[string]interface{}), metricInfo)
}
formatted["summary"] = map[string]interface{}{
"total_metrics": len(response.Data.Metrics),
"query_successful": response.Status == 200,
"time_range_hours": float64(endTime-startTime) / 3600,
}
return formatted
}
// CloudpodsServerStatsTool 用于获取Cloudpods虚拟机实时统计信息
type CloudpodsServerStatsTool struct {
// adapter 用于与 Cloudpods API 进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerStatsTool 创建一个新的CloudpodsServerStatsTool实例
// adapter: 用于与Cloudpods API交互的适配器
// 返回值: CloudpodsServerStatsTool实例指针
func NewCloudpodsServerStatsTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerStatsTool {
return &CloudpodsServerStatsTool{
adapter: adapter,
}
}
// GetTool 定义并返回获取虚拟机统计信息工具的元数据
// 该工具用于获取Cloudpods虚拟机的实时统计信息包括CPU使用率、内存使用率、磁盘使用率和网络流量
// server_id: 虚拟机ID (必填)
// ak: 用户登录cloudpods后获取的access key
// sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsServerStatsTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_get_server_stats",
mcp.WithDescription("获取Cloudpods虚拟机实时统计信息包括CPU使用率、内存使用率、磁盘使用率和网络流量"),
mcp.WithString("server_id", mcp.Required(), mcp.Description("虚拟机ID")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理获取虚拟机统计信息的请求
// ctx: 控制生命周期的上下文
// req: 包含获取统计信息所需参数的请求对象
// 返回值: 包含统计信息的响应对象和可能的错误
func (c *CloudpodsServerStatsTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取必填参数虚拟机ID
serverID, err := req.RequireString("server_id")
if err != nil {
return nil, err
}
// 获取可选参数:访问凭证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器获取虚拟机统计信息
statsResponse, err := c.adapter.GetServerStats(ctx, serverID, ak, sk)
if err != nil {
log.Errorf("Fail to get server stats: %s", err)
return nil, fmt.Errorf("fail to get server stats: %w", err)
}
// 格式化统计结果
formattedResult := c.formatStatsResult(statsResponse, serverID)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
// 返回格式化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
// 返回值: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsServerStatsTool) GetName() string {
return "cloudpods_get_server_stats"
}
// formatStatsResult 格式化虚拟机统计信息的响应结果
// response: 原始统计响应数据
// serverID: 虚拟机ID
// 返回值: 包含统计信息的格式化结果
func (c *CloudpodsServerStatsTool) formatStatsResult(response *models.ServerStatsResponse, serverID string) map[string]interface{} {
// 初始化格式化结果结构
formatted := map[string]interface{}{
"server_id": serverID,
"status": response.Status,
// 添加统计信息
"stats": map[string]interface{}{
"cpu_usage": fmt.Sprintf("%.1f%%", response.Data.CPUUsage),
"memory_usage": fmt.Sprintf("%.1f%%", response.Data.MemUsage),
"disk_usage": fmt.Sprintf("%.1f%%", response.Data.DiskUsage),
"network": map[string]interface{}{
"receive_bps": response.Data.NetBpsRx,
"transmit_bps": response.Data.NetBpsTx,
"receive_mbps": fmt.Sprintf("%.2f Mbps", float64(response.Data.NetBpsRx)/(1024*1024)),
"transmit_mbps": fmt.Sprintf("%.2f Mbps", float64(response.Data.NetBpsTx)/(1024*1024)),
},
"updated_at": response.Data.UpdatedAt,
},
// 添加原始数据
"raw_data": map[string]interface{}{
"cpu_usage": response.Data.CPUUsage,
"mem_usage": response.Data.MemUsage,
"disk_usage": response.Data.DiskUsage,
"net_bps_rx": response.Data.NetBpsRx,
"net_bps_tx": response.Data.NetBpsTx,
},
}
// 评估虚拟机健康状态
var healthStatus string
var healthScore int
if response.Data.CPUUsage > 90 || response.Data.MemUsage > 90 || response.Data.DiskUsage > 90 {
healthStatus = "警告"
healthScore = 1
} else if response.Data.CPUUsage > 70 || response.Data.MemUsage > 70 || response.Data.DiskUsage > 80 {
healthStatus = "注意"
healthScore = 2
} else {
healthStatus = "正常"
healthScore = 3
}
// 添加健康状态信息
formatted["health"] = map[string]interface{}{
"status": healthStatus,
"score": healthScore,
"notes": []string{},
}
// 添加健康状态建议
notes := []string{}
if response.Data.CPUUsage > 90 {
notes = append(notes, "CPU使用率过高建议检查系统负载")
}
if response.Data.MemUsage > 90 {
notes = append(notes, "内存使用率过高,建议释放内存或增加内存")
}
if response.Data.DiskUsage > 90 {
notes = append(notes, "磁盘使用率过高,建议清理磁盘空间")
}
formatted["health"].(map[string]interface{})["notes"] = notes
return formatted
}

View File

@@ -1,524 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsServerStartTool 用于启动指定的Cloudpods虚拟机实例
type CloudpodsServerStartTool struct {
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerStartTool 创建一个新的CloudpodsServerStartTool实例
func NewCloudpodsServerStartTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerStartTool {
return &CloudpodsServerStartTool{
adapter: adapter,
}
}
// GetTool 返回启动虚拟机工具的定义,包括参数和描述
func (c *CloudpodsServerStartTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_start_server",
mcp.WithDescription("启动指定的Cloudpods虚拟机实例。用户要求开机/启动时必须调用本工具真正执行启动仅调用cloudpods_list_servers 查询不算完成。若尚不知 server_id先用 cloudpods_list_servers可用 search定位拿到 id 后立即调用本工具,不要只查询就结束。"),
mcp.WithString("server_id", mcp.Required(), mcp.Description("虚拟机ID来自 cloudpods_list_servers 返回的 id 字段")),
mcp.WithString("auto_prepaid", mcp.Description("按量机器自动转换为包年包月默认为false")),
mcp.WithString("qemu_version", mcp.Description("指定启动虚拟机的Qemu版本可选值2.12.1, 4.2.0仅适用于KVM虚拟机")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理启动虚拟机的请求,调用适配器执行启动操作并返回结果
func (c *CloudpodsServerStartTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 从请求中获取必需的 server_id 参数
serverID, err := req.RequireString("server_id")
if err != nil {
return nil, err
}
// 解析 auto_prepaid 参数,决定是否自动转换为包年包月
autoPrepaid := false
if autoPrepaidStr := req.GetString("auto_prepaid", "false"); autoPrepaidStr == "true" {
autoPrepaid = true
}
// 获取 qemu_version 参数,用于指定启动虚拟机的 Qemu 版本
qemuVersion := req.GetString("qemu_version", "")
// 构造启动虚拟机的请求参数
startReq := models.ServerStartRequest{
AutoPrepaid: autoPrepaid,
QemuVersion: qemuVersion,
}
// 获取认证所需的 access key 和 secret key
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器的 StartServer 方法执行启动操作
response, err := c.adapter.StartServer(ctx, serverID, startReq, ak, sk)
if err != nil {
log.Errorf("Fail to start server: %s", err)
return nil, fmt.Errorf("fail to start server: %w", err)
}
// 构造返回结果包含任务ID、成功状态和状态信息
result := map[string]interface{}{
"server_id": serverID,
"operation": "start",
"task_id": response.TaskId,
"success": response.Success,
"status": response.Status,
}
// 如果有错误信息,则添加到结果中
if response.Error != "" {
result["error"] = response.Error
}
// 将结果序列化为 JSON 格式
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %w", err)
}
// 返回序列化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回启动虚拟机工具的名称
func (c *CloudpodsServerStartTool) GetName() string {
return "cloudpods_start_server"
}
// CloudpodsServerStopTool 用于停止指定的Cloudpods虚拟机实例
type CloudpodsServerStopTool struct {
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerStopTool 创建一个新的CloudpodsServerStopTool实例
func NewCloudpodsServerStopTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerStopTool {
return &CloudpodsServerStopTool{
adapter: adapter,
}
}
// GetTool 返回停止虚拟机工具的定义,包括参数和描述
func (c *CloudpodsServerStopTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_stop_server",
mcp.WithDescription("停止指定的Cloudpods虚拟机实例。用户要求关机/停止时必须调用本工具真正执行停止,仅调用 cloudpods_list_servers 查询不算完成。若尚不知 server_id先用 cloudpods_list_servers可用 search定位拿到 id 后立即调用本工具,不要只查询就结束,也不要再次向用户确认。"),
mcp.WithString("server_id", mcp.Required(), mcp.Description("虚拟机ID来自 cloudpods_list_servers 返回的 id 字段")),
mcp.WithString("is_force", mcp.Description("是否强制停止默认为false")),
mcp.WithString("stop_charging", mcp.Description("是否关机停止计费默认为false")),
mcp.WithString("timeout_secs", mcp.Description("关机等待时间如果是强制关机则等待时间为0如果不设置默认为30秒")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理停止虚拟机的请求,调用适配器执行停止操作并返回结果
func (c *CloudpodsServerStopTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 从请求中获取必需的 server_id 参数
serverID, err := req.RequireString("server_id")
if err != nil {
return nil, err
}
// 解析 is_force 参数,决定是否强制停止虚拟机
isForce := false
if isForceStr := req.GetString("is_force", "false"); isForceStr == "true" {
isForce = true
}
// 解析 stop_charging 参数,决定是否停止计费
stopCharging := false
if stopChargingStr := req.GetString("stop_charging", "false"); stopChargingStr == "true" {
stopCharging = true
}
// 解析 timeout_secs 参数,设置停止操作的超时时间
timeoutSecs := int64(0)
if timeoutSecsStr := req.GetString("timeout_secs", ""); timeoutSecsStr != "" {
if parsed, err := strconv.ParseInt(timeoutSecsStr, 10, 64); err == nil && parsed > 0 {
timeoutSecs = parsed
}
}
// 构造停止虚拟机的请求参数
stopReq := models.ServerStopRequest{
IsForce: isForce,
StopCharging: stopCharging,
TimeoutSecs: timeoutSecs,
}
// 获取认证所需的 access key 和 secret key
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器的 StopServer 方法执行停止操作
response, err := c.adapter.StopServer(ctx, serverID, stopReq, ak, sk)
if err != nil {
log.Errorf("Fail to stop server: %s", err)
return nil, fmt.Errorf("fail to stop server: %w", err)
}
// 构造返回结果包含任务ID、成功状态和状态信息
result := map[string]interface{}{
"server_id": serverID,
"operation": "stop",
"task_id": response.TaskId,
"success": response.Success,
"status": response.Status,
}
// 如果有错误信息,则添加到结果中
if response.Error != "" {
result["error"] = response.Error
}
// 将结果序列化为 JSON 格式
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %w", err)
}
// 返回序列化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回停止虚拟机工具的名称
func (c *CloudpodsServerStopTool) GetName() string {
return "cloudpods_stop_server"
}
// CloudpodsServerRestartTool 用于重启指定的Cloudpods虚拟机实例
type CloudpodsServerRestartTool struct {
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerRestartTool 创建一个新的CloudpodsServerRestartTool实例
func NewCloudpodsServerRestartTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerRestartTool {
return &CloudpodsServerRestartTool{
adapter: adapter,
}
}
// GetTool 返回重启虚拟机工具的定义,包括参数和描述
func (c *CloudpodsServerRestartTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_restart_server",
mcp.WithDescription("重启指定的Cloudpods虚拟机实例。用户要求重启时必须调用本工具真正执行重启仅调用 cloudpods_list_servers 查询不算完成。若尚不知 server_id先用 cloudpods_list_servers可用 search定位拿到 id 后立即调用本工具,不要只查询就结束,也不要再次向用户确认。"),
mcp.WithString("server_id", mcp.Required(), mcp.Description("虚拟机ID来自 cloudpods_list_servers 返回的 id 字段")),
mcp.WithString("is_force", mcp.Description("是否强制重启默认为false")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理重启虚拟机的请求,调用适配器执行重启操作并返回结果
func (c *CloudpodsServerRestartTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 从请求中获取必需的 server_id 参数
serverID, err := req.RequireString("server_id")
if err != nil {
return nil, err
}
// 解析 is_force 参数,决定是否强制重启虚拟机
isForce := false
if isForceStr := req.GetString("is_force", "false"); isForceStr == "true" {
isForce = true
}
// 构造重启虚拟机的请求参数
restartReq := models.ServerRestartRequest{
IsForce: isForce,
}
// 获取认证所需的 access key 和 secret key
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器的 RestartServer 方法执行重启操作
response, err := c.adapter.RestartServer(ctx, serverID, restartReq, ak, sk)
if err != nil {
log.Errorf("Fail to query restart server: %s", err)
return nil, fmt.Errorf("fail to restart server: %w", err)
}
// 构造返回结果包含任务ID、成功状态和状态信息
result := map[string]interface{}{
"server_id": serverID,
"operation": "restart",
"task_id": response.TaskId,
"success": response.Success,
"status": response.Status,
}
// 如果有错误信息,则添加到结果中
if response.Error != "" {
result["error"] = response.Error
}
// 将结果序列化为 JSON 格式
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %w", err)
}
// 返回序列化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回重启虚拟机工具的名称
func (c *CloudpodsServerRestartTool) GetName() string {
return "cloudpods_restart_server"
}
// CloudpodsServerResetPasswordTool 用于重置指定Cloudpods虚拟机的登录密码
type CloudpodsServerResetPasswordTool struct {
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerResetPasswordTool 创建一个新的CloudpodsServerResetPasswordTool实例
func NewCloudpodsServerResetPasswordTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerResetPasswordTool {
return &CloudpodsServerResetPasswordTool{
adapter: adapter,
}
}
// GetTool 返回重置虚拟机密码工具的定义,包括参数和描述
func (c *CloudpodsServerResetPasswordTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_reset_server_password",
mcp.WithDescription("重置指定Cloudpods虚拟机的登录密码。用户要求重置密码时必须调用本工具真正执行仅调用 cloudpods_list_servers 查询不算完成。若尚不知 server_id先用 cloudpods_list_servers 定位,拿到 id 后立即调用本工具。"),
mcp.WithString("server_id", mcp.Required(), mcp.Description("虚拟机ID来自 cloudpods_list_servers 返回的 id 字段")),
mcp.WithString("password", mcp.Required(), mcp.Description("新密码长度8-30个字符")),
mcp.WithString("reset_password", mcp.Description("是否重置密码默认为true")),
mcp.WithString("auto_start", mcp.Description("重置后是否自动启动默认为true")),
mcp.WithString("username", mcp.Description("用户名,可选,默认为空")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理重置虚拟机密码的请求,调用适配器执行密码重置操作并返回结果
func (c *CloudpodsServerResetPasswordTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 从请求中获取必需的 server_id 参数
serverID, err := req.RequireString("server_id")
if err != nil {
return nil, err
}
// 从请求中获取必需的 password 参数,并验证其长度
password, err := req.RequireString("password")
if err != nil {
return nil, err
}
if len(password) < 8 || len(password) > 30 {
return nil, fmt.Errorf("密码长度必须在8-30个字符之间")
}
// 解析 reset_password 参数,决定是否重置密码
resetPassword := true
if resetPasswordStr := req.GetString("reset_password", "true"); resetPasswordStr == "false" {
resetPassword = false
}
// 解析 auto_start 参数,决定重置密码后是否自动启动虚拟机
autoStart := true
if autoStartStr := req.GetString("auto_start", "true"); autoStartStr == "false" {
autoStart = false
}
// 获取 username 参数,可选
username := req.GetString("username", "")
// 构造重置虚拟机密码的请求参数
resetPasswordReq := models.ServerResetPasswordRequest{
Password: password,
ResetPassword: resetPassword,
AutoStart: autoStart,
Username: username,
}
// 获取认证所需的 access key 和 secret key
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器的 ResetServerPassword 方法执行密码重置操作
response, err := c.adapter.ResetServerPassword(ctx, serverID, resetPasswordReq, ak, sk)
if err != nil {
log.Errorf("Fail to reset server password: %s", err)
return nil, fmt.Errorf("fail to reset server password: %w", err)
}
// 构造返回结果包含任务ID、成功状态和状态信息
result := map[string]interface{}{
"server_id": serverID,
"operation": "reset-password",
"task_id": response.TaskId,
"success": response.Success,
"status": response.Status,
}
// 如果有错误信息,则添加到结果中
if response.Error != "" {
result["error"] = response.Error
}
// 将结果序列化为 JSON 格式
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %w", err)
}
// 返回序列化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回重置虚拟机密码工具的名称
func (c *CloudpodsServerResetPasswordTool) GetName() string {
return "cloudpods_reset_server_password"
}
// CloudpodsServerDeleteTool 用于删除指定的Cloudpods虚拟机实例
type CloudpodsServerDeleteTool struct {
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerDeleteTool 创建一个新的CloudpodsServerDeleteTool实例
func NewCloudpodsServerDeleteTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerDeleteTool {
return &CloudpodsServerDeleteTool{
adapter: adapter,
}
}
// GetTool 返回删除虚拟机工具的定义,包括参数和描述
func (c *CloudpodsServerDeleteTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_delete_server",
mcp.WithDescription("删除指定的Cloudpods虚拟机实例。用户要求删除时必须调用本工具真正执行删除仅调用 cloudpods_list_servers 查询不算完成。若尚不知 server_id先用 cloudpods_list_servers 定位,拿到 id 后立即调用本工具。"),
mcp.WithString("server_id", mcp.Required(), mcp.Description("虚拟机ID来自 cloudpods_list_servers 返回的 id 字段")),
mcp.WithString("override_pending_delete", mcp.Description("是否强制删除包括在回收站中的实例默认为false")),
mcp.WithString("purge", mcp.Description("是否仅删除本地资源默认为false")),
mcp.WithString("delete_snapshots", mcp.Description("是否删除快照默认为false")),
mcp.WithString("delete_eip", mcp.Description("是否删除关联的EIP默认为false")),
mcp.WithString("delete_disks", mcp.Description("是否删除关联的数据盘默认为false")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理删除虚拟机的请求,调用适配器执行删除操作并返回结果
func (c *CloudpodsServerDeleteTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 从请求中获取必需的 server_id 参数
serverID, err := req.RequireString("server_id")
if err != nil {
return nil, err
}
// 解析 override_pending_delete 参数,决定是否强制删除(包括在回收站中的实例)
overridePendingDelete := false
if overrideStr := req.GetString("override_pending_delete", "false"); overrideStr == "true" {
overridePendingDelete = true
}
// 解析 purge 参数,决定是否仅删除本地资源
purge := false
if purgeStr := req.GetString("purge", "false"); purgeStr == "true" {
purge = true
}
// 解析 delete_snapshots 参数,决定是否删除快照
deleteSnapshots := false
if deleteSnapshotsStr := req.GetString("delete_snapshots", "false"); deleteSnapshotsStr == "true" {
deleteSnapshots = true
}
// 解析 delete_eip 参数决定是否删除关联的EIP
deleteEip := false
if deleteEipStr := req.GetString("delete_eip", "false"); deleteEipStr == "true" {
deleteEip = true
}
// 解析 delete_disks 参数,决定是否删除关联的数据盘
deleteDisks := false
if deleteDisksStr := req.GetString("delete_disks", "false"); deleteDisksStr == "true" {
deleteDisks = true
}
// 构造删除虚拟机的请求参数
deleteReq := models.ServerDeleteRequest{
OverridePendingDelete: overridePendingDelete,
Purge: purge,
DeleteSnapshots: deleteSnapshots,
DeleteEip: deleteEip,
DeleteDisks: deleteDisks,
}
// 获取认证所需的 access key 和 secret key
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器的 DeleteServer 方法执行删除操作
response, err := c.adapter.DeleteServer(ctx, serverID, deleteReq, ak, sk)
if err != nil {
log.Errorf("Fail to delete server: %s", err)
return nil, fmt.Errorf("fail to delete server: %w", err)
}
// 构造返回结果包含任务ID、成功状态和状态信息
result := map[string]interface{}{
"server_id": serverID,
"operation": "delete",
"task_id": response.TaskId,
"success": response.Success,
"status": response.Status,
}
// 如果有错误信息,则添加到结果中
if response.Error != "" {
result["error"] = response.Error
}
// 将结果序列化为 JSON 格式
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %w", err)
}
// 返回序列化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回删除虚拟机工具的名称
func (c *CloudpodsServerDeleteTool) GetName() string {
return "cloudpods_delete_server"
}

View File

@@ -1,176 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsServersTool 是用于查询 Cloudpods 虚拟机实例列表的工具
type CloudpodsServersTool struct {
// adapter 用于与 Cloudpods API 进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServersTool 创建一个新的 Cloudpods 虚拟机查询工具
// adapter: 用于与Cloudpods API交互的适配器
// 返回值: CloudpodsServersTool实例指针
func NewCloudpodsServersTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServersTool {
return &CloudpodsServersTool{
adapter: adapter,
}
}
// GetTool 定义并返回查询虚拟机实例列表工具的元数据
// 该工具用于查询Cloudpods虚拟机实例列表获取虚拟机信息
// limit: 返回结果数量限制默认为50
// offset: 结果偏移量默认为0
// search: 按名称或ID模糊搜索
// status: 虚拟机状态例如running、stopped、creating等
// ak: 用户登录cloudpods后获取的access key
// sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsServersTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_list_servers",
mcp.WithDescription("查询Cloudpods虚拟机实例列表获取虚拟机信息含id、name、status。当用户要求启动/停止/重启/删除/重置密码时先用本工具定位目标虚拟机拿到返回结果中的id后必须立刻继续调用对应操作工具cloudpods_start_server / cloudpods_stop_server / cloudpods_restart_server / cloudpods_delete_server / cloudpods_reset_server_password完成操作不要只查询就结束也不要再次向用户确认除非匹配到多台需用户选择。"),
mcp.WithString("limit", mcp.Description("返回结果数量限制默认为50")),
mcp.WithString("offset", mcp.Description("结果偏移量默认为0")),
mcp.WithString("search", mcp.Description("按名称或ID模糊搜索")),
mcp.WithString("status", mcp.Description("虚拟机状态例如running、stopped、creating等")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理查询 Cloudpods 虚拟机实例列表的请求
// ctx: 控制生命周期的上下文
// req: 包含查询参数的请求对象
// 返回值: 包含虚拟机列表的响应对象和可能的错误
func (c *CloudpodsServersTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取可选参数:返回结果数量限制,如果指定则转换为整数
limit := 50
if limitStr := req.GetString("limit", ""); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
// 获取可选参数:结果偏移量,如果指定则转换为整数
offset := 0
if offsetStr := req.GetString("offset", ""); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// 获取可选参数:搜索关键词和虚拟机状态
search := req.GetString("search", "")
status := req.GetString("status", "")
// 获取可选参数:访问凭证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器查询虚拟机列表
serversResponse, err := c.adapter.ListServers(ctx, limit, offset, search, status, ak, sk)
if err != nil {
log.Errorf("Fail to query server: %s", err)
return nil, fmt.Errorf("fail to query server: %w", err)
}
// 格式化查询结果
formattedResult := c.formatServersResult(serversResponse, limit, offset, search, status)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
// 返回格式化后的结果
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
// 返回值: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsServersTool) GetName() string {
return "cloudpods_list_servers"
}
// formatServersResult 格式化虚拟机实例列表查询结果
// response: 原始虚拟机列表响应数据
// limit: 查询限制数量
// offset: 查询偏移量
// search: 搜索关键词
// status: 虚拟机状态
// 返回值: 包含虚拟机列表的格式化结果
func (c *CloudpodsServersTool) formatServersResult(response *models.ServerListResponse, limit int, offset int, search string, status string) map[string]interface{} {
// 初始化格式化结果结构
formatted := map[string]interface{}{
// 添加查询信息
"query_info": map[string]interface{}{
"limit": limit,
"offset": offset,
"search": search,
"status": status,
"total": response.Total,
"count": len(response.Servers),
},
// 初始化虚拟机列表
"servers": make([]map[string]interface{}, 0, len(response.Servers)),
}
// 遍历虚拟机列表,构造每个虚拟机的详细信息
for _, server := range response.Servers {
// 将内存大小从MB转换为GB
memoryGB := float64(server.VmemSize) / 1024
// 构造虚拟机信息
serverInfo := map[string]interface{}{
"id": server.Id,
"name": server.Name,
"status": server.Status,
"vcpu_count": server.VcpuCount,
"vmem_size": server.VmemSize,
"memory_gb": fmt.Sprintf("%.1f GB", memoryGB),
"os_name": server.OsName,
"ips": server.Ips,
"host": server.Host,
"zone": server.Zone,
"region": server.Cloudregion,
"created_at": server.CreatedAt,
}
formatted["servers"] = append(formatted["servers"].([]map[string]interface{}), serverInfo)
}
// 构造摘要信息
formatted["summary"] = map[string]interface{}{
"total_servers": response.Total,
"returned_count": len(response.Servers),
}
return formatted
}

View File

@@ -1,310 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsServerSkusTool 用于查询Cloudpods主机套餐规格列表的工具
type CloudpodsServerSkusTool struct {
// adapter 用于与Cloudpods API进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsServerSkusTool 创建一个新的CloudpodsServerSkusTool实例
//
// 参数:
// - adapter: 用于与Cloudpods API交互的适配器
//
// 返回值:
// - *CloudpodsServerSkusTool: CloudpodsServerSkusTool实例指针
func NewCloudpodsServerSkusTool(adapter *adapters.CloudpodsAdapter) *CloudpodsServerSkusTool {
return &CloudpodsServerSkusTool{
adapter: adapter,
}
}
// GetTool 定义并返回查询主机套餐规格列表工具的元数据
//
// 工具用途:
//
// 查询Cloudpods主机套餐规格列表获取虚拟机规格信息
//
// 参数说明:
// - limit: 返回结果数量限制默认为20
// - offset: 返回结果偏移量默认为0
// - search: 搜索关键词,可以按规格名称搜索
// - cloudregion_ids: 云区域ID多个用逗号分隔
// - zone_ids: 可用区ID多个用逗号分隔
// - cpu_core_count: CPU核心数多个用逗号分隔1,2,4,8
// - memory_size_mb: 内存大小MB多个用逗号分隔1024,2048,4096
// - providers: 云平台提供商多个用逗号分隔OneCloud,Aliyun,Huawei
// - cpu_arch: CPU架构多个用逗号分隔x86,arm
// - ak: 用户登录cloudpods后获取的access key
// - sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsServerSkusTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_list_serverskus",
mcp.WithDescription("查询Cloudpods主机套餐规格列表获取虚拟机规格信息"),
mcp.WithString("limit", mcp.Description("返回结果数量限制默认为20")),
mcp.WithString("offset", mcp.Description("返回结果偏移量默认为0")),
mcp.WithString("search", mcp.Description("搜索关键词,可以按规格名称搜索")),
mcp.WithString("cloudregion_ids", mcp.Description("云区域ID多个用逗号分隔")),
mcp.WithString("zone_ids", mcp.Description("可用区ID多个用逗号分隔")),
mcp.WithString("cpu_core_count", mcp.Description("CPU核心数多个用逗号分隔1,2,4,8")),
mcp.WithString("memory_size_mb", mcp.Description("内存大小MB多个用逗号分隔1024,2048,4096")),
mcp.WithString("providers", mcp.Description("云平台提供商多个用逗号分隔OneCloud,Aliyun,Huawei")),
mcp.WithString("cpu_arch", mcp.Description("CPU架构多个用逗号分隔x86,arm")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理查询主机套餐规格列表的请求
//
// 参数:
// - ctx: 控制生命周期的上下文
// - req: 包含查询参数的请求对象
//
// 返回值:
// - *mcp.CallToolResult: 包含主机套餐规格列表的响应对象
// - error: 可能的错误信息
func (c *CloudpodsServerSkusTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取可选参数:返回结果数量限制,如果指定则转换为整数
limit := 20
if limitStr := req.GetString("limit", ""); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
// 获取可选参数:结果偏移量,如果指定则转换为整数
offset := 0
if offsetStr := req.GetString("offset", ""); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// 获取可选参数:搜索关键词
search := req.GetString("search", "")
// 获取可选参数云区域ID列表
var cloudregionIds []string
if cloudregionIdsStr := req.GetString("cloudregion_ids", ""); cloudregionIdsStr != "" {
cloudregionIds = strings.Split(cloudregionIdsStr, ",")
for i, id := range cloudregionIds {
cloudregionIds[i] = strings.TrimSpace(id)
}
}
// 获取可选参数可用区ID列表
var zoneIds []string
if zoneIdsStr := req.GetString("zone_ids", ""); zoneIdsStr != "" {
zoneIds = strings.Split(zoneIdsStr, ",")
for i, id := range zoneIds {
zoneIds[i] = strings.TrimSpace(id)
}
}
// 获取可选参数CPU核心数列表
var cpuCoreCount []string
if cpuCoreCountStr := req.GetString("cpu_core_count", ""); cpuCoreCountStr != "" {
cpuCoreCount = strings.Split(cpuCoreCountStr, ",")
for i, count := range cpuCoreCount {
cpuCoreCount[i] = strings.TrimSpace(count)
}
}
// 获取可选参数内存大小列表MB
var memorySizeMB []string
if memorySizeMBStr := req.GetString("memory_size_mb", ""); memorySizeMBStr != "" {
memorySizeMB = strings.Split(memorySizeMBStr, ",")
for i, size := range memorySizeMB {
memorySizeMB[i] = strings.TrimSpace(size)
}
}
// 获取可选参数:云平台提供商列表
var providers []string
if providersStr := req.GetString("providers", ""); providersStr != "" {
providers = strings.Split(providersStr, ",")
for i, provider := range providers {
providers[i] = strings.TrimSpace(provider)
}
}
// 获取可选参数CPU架构列表
var cpuArch []string
if cpuArchStr := req.GetString("cpu_arch", ""); cpuArchStr != "" {
cpuArch = strings.Split(cpuArchStr, ",")
for i, arch := range cpuArch {
cpuArch[i] = strings.TrimSpace(arch)
}
}
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器查询主机套餐规格列表
skusResponse, err := c.adapter.ListServerSkus(ctx, limit, offset, search, cloudregionIds, zoneIds, cpuCoreCount, memorySizeMB, providers, cpuArch, ak, sk)
if err != nil {
log.Errorf("Fail to query server skus: %s", err)
return nil, fmt.Errorf("fail to query server skus: %w", err)
}
// 格式化查询结果
formattedResult := c.formatServerSkusResult(skusResponse, limit, offset, search, cloudregionIds, zoneIds, cpuCoreCount, memorySizeMB, providers, cpuArch)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
//
// 返回值:
// - string: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsServerSkusTool) GetName() string {
return "cloudpods_list_serverskus"
}
// formatServerSkusResult 格式化主机套餐规格列表的响应结果
//
// 参数:
// - response: 原始主机套餐规格列表响应数据
// - limit: 查询限制数量
// - offset: 查询偏移量
// - search: 搜索关键词
// - cloudregionIds: 云区域ID列表
// - zoneIds: 可用区ID列表
// - cpuCoreCount: CPU核心数列表
// - memorySizeMB: 内存大小列表MB
// - providers: 云平台提供商列表
// - cpuArch: CPU架构列表
//
// 返回值:
// - map[string]interface{}: 包含主机套餐规格列表的格式化结果
func (c *CloudpodsServerSkusTool) formatServerSkusResult(
response *models.ServerSkuListResponse,
limit, offset int,
search string,
cloudregionIds, zoneIds, cpuCoreCount, memorySizeMB, providers, cpuArch []string,
) map[string]interface{} {
// 初始化格式化结果结构
formatted := map[string]interface{}{
"query_info": map[string]interface{}{
"limit": limit,
"offset": offset,
"search": search,
"cloudregion_ids": cloudregionIds,
"zone_ids": zoneIds,
"cpu_core_count": cpuCoreCount,
"memory_size_mb": memorySizeMB,
"providers": providers,
"cpu_arch": cpuArch,
"total": response.Total,
"count": len(response.Serverskus),
},
"serverskus": make([]map[string]interface{}, 0, len(response.Serverskus)),
}
// 遍历主机套餐列表,构造每个主机套餐的详细信息
for _, sku := range response.Serverskus {
skuInfo := map[string]interface{}{
"id": sku.Id,
"name": sku.Name,
"description": sku.Description,
"status": sku.Status,
"enabled": sku.Enabled,
"provider": sku.Provider,
"cloud_env": sku.CloudEnv,
"cloudregion": sku.Cloudregion,
"cloudregion_id": sku.CloudregionId,
"zone": sku.Zone,
"zone_id": sku.ZoneId,
"zone_ext_id": sku.ZoneExtId,
"cpu_core_count": sku.CpuCoreCount,
"memory_size_mb": sku.MemorySizeMB,
"cpu_arch": sku.CpuArch,
"instance_type_family": sku.InstanceTypeFamily,
"instance_type_category": sku.InstanceTypeCategory,
"local_category": sku.LocalCategory,
"sys_disk_type": sku.SysDiskType,
"sys_disk_min_size_gb": sku.SysDiskMinSizeGB,
"sys_disk_max_size_gb": sku.SysDiskMaxSizeGB,
"sys_disk_resizable": sku.SysDiskResizable,
"data_disk_types": sku.DataDiskTypes,
"data_disk_max_count": sku.DataDiskMaxCount,
"attached_disk_count": sku.AttachedDiskCount,
"attached_disk_size_gb": sku.AttachedDiskSizeGB,
"attached_disk_type": sku.AttachedDiskType,
"nic_type": sku.NicType,
"nic_max_count": sku.NicMaxCount,
"gpu_attachable": sku.GpuAttachable,
"gpu_count": sku.GpuCount,
"gpu_max_count": sku.GpuMaxCount,
"gpu_spec": sku.GpuSpec,
"os_name": sku.OsName,
"postpaid_status": sku.PostpaidStatus,
"prepaid_status": sku.PrepaidStatus,
"total_guest_count": sku.TotalGuestCount,
"external_id": sku.ExternalId,
"source": sku.Source,
"is_emulated": sku.IsEmulated,
"region": sku.Region,
"region_id": sku.RegionId,
"region_ext_id": sku.RegionExtId,
"region_external_id": sku.RegionExternalId,
"md5": sku.Md5,
"metadata": sku.Metadata,
"progress": sku.Progress,
"can_delete": sku.CanDelete,
"can_update": sku.CanUpdate,
"update_version": sku.UpdateVersion,
"created_at": sku.CreatedAt,
"updated_at": sku.UpdatedAt,
"imported_at": sku.ImportedAt,
}
formatted["serverskus"] = append(formatted["serverskus"].([]map[string]interface{}), skuInfo)
}
// 构造摘要信息
formatted["summary"] = map[string]interface{}{
"total_serverskus": response.Total,
"returned_count": len(response.Serverskus),
"has_more": response.Total > int64(offset+len(response.Serverskus)),
"next_offset": offset + len(response.Serverskus),
}
return formatted
}

View File

@@ -1,319 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsStoragesTool 用于查询Cloudpods块存储列表的工具
type CloudpodsStoragesTool struct {
// adapter 用于与Cloudpods API进行交互
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsStoragesTool 创建一个新的CloudpodsStoragesTool实例
//
// 参数:
// - adapter: 用于与Cloudpods API交互的适配器
//
// 返回值:
// - *CloudpodsStoragesTool: CloudpodsStoragesTool实例指针
func NewCloudpodsStoragesTool(adapter *adapters.CloudpodsAdapter) *CloudpodsStoragesTool {
return &CloudpodsStoragesTool{
adapter: adapter,
}
}
// GetTool 定义并返回查询块存储列表工具的元数据
//
// 工具用途:
//
// 查询Cloudpods块存储列表获取存储资源信息
//
// 参数说明:
// - limit: 返回结果数量限制默认为20
// - offset: 返回结果偏移量默认为0
// - search: 搜索关键词,可以按存储名称搜索
// - cloudregion_ids: 云区域ID多个用逗号分隔
// - zone_ids: 可用区ID多个用逗号分隔
// - providers: 云平台提供商多个用逗号分隔OneCloud,Aliyun,Huawei
// - storage_types: 存储类型多个用逗号分隔local,rbd,nfs,cephfs
// - host_id: 主机ID过滤关联指定主机的存储
// - ak: 用户登录cloudpods后获取的access key
// - sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsStoragesTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_list_storages",
mcp.WithDescription("查询Cloudpods块存储列表获取存储资源信息"),
mcp.WithString("limit", mcp.Description("返回结果数量限制默认为20")),
mcp.WithString("offset", mcp.Description("返回结果偏移量默认为0")),
mcp.WithString("search", mcp.Description("搜索关键词,可以按存储名称搜索")),
mcp.WithString("cloudregion_ids", mcp.Description("云区域ID多个用逗号分隔")),
mcp.WithString("zone_ids", mcp.Description("可用区ID多个用逗号分隔")),
mcp.WithString("providers", mcp.Description("云平台提供商多个用逗号分隔OneCloud,Aliyun,Huawei")),
mcp.WithString("storage_types", mcp.Description("存储类型多个用逗号分隔local,rbd,nfs,cephfs")),
mcp.WithString("host_id", mcp.Description("主机ID过滤关联指定主机的存储")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理查询块存储列表的请求
//
// 参数:
// - ctx: 控制生命周期的上下文
// - req: 包含查询参数的请求对象
//
// 返回值:
// - *mcp.CallToolResult: 包含块存储列表的响应对象
// - error: 可能的错误信息
func (c *CloudpodsStoragesTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取可选参数:返回结果数量限制,如果指定则转换为整数
limit := 20
if limitStr := req.GetString("limit", ""); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
// 获取可选参数:结果偏移量,如果指定则转换为整数
offset := 0
if offsetStr := req.GetString("offset", ""); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// 获取可选参数:搜索关键词
search := req.GetString("search", "")
// 获取可选参数云区域ID列表
var cloudregionIds []string
if cloudregionIdsStr := req.GetString("cloudregion_ids", ""); cloudregionIdsStr != "" {
cloudregionIds = strings.Split(cloudregionIdsStr, ",")
for i, id := range cloudregionIds {
cloudregionIds[i] = strings.TrimSpace(id)
}
}
// 获取可选参数可用区ID列表
var zoneIds []string
if zoneIdsStr := req.GetString("zone_ids", ""); zoneIdsStr != "" {
zoneIds = strings.Split(zoneIdsStr, ",")
for i, id := range zoneIds {
zoneIds[i] = strings.TrimSpace(id)
}
}
// 获取可选参数:云平台提供商列表
var providers []string
if providersStr := req.GetString("providers", ""); providersStr != "" {
providers = strings.Split(providersStr, ",")
for i, provider := range providers {
providers[i] = strings.TrimSpace(provider)
}
}
// 获取可选参数:存储类型列表
var storageTypes []string
if storageTypesStr := req.GetString("storage_types", ""); storageTypesStr != "" {
storageTypes = strings.Split(storageTypesStr, ",")
for i, storageType := range storageTypes {
storageTypes[i] = strings.TrimSpace(storageType)
}
}
// 获取可选参数主机ID
hostId := req.GetString("host_id", "")
// 获取可选参数:访问凭证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器查询块存储列表
storagesResponse, err := c.adapter.ListStorages(ctx, limit, offset, search, cloudregionIds, zoneIds, providers, storageTypes, hostId, ak, sk)
if err != nil {
log.Errorf("Fail to query storage: %s", err)
return nil, fmt.Errorf("fail to query storage: %w", err)
}
// 格式化查询结果
formattedResult := c.formatStoragesResult(storagesResponse, limit, offset, search, cloudregionIds, zoneIds, providers, storageTypes, hostId)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
//
// 返回值:
// - string: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsStoragesTool) GetName() string {
return "cloudpods_list_storages"
}
// formatStoragesResult 格式化块存储列表的响应结果
//
// 参数:
// - response: 原始响应数据
// - limit: 查询限制
// - offset: 查询偏移量
// - search: 搜索关键词
// - cloudregionIds: 云区域ID列表
// - zoneIds: 可用区ID列表
// - providers: 云平台提供商列表
// - storageTypes: 存储类型列表
// - hostId: 主机ID
//
// 返回值:
// - map[string]interface{}: 包含块存储列表的格式化结果
func (c *CloudpodsStoragesTool) formatStoragesResult(
response *models.StorageListResponse,
limit, offset int,
search string,
cloudregionIds, zoneIds, providers, storageTypes []string,
hostId string,
) map[string]interface{} {
// 初始化格式化结果结构
formatted := map[string]interface{}{
"query_info": map[string]interface{}{
"limit": limit,
"offset": offset,
"search": search,
"cloudregion_ids": cloudregionIds,
"zone_ids": zoneIds,
"providers": providers,
"storage_types": storageTypes,
"host_id": hostId,
"total": response.Total,
"count": len(response.Storages),
},
"storages": make([]map[string]interface{}, 0, len(response.Storages)),
}
// 遍历块存储列表,构造每个块存储的详细信息
for _, storage := range response.Storages {
capacityGB := float64(storage.Capacity) / 1024
usedCapacityGB := float64(storage.UsedCapacity) / 1024
freeCapacityGB := float64(storage.FreeCapacity) / 1024
actualUsedGB := float64(storage.ActualCapacityUsed) / 1024
storageInfo := map[string]interface{}{
"id": storage.Id,
"name": storage.Name,
"description": storage.Description,
"status": storage.Status,
"enabled": storage.Enabled,
"storage_type": storage.StorageType,
"medium_type": storage.MediumType,
"provider": storage.Provider,
"brand": storage.Brand,
"cloud_env": storage.CloudEnv,
"cloudregion": storage.Cloudregion,
"cloudregion_id": storage.CloudregionId,
"zone": storage.Zone,
"zone_id": storage.ZoneId,
"zone_ext_id": storage.ZoneExtId,
"capacity_mb": storage.Capacity,
"capacity_gb": fmt.Sprintf("%.2f GB", capacityGB),
"used_capacity_mb": storage.UsedCapacity,
"used_capacity_gb": fmt.Sprintf("%.2f GB", usedCapacityGB),
"free_capacity_mb": storage.FreeCapacity,
"free_capacity_gb": fmt.Sprintf("%.2f GB", freeCapacityGB),
"actual_capacity_used": storage.ActualCapacityUsed,
"actual_used_gb": fmt.Sprintf("%.2f GB", actualUsedGB),
"virtual_capacity": storage.VirtualCapacity,
"waste_capacity": storage.WasteCapacity,
"reserved": storage.Reserved,
"commit_bound": storage.CommitBound,
"commit_rate": storage.CommitRate,
"cmtbound": storage.Cmtbound,
"is_sys_disk_store": storage.IsSysDiskStore,
"is_public": storage.IsPublic,
"is_emulated": storage.IsEmulated,
"disk_count": storage.DiskCount,
"host_count": storage.HostCount,
"snapshot_count": storage.SnapshotCount,
"master_host": storage.MasterHost,
"master_host_name": storage.MasterHostName,
"storagecache_id": storage.StoragecacheId,
"account": storage.Account,
"account_id": storage.AccountId,
"account_status": storage.AccountStatus,
"account_health_status": storage.AccountHealthStatus,
"account_read_only": storage.AccountReadOnly,
"manager": storage.Manager,
"manager_id": storage.ManagerId,
"manager_domain": storage.ManagerDomain,
"manager_domain_id": storage.ManagerDomainId,
"manager_project": storage.ManagerProject,
"manager_project_id": storage.ManagerProjectId,
"external_id": storage.ExternalId,
"source": storage.Source,
"region": storage.Region,
"region_id": storage.RegionId,
"region_ext_id": storage.RegionExtId,
"region_external_id": storage.RegionExternalId,
"environment": storage.Environment,
"domain_id": storage.DomainId,
"domain_src": storage.DomainSrc,
"project_domain": storage.ProjectDomain,
"public_scope": storage.PublicScope,
"public_src": storage.PublicSrc,
"shared_domains": storage.SharedDomains,
"shared_projects": storage.SharedProjects,
"schedtags": storage.Schedtags,
"hosts": storage.Hosts,
"storage_conf": storage.StorageConf,
"metadata": storage.Metadata,
"progress": storage.Progress,
"can_delete": storage.CanDelete,
"can_update": storage.CanUpdate,
"update_version": storage.UpdateVersion,
"created_at": storage.CreatedAt,
"updated_at": storage.UpdatedAt,
"imported_at": storage.ImportedAt,
}
formatted["storages"] = append(formatted["storages"].([]map[string]interface{}), storageInfo)
}
// 构造摘要信息
formatted["summary"] = map[string]interface{}{
"total_storages": response.Total, // 总存储数量
"returned_count": len(response.Storages), // 当前返回的存储数量
"has_more": response.Total > int64(offset+len(response.Storages)), // 是否还有更多数据
"next_offset": offset + len(response.Storages), // 下一页的偏移量
}
return formatted
}

View File

@@ -1,239 +0,0 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/models"
)
// CloudpodsVPCsTool 用于查询Cloudpods VPC列表的工具
//
// 字段:
// - adapter: 用于与Cloudpods API进行交互的适配器
type CloudpodsVPCsTool struct {
adapter *adapters.CloudpodsAdapter
}
// NewCloudpodsVPCsTool 创建CloudpodsVPCsTool实例
//
// 参数:
// - adapter: 用于与Cloudpods API交互的适配器
//
// 返回值:
// - *CloudpodsVPCsTool: CloudpodsVPCsTool实例指针
func NewCloudpodsVPCsTool(adapter *adapters.CloudpodsAdapter) *CloudpodsVPCsTool {
return &CloudpodsVPCsTool{
adapter: adapter,
}
}
// GetTool 定义并返回查询VPC列表工具的元数据
//
// 工具用途:
//
// 查询Cloudpods VPC列表获取虚拟私有网络信息
//
// 参数说明:
// - limit: 返回结果数量限制默认为20
// - offset: 返回结果偏移量默认为0
// - search: 搜索关键词可以按VPC名称搜索
// - cloudregion_id: 过滤指定云区域的VPC资源
// - ak: 用户登录cloudpods后获取的access key
// - sk: 用户登录cloudpods后获取的secret key
func (c *CloudpodsVPCsTool) GetTool() mcp.Tool {
return mcp.NewTool(
"cloudpods_list_vpcs",
mcp.WithDescription("查询Cloudpods VPC列表获取虚拟私有网络信息"),
mcp.WithString("limit", mcp.Description("返回结果数量限制默认为20")),
mcp.WithString("offset", mcp.Description("返回结果偏移量默认为0")),
mcp.WithString("search", mcp.Description("搜索关键词可以按VPC名称搜索")),
mcp.WithString("cloudregion_id", mcp.Description("过滤指定云区域的VPC资源")),
mcp.WithString("ak", mcp.Description("用户登录cloudpods后获取的access key")),
mcp.WithString("sk", mcp.Description("用户登录cloudpods后获取的secret key")),
)
}
// Handle 处理查询VPC列表的请求
//
// 参数:
// - ctx: 控制生命周期的上下文
// - req: 包含查询参数的请求对象
//
// 返回值:
// - *mcp.CallToolResult: 包含VPC列表的响应对象
// - error: 可能的错误信息
func (c *CloudpodsVPCsTool) Handle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取可选参数:返回结果数量限制,如果指定则转换为整数
limit := 20
if limitStr := req.GetString("limit", ""); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
// 获取可选参数:结果偏移量,如果指定则转换为整数
offset := 0
if offsetStr := req.GetString("offset", ""); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// 获取可选参数:搜索关键词
search := req.GetString("search", "")
// 获取可选参数云区域ID
cloudRegionID := req.GetString("cloudregion_id", "")
// 获取可选参数:访问凭证
ak := req.GetString("ak", "")
sk := req.GetString("sk", "")
// 调用适配器查询VPC列表
vpcsResponse, err := c.adapter.ListVPCs(ctx, limit, offset, search, cloudRegionID, ak, sk)
if err != nil {
log.Errorf("Fail to query vpc: %s", err)
return nil, fmt.Errorf("fail to query vpc: %w", err)
}
// 格式化查询结果
formattedResult := c.formatVPCsResult(vpcsResponse, limit, offset, search, cloudRegionID)
// 将结果序列化为JSON格式
resultJSON, err := json.MarshalIndent(formattedResult, "", " ")
if err != nil {
log.Errorf("Fail to serialize result: %s", err)
return nil, fmt.Errorf("fail to serialize result: %w", err)
}
return mcp.NewToolResultText(string(resultJSON)), nil
}
// GetName 返回工具的名称标识符
//
// 返回值:
// - string: 工具名称字符串,用于唯一标识该工具
func (c *CloudpodsVPCsTool) GetName() string {
return "cloudpods_list_vpcs"
}
// formatVPCsResult 格式化VPC列表的响应结果
//
// 参数:
// - response: 原始响应数据
// - limit: 查询限制
// - offset: 查询偏移量
// - search: 搜索关键词
// - cloudRegionID: 云区域ID
//
// 返回值:
// - map[string]interface{}: 包含VPC列表的格式化结果
func (c *CloudpodsVPCsTool) formatVPCsResult(response *models.VpcListResponse, limit, offset int, search, cloudRegionID string) map[string]interface{} {
// 初始化格式化结果结构
formatted := map[string]interface{}{
"query_info": map[string]interface{}{
"limit": limit,
"offset": offset,
"search": search,
"cloudregion_id": cloudRegionID,
"total": response.Total,
"count": len(response.Vpcs),
},
"vpcs": make([]map[string]interface{}, 0, len(response.Vpcs)),
}
// 遍历VPC列表构造每个VPC的详细信息
for _, vpc := range response.Vpcs {
vpcInfo := map[string]interface{}{
"id": vpc.Id,
"name": vpc.Name,
"description": vpc.Description,
"cidr_block": vpc.CidrBlock,
"cidr_block6": vpc.CidrBlock6,
"status": vpc.Status,
"enabled": vpc.Enabled,
"is_default": vpc.IsDefault,
"is_public": vpc.IsPublic,
"provider": vpc.Provider,
"brand": vpc.Brand,
"cloud_env": vpc.CloudEnv,
"environment": vpc.Environment,
"cloudregion": vpc.Cloudregion,
"cloudregion_id": vpc.CloudregionId,
"region": vpc.Region,
"region_id": vpc.RegionId,
"external_id": vpc.ExternalId,
"external_access_mode": vpc.ExternalAccessMode,
"globalvpc": vpc.Globalvpc,
"globalvpc_id": vpc.GlobalvpcId,
"account": vpc.Account,
"account_id": vpc.AccountId,
"account_status": vpc.AccountStatus,
"account_health_status": vpc.AccountHealthStatus,
"manager": vpc.Manager,
"manager_id": vpc.ManagerId,
"manager_domain": vpc.ManagerDomain,
"manager_domain_id": vpc.ManagerDomainId,
"manager_project": vpc.ManagerProject,
"manager_project_id": vpc.ManagerProjectId,
"network_count": vpc.NetworkCount,
"wire_count": vpc.WireCount,
"dns_zone_count": vpc.DnsZoneCount,
"natgateway_count": vpc.NatgatewayCount,
"routetable_count": vpc.RoutetableCount,
"accept_vpc_peer_count": vpc.AcceptVpcPeerCount,
"request_vpc_peer_count": vpc.RequestVpcPeerCount,
"direct": vpc.Direct,
"domain_id": vpc.DomainId,
"domain_src": vpc.DomainSrc,
"project_domain": vpc.ProjectDomain,
"public_scope": vpc.PublicScope,
"public_src": vpc.PublicSrc,
"region_ext_id": vpc.RegionExtId,
"region_external_id": vpc.RegionExternalId,
"source": vpc.Source,
"progress": vpc.Progress,
"shared_domains": vpc.SharedDomains,
"shared_projects": vpc.SharedProjects,
"can_delete": vpc.CanDelete,
"can_update": vpc.CanUpdate,
"is_emulated": vpc.IsEmulated,
"metadata": vpc.Metadata,
"created_at": vpc.CreatedAt,
"updated_at": vpc.UpdatedAt,
"imported_at": vpc.ImportedAt,
}
formatted["vpcs"] = append(formatted["vpcs"].([]map[string]interface{}), vpcInfo)
}
// 构造摘要信息
formatted["summary"] = map[string]interface{}{
"total_vpcs": response.Total,
"returned_count": len(response.Vpcs),
"has_more": response.Total > int64(offset+len(response.Vpcs)),
"next_offset": offset + len(response.Vpcs),
}
return formatted
}

View File

@@ -1 +0,0 @@
package tools // import "yunion.io/x/onecloud/pkg/mcp-server/tools"