mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
fix(mcp-server): mcp use climc struct (#25184)
This commit is contained in:
@@ -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{})
|
||||
|
||||
@@ -34,16 +34,18 @@ func init() {
|
||||
cmd.Perform("purge", &compute.CloudregionPurgeOptions{})
|
||||
|
||||
type CloudregionListOptions struct {
|
||||
_ struct{} `mcp-desc:"【创建流程中的中间步骤】本工具不能完成创建。指定云厂商时必须传 provider,例如创建阿里云:provider=[\"Aliyun\"]。创建虚拟机时必须 usable=true(MCP 默认注入)。查完后继续 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:"只列网络可用的区域;创建虚拟机时必须为 true(MCP 默认注入 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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
printutils.PrintJSONList(list, columns)
|
||||
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:
|
||||
printutils.PrintJSONObject(obj)
|
||||
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) {
|
||||
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) {
|
||||
printutils.PrintJSONBatchResults(results, columns)
|
||||
PrintBatchResults(results, columns)
|
||||
}
|
||||
|
||||
func ExportList(list *printutils.ListResult, file string, exportKeys string, exportTexts string, columns []string) {
|
||||
|
||||
@@ -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 callback,JSON 输出返回给客户端
|
||||
|
||||
## 主要功能
|
||||
## 扩展工具
|
||||
|
||||
- 统一管理多云资源(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 即可注册。
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SERVICE_TYPE = "llm"
|
||||
SERVICE_TYPE = "llm"
|
||||
SERVICE_VERSION = ""
|
||||
)
|
||||
|
||||
type LLMBaseListDetails struct {
|
||||
|
||||
@@ -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_capability(ID=区域 id;从 storage_types2 取系统盘 backend)
|
||||
3. climc_cached_image_list(公有云)或 climc_image_list(KVM);公有云必须带 provider + region=区域 id;不要重复调用
|
||||
4. climc_server_sku_list(公有云带 provider+cloudregion;用户说 2c2g/2核2G 时传 spec="2c2g")
|
||||
5. climc_server_create(name、disk 须含 image+backend、instance-type 或 ncpu/mem-spec;公有云 hypervisor=aliyun、prefer-region=区域 id)。net 可省略:未指定时自动 random(nets:[{exit:false}])调度,默认不要先 network-list/vpc-list
|
||||
查询工具的返回不等于任务完成;必须最终调用 climc_server_create。创建失败时根据工具错误向用户说明原因。
|
||||
|
||||
## 工作流程
|
||||
1. 理解用户的需求
|
||||
2. **优先检查是否有合适的工具可以完成任务,如果有则必须调用工具**
|
||||
2. **优先检查是否有合适的工具可以完成任务,如果有则必须调用工具(发 tool_calls,不要只输出计划文字)**
|
||||
3. 分析工具返回的结果
|
||||
4. 如果需要更多信息,继续调用其他工具
|
||||
5. 最后用自然语言总结结果给用户
|
||||
|
||||
@@ -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 {
|
||||
|
||||
30
pkg/llm/drivers/llm_client/openai_error_test.go
Normal file
30
pkg/llm/drivers/llm_client/openai_error_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
|
||||
// 立刻推一条注释帧,避免 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 {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
fmt.Fprintf(w, "data: %s\n", line)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
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,128 +563,143 @@ 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))
|
||||
|
||||
// 记录工具调用
|
||||
var toolCallRecords []api.MCPAgentToolCallRecord
|
||||
|
||||
log.Infof("Phase 1: Thinking & Acting...")
|
||||
|
||||
// 处理流式的工具调用参数
|
||||
type accumToolCall struct {
|
||||
Id string
|
||||
Name string
|
||||
RawArguments strings.Builder
|
||||
maxRounds := options.Options.MCPAgentMaxToolRounds
|
||||
if maxRounds <= 0 {
|
||||
maxRounds = 8
|
||||
}
|
||||
accToolCalls := make(map[int]*accumToolCall)
|
||||
var accumulatedContent strings.Builder
|
||||
var accumulatedReasoning strings.Builder
|
||||
hasToolCalls := false
|
||||
|
||||
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(),
|
||||
var toolCallRecords []api.MCPAgentToolCallRecord
|
||||
var finalAnswer strings.Builder
|
||||
nudged := false
|
||||
resourceOp := looksLikeResourceOperation(req.Message)
|
||||
labels := newProgressLabelCache()
|
||||
|
||||
for round := 1; round <= maxRounds; round++ {
|
||||
log.Infof("MCP agent tool round %d/%d", round, maxRounds)
|
||||
|
||||
type accumToolCall struct {
|
||||
Id string
|
||||
Name string
|
||||
RawArguments strings.Builder
|
||||
}
|
||||
accToolCalls := make(map[int]*accumToolCall)
|
||||
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()}
|
||||
}
|
||||
atc := accToolCalls[idx]
|
||||
if id := tc.GetId(); id != "" {
|
||||
atc.Id = id
|
||||
}
|
||||
if name := tc.GetFunction().GetName(); name != "" {
|
||||
atc.Name = name
|
||||
}
|
||||
if args := tc.GetFunction().GetRawArguments(); args != "" {
|
||||
atc.RawArguments.WriteString(args)
|
||||
}
|
||||
}
|
||||
|
||||
atc := accToolCalls[idx]
|
||||
if id := tc.GetId(); id != "" {
|
||||
atc.Id = id
|
||||
}
|
||||
if name := tc.GetFunction().GetName(); name != "" {
|
||||
atc.Name = name
|
||||
}
|
||||
if args := tc.GetFunction().GetRawArguments(); args != "" {
|
||||
atc.RawArguments.WriteString(args)
|
||||
}
|
||||
if r := chunk.GetReasoningContent(); len(r) > 0 {
|
||||
accumulatedReasoning.WriteString(r)
|
||||
}
|
||||
if content := chunk.GetContent(); len(content) > 0 {
|
||||
accumulatedContent.WriteString(content)
|
||||
// 尚未出现 tool_calls 时按 token 增量推送,避免最终结果整段一次性返回
|
||||
if onStream != nil && optimisticStream && !hasToolCalls {
|
||||
streamedAnswer = true
|
||||
if err := onStream(content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "chat stream round %d", round)
|
||||
}
|
||||
|
||||
if r := chunk.GetReasoningContent(); len(r) > 0 {
|
||||
accumulatedReasoning.WriteString(r)
|
||||
}
|
||||
|
||||
content := chunk.GetContent()
|
||||
if len(content) > 0 {
|
||||
accumulatedContent.WriteString(content)
|
||||
if onStream != nil {
|
||||
if err := onStream(content); err != nil {
|
||||
return err
|
||||
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: finalAnswer.String(),
|
||||
ToolCalls: toolCallRecords,
|
||||
}, nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "phase 1 chat stream error")
|
||||
}
|
||||
|
||||
// 检查是否有工具调用
|
||||
if !hasToolCalls {
|
||||
// 如果阶段一没有调用工具,直接返回结果
|
||||
return &api.MCPAgentResponse{
|
||||
Success: true,
|
||||
Answer: accumulatedContent.String(),
|
||||
ToolCalls: toolCallRecords,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert accumulated tool calls to ILLMToolCall
|
||||
var toolCalls []ILLMToolCall
|
||||
// Find max index
|
||||
maxIdx := -1
|
||||
for idx := range accToolCalls {
|
||||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
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{}
|
||||
for i := 0; i <= maxIdx; i++ {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "process tool calls")
|
||||
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...)
|
||||
}
|
||||
|
||||
// 将工具调用相关的消息加入历史
|
||||
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)
|
||||
|
||||
// 实时流式输出
|
||||
// 工具轮次用尽后,再给模型一轮纯文本总结(不再传 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 {
|
||||
if err := onStream(content); err != nil {
|
||||
return err
|
||||
}
|
||||
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: 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: finalAnswer.String(),
|
||||
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))
|
||||
}
|
||||
|
||||
624
pkg/llm/models/mcp_agent_progress.go
Normal file
624
pkg/llm/models/mcp_agent_progress.go
Normal 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]) + "…"
|
||||
}
|
||||
69
pkg/llm/models/mcp_agent_progress_test.go
Normal file
69
pkg/llm/models/mcp_agent_progress_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,21 +63,48 @@ 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,
|
||||
},
|
||||
},
|
||||
userCred: userCred,
|
||||
pendingReqs: make(map[int64]chan *rawMCPResponse),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -21,14 +21,17 @@ import (
|
||||
)
|
||||
|
||||
type CachedImageListOptions struct {
|
||||
_ struct{} `mcp-desc:"【创建流程中的中间步骤】公有云/非KVM 选镜像。必须带 provider(如 [\"Aliyun\"]),region 必须传 climc_cloud_region_list 返回的 id(UUID),禁止传 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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -73,10 +73,12 @@ func (opts *DBInstanceCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
}
|
||||
|
||||
type DBInstanceListOptions struct {
|
||||
_ struct{} `mcp-desc:"列出 RDS(dbinstance)。可用 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:"查询 RDS(dbinstance)详情。ID 可用 climc_dbinstance_list 返回的 id/name"`
|
||||
|
||||
DBInstanceIdOptions
|
||||
}
|
||||
|
||||
type DBInstanceRenewOptions struct {
|
||||
DBInstanceIdOptions
|
||||
DURATION string `help:"Duration of renew, ADMIN only command"`
|
||||
|
||||
@@ -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/name,SIZE 为目标容量(如 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"`
|
||||
}
|
||||
|
||||
@@ -21,18 +21,20 @@ import (
|
||||
)
|
||||
|
||||
type ElasticipListOptions struct {
|
||||
Region string `help:"List eips in cloudregion"`
|
||||
_ struct{} `mcp-desc:"列出弹性公网 IP(EIP)。可用 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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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/name,RULE 为规则字符串(如 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) {
|
||||
|
||||
@@ -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、disk(image+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 与 EIP(climc_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) {
|
||||
|
||||
@@ -15,25 +15,34 @@
|
||||
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"`
|
||||
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"`
|
||||
PrepaidStatus string `help:"Prepaid status" choices:"soldout|available"`
|
||||
CpuArch string `help:"Cpu Arch" choices:"x86|arm"`
|
||||
Enabled *bool `help:"Filter enabled skus"`
|
||||
Distinct bool `help:"distinct sku by 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"`
|
||||
// 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 MB;2G=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" 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"`
|
||||
}
|
||||
|
||||
69
pkg/mcclient/options/compute/serverskus_spec_test.go
Normal file
69
pkg/mcclient/options/compute/serverskus_spec_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
81
pkg/mcp-server/adapters/adapter.go
Normal file
81
pkg/mcp-server/adapters/adapter.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
58
pkg/mcp-server/adapters/credentials.go
Normal file
58
pkg/mcp-server/adapters/credentials.go
Normal 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")
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(®ion); 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
|
||||
}
|
||||
595
pkg/mcp-server/climcgen/create_flow.go
Normal file
595
pkg/mcp-server/climcgen/create_flow.go
Normal 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 嵌入 ServerConfigs,CLI 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 的 --mem(MB)
|
||||
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() 为 nil,host_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")
|
||||
}
|
||||
174
pkg/mcp-server/climcgen/create_flow_test.go
Normal file
174
pkg/mcp-server/climcgen/create_flow_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
30
pkg/mcp-server/climcgen/doc.go
Normal file
30
pkg/mcp-server/climcgen/doc.go
Normal 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
|
||||
|
||||
未标记的可选参数不会进入 schema(positional / climc required 仍会保留)。
|
||||
|
||||
MCP tool 补充说明通过 Options 上的 mcp-desc tag 写入(常用 `_ struct{}` 承载),
|
||||
由 buildDescription 拼入 tool description。
|
||||
*/
|
||||
package climcgen
|
||||
285
pkg/mcp-server/climcgen/exec.go
Normal file
285
pkg/mcp-server/climcgen/exec.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
pkg/mcp-server/climcgen/extra.go
Normal file
75
pkg/mcp-server/climcgen/extra.go
Normal 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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
56
pkg/mcp-server/climcgen/instructions.go
Normal file
56
pkg/mcp-server/climcgen/instructions.go
Normal 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 MCP(climc tools)使用规则:
|
||||
|
||||
认证:initialize / tools/list 可匿名;tools/call 必须带 Header(X-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_list(KVM)或 climc_cached_image_list(公有云,须 provider+region=区域 id)
|
||||
d) climc_server_sku_list(口语 2c2g 用 spec=\"2c2g\")或直接 ncpu/mem
|
||||
e) climc_server_create(name、disk 含 image+backend、规格;公有云 hypervisor+prefer-region)。net 可省略自动调度。工具会 forecast 预调度并等待 running/ready;若返回 wait_pending,用 climc_server_show 继续查,勿重复创建。
|
||||
3. 启停/重启/删除/重置密码/改配/挂盘/绑 EIP:climc_server_list 定位 id 后立刻调用对应操作工具。
|
||||
4. 监控指标用 climc_monitor_unifiedmonitor_query;climc_server_monitor 是 QEMU HMP/QMP,不是指标。
|
||||
5. 缺参只追问真正缺失项;已有 id 直接下一步。
|
||||
`
|
||||
|
||||
// BuildServerInstructions 用 platformName(BaseOptions.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), "创建流程中的中间步骤")
|
||||
}
|
||||
139
pkg/mcp-server/climcgen/mcp_tags.go
Normal file
139
pkg/mcp-server/climcgen/mcp_tags.go
Normal 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
|
||||
}
|
||||
94
pkg/mcp-server/climcgen/register.go
Normal file
94
pkg/mcp-server/climcgen/register.go
Normal 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
|
||||
}
|
||||
73
pkg/mcp-server/climcgen/register_test.go
Normal file
73
pkg/mcp-server/climcgen/register_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
272
pkg/mcp-server/climcgen/schema.go
Normal file
272
pkg/mcp-server/climcgen/schema.go
Normal 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_types2;ISO 请用 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 返回的 id(UUID)。禁止传 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"] = "内存 MB;2G=2048。有口语规格时优先用 spec"
|
||||
properties["mem"] = prop
|
||||
}
|
||||
if prop, ok := properties["postpaid-status"].(map[string]interface{}); ok {
|
||||
prop["description"] = "按量付费状态;创建时优先 available,避免选到 soldout"
|
||||
properties["postpaid-status"] = prop
|
||||
}
|
||||
}
|
||||
|
||||
// 认证:请使用连接 Header(X-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
|
||||
}
|
||||
82
pkg/mcp-server/climcgen/schema_test.go
Normal file
82
pkg/mcp-server/climcgen/schema_test.go
Normal 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
|
||||
}
|
||||
155
pkg/mcp-server/climcgen/tool.go
Normal file
155
pkg/mcp-server/climcgen/tool.go
Normal 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_create(disk 带 backend;net 可省略)。`
|
||||
case command == "cached-image-list":
|
||||
return `[MCP下一步] 确认已带 provider 及 region=区域id。继续 climc_server_sku_list(若未查),然后立刻 climc_server_create(disk 含 image+backend;net 可省略);禁止用 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 走自动调度;若已选中网络 id,create 时传入 net=["<id>"]。`
|
||||
case command == "server-sku-list" || command == "storage-list":
|
||||
return `[MCP下一步] 请立刻调用 climc_server_create;disk 须含 backend;net 可省略(自动 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 ""
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package models // import "yunion.io/x/onecloud/pkg/mcp-server/models"
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 tag(mcp-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,65 +121,62 @@ 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
if tokenStr != "" {
|
||||
if auth.IsAuthed() {
|
||||
userCred, err := auth.Verify(ctx, tokenStr)
|
||||
if err != nil {
|
||||
log.Errorf("Verify token failed: %s", err)
|
||||
} else {
|
||||
ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred)
|
||||
return ctx, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if akStr != "" && skStr != "" {
|
||||
ctx = context.WithValue(ctx, adapters.ContextKeyAK, akStr)
|
||||
ctx = context.WithValue(ctx, adapters.ContextKeySK, skStr)
|
||||
return ctx, true
|
||||
}
|
||||
|
||||
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)
|
||||
return ctx, true
|
||||
}
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(apiKey)
|
||||
if err == nil {
|
||||
parts := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
|
||||
ctx = context.WithValue(ctx, adapters.ContextKeyAK, parts[0])
|
||||
ctx = context.WithValue(ctx, adapters.ContextKeySK, parts[1])
|
||||
return ctx, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctx, false
|
||||
}
|
||||
|
||||
// Start 以sse模式启动 mcp 服务
|
||||
func (s *CloudpodsMCPServer) Start() error {
|
||||
// 设置 contextFunc 来从 HTTP header 中提取认证信息并放入 context
|
||||
// 支持:X-Auth-Token(token)、AK/SK(Cursor 双 header)、X-API-Key(Claude 单 header:token 或 base64(ak:sk))
|
||||
contextFunc := func(ctx context.Context, r *http.Request) context.Context {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(apiKey)
|
||||
if err == nil {
|
||||
parts := strings.SplitN(string(decoded), ":", 2)
|
||||
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
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
log.Infof("Start mcp server successfully")
|
||||
addr := fmt.Sprintf("%s:%d", options.Options.Address, options.Options.Port)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- sseServer.Start(addr)
|
||||
}()
|
||||
|
||||
return nil
|
||||
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
|
||||
}
|
||||
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 服务
|
||||
// StartStdio 以stdio模式启动 mcp 服务
|
||||
func (s *CloudpodsMCPServer) StartStdio() error {
|
||||
|
||||
err := server.ServeStdio(s.mcpServer)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package tools // import "yunion.io/x/onecloud/pkg/mcp-server/tools"
|
||||
Reference in New Issue
Block a user