Compare commits

...

28 Commits

Author SHA1 Message Date
Zexi Li
203f5bb396 fix(llm): require memory_mb for local_path HAMi devices (#25244)
local_path SKUs cannot estimate VRAM from InstantModel; validate HAMi devices set devices[].memory_mb on create/update and deployment.
2026-07-29 10:22:39 +08:00
wanyaoqi
4a729a7639 Automated cherry pick of #25206: fix(region,host): host register with correct hostname (#25232)
* fix(region,host): host register with correct hostname

* fix(region): guest migrate reuse create params schedtag
2026-07-28 10:59:34 +08:00
wanyaoqi
712ce702ce feat(host): support ascend npu hami (#25236) 2026-07-28 10:55:50 +08:00
Zexi Li
80ea7e60e1 feat(baremetal): support IPMI RMCP+ cipher suite probe and persist (#25238)
Auto-detect working ipmitool -C suites (0/3/17) and store cipher_suite on host IPMI config for subsequent connections.
2026-07-28 10:27:06 +08:00
屈轩
daf21aa6a3 fix(climc): climc options update (#25228) 2026-07-27 19:41:40 +08:00
wanyaoqi
6bc05ca5c1 fix(host): qmp device add convert string to boolean (#25231) 2026-07-27 19:08:27 +08:00
Zexi Li
ea0e1ca42f fix(aiproxy): avoid permanent ai_key blacklist and clarify skip reasons (#25224)
Enter cooldown when health score hits zero so keys can recover, and include concrete skip reasons in resolve errors.
2026-07-27 12:15:55 +08:00
Jian Qiu
94862f5232 feature: ip set support (#25219)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
2026-07-24 16:34:50 +08:00
Jian Qiu
887bb943b8 fix: vpc distributes static routes (#25212)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
2026-07-23 19:12:22 +08:00
Zexi Li
205dbe2f64 feat(llm): support hami (#25214) 2026-07-23 17:08:26 +08:00
Jian Qiu
554cdd01be fix: baremetal use legacy http client (#25209)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
2026-07-23 16:33:36 +08:00
屈轩
da03a421a5 fix(region): vendor update (#25205) 2026-07-22 11:19:14 +08:00
Jian Qiu
06517e5aa6 fix: allow empty ip on link-local nic (#25201)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
2026-07-22 11:06:30 +08:00
屈轩
6d49bee5e6 fix(region): vendor update (#25198) 2026-07-21 23:38:10 +08:00
屈轩
033408e7d0 fix(mcp-server): optimized mcp desc (#25195) 2026-07-21 16:19:30 +08:00
屈轩
fe1846f690 fix(mcp-server): support forwarding mcp-server request (#25192) 2026-07-21 13:27:46 +08:00
屈轩
7aa20811f5 fix(mcp-server): optimized mcp server options (#25189) 2026-07-20 20:58:37 +08:00
屈轩
93971e83fb fix(mcp-server): mcp use climc struct (#25185) 2026-07-20 17:17:27 +08:00
wanyaoqi
1950824dfa feat(region,host,scheduler): isolated device refactor and hami support (#25181) 2026-07-20 13:20:49 +08:00
wanyaoqi
7fec30216b fix(host-deployer): bond netplan add vlan subinterface config (#25179) 2026-07-20 12:19:46 +08:00
wanyaoqi
23e1910817 fix(region): baremetal boot mode decided by selected host on image support both uefi&bios (#25167) 2026-07-20 12:07:39 +08:00
wanyaoqi
a8f3474fb2 fix(host-deployer): windows disk extend separate partition and filesystem (#25168) 2026-07-20 12:06:33 +08:00
wanyaoqi
bee68803b2 fix(host): add nest virtualization options (#25169) 2026-07-20 12:03:50 +08:00
wanyaoqi
f58fcc2999 fix(region): disk change driver support change aio_mode and cache_mode (#25170) 2026-07-20 11:57:55 +08:00
屈轩
fcbf833fde fix(mcp-server): query scope params (#25173) 2026-07-17 00:26:57 +08:00
屈轩
af2e410c24 fix(cloudid): batch detach policy (#25165) 2026-07-16 19:27:36 +08:00
屈轩
f6e32e7707 fix(region): support cas hypervisor (#25162) 2026-07-16 19:08:01 +08:00
Zexi Li
277546c95c fix(aiproxy): emit done events before response.completed in stream converter (#25159)
Emit output_item.done, reasoning_summary_part.done, function_call_arguments.done,
and related finalize events for text, reasoning, and tool calls before the
response.completed event to match the Responses API stream contract.
2026-07-16 17:04:02 +08:00
526 changed files with 14142 additions and 176338 deletions

View File

@@ -1,4 +1,4 @@
FROM registry.cn-beijing.aliyuncs.com/yunionio/baremetal-base:v0.3.9-20251215.0
FROM registry.cn-beijing.aliyuncs.com/yunionio/baremetal-base:v0.3.9-20260724.0
LABEL maintainer="Zexi Li <lizexi@yunionyun.com>"

View File

@@ -1,8 +1,8 @@
#RUN yum install -y https://iso.yunion.cn/vm-images/baremetal-pxerom-1.1.0-21092209.x86_64.rpm
#RUN yum install -y http://192.168.23.50:8083/baremetal-pxerom-1.1.0-21092209.x86_64.rpm
FROM registry.cn-beijing.aliyuncs.com/yunionio/yunionos:v4.0.0-20251201.0 as yunionos
FROM registry.cn-beijing.aliyuncs.com/yunionio/yunionos:v4.0.0-20251203.0 as yunionos
FROM centos:8 as grub-stage
FROM registry.cn-beijing.aliyuncs.com/cloudpods/centos:8 as grub-stage
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*

View File

@@ -1,6 +1,7 @@
REGISTRY ?= "registry.cn-beijing.aliyuncs.com/yunionio"
DOCKER_BUILD = docker build -t $(REGISTRY)
DOCKER_BUILDX = docker buildx build --platform linux/arm64,linux/amd64,linux/riscv64 --push -t $(REGISTRY)
DOCKER_BUILDX_BM = docker buildx build --platform linux/arm64,linux/amd64 --push -t $(REGISTRY)
debian10-base:
@@ -40,7 +41,7 @@ WEBCONSOLE_BASE_VERSION_3-22-2 = 3.22.2-1
webconsole-base:
$(DOCKER_BUILDX)/webconsole-base:$(WEBCONSOLE_BASE_VERSION_3-22-2) -f ./Dockerfile.webconsole-base .
BAREMETAL_BASE_VERSION = v0.3.9-20251112.1
BAREMETAL_BASE_VERSION = v0.3.9-20260724.0
FEDORA_RISCV64_VERSION = 42
fedora-riscv64-base:
@@ -53,8 +54,9 @@ baremetal-base-riscv: fedora-riscv64-base
$(DOCKER_BUILDX)/baremetal-base:$(BAREMETAL_BASE_VERSION) -f ./Dockerfile.baremetal-base-riscv .
#docker push $(REGISTRY)/baremetal-base:$(BAREMETAL_BASE_VERSION)
# TODO: support riscv64 for baremetal-base
baremetal-base:
$(DOCKER_BUILDX)/baremetal-base:$(BAREMETAL_BASE_VERSION) -f ./Dockerfile.baremetal-base .
$(DOCKER_BUILDX_BM)/baremetal-base:$(BAREMETAL_BASE_VERSION) -f ./Dockerfile.baremetal-base .
#docker push $(REGISTRY)/baremetal-base:$(BAREMETAL_BASE_VERSION)
TORRENT_VERSION = 20210815.0

View File

@@ -58,7 +58,7 @@ func init() {
type HostNetworkDetailOptions struct {
HOST string `help:"ID or Name of Host"`
NETWORK string `help:"ID or Name of Wire"`
NETWORK string `help:"ID or Name of Network"`
}
R(&HostNetworkDetailOptions{}, "host-network-show", "Show baremetal network details", func(s *mcclient.ClientSession, args *HostNetworkDetailOptions) error {
result, err := modules.Baremetalnetworks.Get(s, args.HOST, args.NETWORK, nil)

View File

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

View File

@@ -26,7 +26,7 @@ import (
func init() {
type CloudproviderRegionListOptions struct {
options.BaseListOptions
Region string `help:"ID or Name of Host"`
Region string `help:"ID or Name of Region"`
Capability []string `help:"capability filter" choices:"project|compute|network|loadbalancer|objectstore|rds|cache|event"`
}

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ import (
func init() {
cmd := shell.NewResourceCmd(&modules.Disks)
cmd.List(&compute_options.DiskListOptions{})
cmd.Show(&compute_options.DiskIdOptions{})
cmd.Show(&compute_options.DiskShowOptions{})
cmd.Perform("public", &compute_options.DiskIdOptions{})
cmd.Perform("private", &compute_options.DiskIdOptions{})
cmd.Perform("syncstatus", &compute_options.DiskIdOptions{})
@@ -55,13 +55,7 @@ func init() {
cmd.Perform("change-billing-type", new(compute_options.DiskChangeBillingTypeOptions))
cmd.Perform("change-storage-type", &compute_options.DiskChangeStorageTypeOptions{})
type DiskDeleteOptions struct {
ID []string `help:"ID of disks to delete" metavar:"DISK"`
OverridePendingDelete bool `help:"Delete disk directly instead of pending delete" short-token:"f"`
DeleteSnapshots bool `help:"Delete disk snapshots before delete disk"`
}
R(&DiskDeleteOptions{}, "disk-delete", "Delete a disk", func(s *mcclient.ClientSession, args *DiskDeleteOptions) error {
R(&compute_options.DiskDeleteOptions{}, "disk-delete", "Delete a disk", func(s *mcclient.ClientSession, args *compute_options.DiskDeleteOptions) error {
params := jsonutils.NewDict()
if args.OverridePendingDelete {
params.Add(jsonutils.JSONTrue, "override_pending_delete")
@@ -100,7 +94,7 @@ func init() {
AutoSnapshot string `help:"enable/disable auto snapshot of disk" choices:"enable|disable"`
DiskType string `help:"Disk type" choices:"data|volume|sys"`
IsSsd *bool `help:"mark disk as ssd" negative:"no-is-ssd"`
AutoReset *bool `help:"Enable auto reset disk after geust shutdown"`
AutoReset *bool `help:"Enable auto reset disk after guest shutdown"`
}
R(&DiskUpdateOptions{}, "disk-update", "Update property of a virtual disk", func(s *mcclient.ClientSession, args *DiskUpdateOptions) error {
params := jsonutils.NewDict()
@@ -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)
@@ -189,7 +179,7 @@ func init() {
})
type DiskResetOptions struct {
DISK string `help:"ID or name of disk"`
SNAPSHOT string `help:"snapshots ID of disk"`
SNAPSHOT string `help:"Snapshot ID of disk"`
AutoStart bool `help:"Autostart guest"`
}
R(&DiskResetOptions{}, "disk-reset", "Resize a disk", func(s *mcclient.ClientSession, args *DiskResetOptions) error {
@@ -245,9 +235,9 @@ func init() {
type DiskChangeOwnerOptions struct {
ID string `help:"Disk to change owner" json:"-"`
PROJECT string `help:"Project ID or change" json:"tenant"`
PROJECT string `help:"Project ID or name" json:"tenant"`
}
R(&DiskChangeOwnerOptions{}, "disk-change-owner", "Change owner porject of a disk", func(s *mcclient.ClientSession, opts *DiskChangeOwnerOptions) error {
R(&DiskChangeOwnerOptions{}, "disk-change-owner", "Change owner project of a disk", func(s *mcclient.ClientSession, opts *DiskChangeOwnerOptions) error {
params, err := options.StructToParams(opts)
if err != nil {
return err

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
// 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 (
"yunion.io/x/onecloud/cmd/climc/shell"
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/mcclient/options/compute"
)
func init() {
cmd := shell.NewResourceCmd(&modules.IpSets)
cmd.List(&compute.IpSetListOptions{})
cmd.Create(&compute.IpSetCreateOptions{})
cmd.Update(&compute.IpSetUpdateOptions{})
cmd.Delete(&compute.IpSetIdOptions{})
}

View File

@@ -26,8 +26,12 @@ func init() {
cmd.List(&compute.DeviceListOptions{})
cmd.Update(&compute.DeviceUpdateOptions{})
cmd.Show(&compute.DeviceShowOptions{})
cmd.Create(&compute.DeviceCreateOptions{})
cmd.BatchDelete(&compute.DeviceDeleteOptions{})
cmd.BatchPerform("purge", &compute.DevicePurgeOptions{})
cmd.Perform("public", &options.SharableResourcePublicOptions{})
cmd.Perform("private", &options.BaseIdOptions{})
scmd := shell.NewResourceCmd(&modules.ServerIsolatedDevices)
scmd.List(&compute.ServerDeviceListOptions{})
}

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ import (
func init() {
type NetworkinterfaceNetworkListOptions struct {
Networkinterface string `help:"ID or Name of Server"`
Networkinterface string `help:"ID or Name of Networkinterface"`
Network string `help:"Network ID or name"`
}
R(&NetworkinterfaceNetworkListOptions{}, "networkinterface-network-list", "List server network pairs", func(s *mcclient.ClientSession, args *NetworkinterfaceNetworkListOptions) error {

View File

@@ -212,7 +212,7 @@ func init() {
type NetworkChangeOwnerOptions struct {
ID string `help:"Network to change owner" json:"-"`
PROJECT string `help:"Project ID or change" json:"tenant"`
PROJECT string `help:"Project ID or name" json:"tenant"`
}
R(&NetworkChangeOwnerOptions{}, "network-change-owner", "Change owner project of a network", func(s *mcclient.ClientSession, args *NetworkChangeOwnerOptions) error {
params, err := options.StructToParams(args)

View File

@@ -24,10 +24,10 @@ import (
func init() {
type NetworkReserveIPOptions struct {
NETWORK string `help:"IP or name of network"`
NETWORK string `help:"ID or name of network"`
NOTES string `help:"Why reserve this IP"`
IPS []string `help:"IPs to reserve"`
Duration string `help:"reservation duration, e.g. 1I, 1H, 2M"`
Duration string `help:"reservation duration, e.g. 1D, 1H, 2M"`
Status string `help:"ip status"`
}
R(&NetworkReserveIPOptions{}, "network-reserve-ip", "Reserve an IP address from pool", func(s *mcclient.ClientSession, args *NetworkReserveIPOptions) error {
@@ -49,7 +49,7 @@ func init() {
})
type NetworkReleaseReservedIPOptions struct {
NETWORK string `help:"IP or name of network"`
NETWORK string `help:"ID or name of network"`
IP string `help:"IP to release"`
}
R(&NetworkReleaseReservedIPOptions{}, "network-release-reserved-ip", "Release a reserved IP into pool", func(s *mcclient.ClientSession, args *NetworkReleaseReservedIPOptions) error {

View File

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

View File

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

View File

@@ -77,18 +77,10 @@ func init() {
type ServerDiskUpdateOptions struct {
SERVER string `help:"ID or Name of server"`
DISK string `help:"ID or Name of Disk"`
Cache string `help:"Cache mode of vDisk" choices:"writethrough|none|writeback|directsync"`
Aio string `help:"Asynchronous IO mode of vDisk" choices:"native|threads"`
Index int64 `help:"Index of vDisk" default:"-1"`
}
R(&ServerDiskUpdateOptions{}, "server-disk-update", "Update details of a virtual disk of a virtual server", func(s *mcclient.ClientSession, args *ServerDiskUpdateOptions) error {
params := jsonutils.NewDict()
if len(args.Cache) > 0 {
params.Add(jsonutils.NewString(args.Cache), "cache_mode")
}
if len(args.Aio) > 0 {
params.Add(jsonutils.NewString(args.Aio), "aio_mode")
}
if args.Index >= 0 {
params.Add(jsonutils.NewInt(args.Index), "index")
}
@@ -121,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"`
DISK string `help:"ID or name of disk to attach"`
Driver string `help:"Disk driver" choices:"virtio|ide|scsi" mcp:"true"`
Cache string `help:"Cache mode" choices:"writeback|none|writethrough" 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()
@@ -144,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 the disk after detach (ignored if disk has auto_delete)" mcp:"true"`
}
R(&ServerDetachDiskOptions{}, "server-detach-disk", "Detach a disk from a virtual server", func(s *mcclient.ClientSession, args *ServerDetachDiskOptions) error {
params := jsonutils.NewDict()

View File

@@ -23,15 +23,15 @@ import (
func init() {
type ServerAttachDeviceOptions struct {
SERVER string `help:"ID or name of server"`
DEVICE string `help:"ID of isolated device to attach"`
Type string `help:"Device type" choices:"GPU-HPC|GPU-VGA|PCI"`
SERVER string `help:"ID or name of server"`
DEVICE string `help:"ID of isolated device to attach"`
GpuType string `help:"Type of GPU to attach to" choices:"HPC|VGA"`
}
R(&ServerAttachDeviceOptions{}, "server-attach-isolated-device", "Attach an existing isolated device to a virtual server", func(s *mcclient.ClientSession, args *ServerAttachDeviceOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.DEVICE), "device")
if len(args.Type) > 0 {
params.Add(jsonutils.NewString(args.Type), "dev_type")
if args.GpuType != "" {
params.Add(jsonutils.NewString(args.GpuType), "gpu_type")
}
srv, err := modules.Servers.PerformAction(s, args.SERVER, "attach-isolated-device", params)
if err != nil {
@@ -44,11 +44,13 @@ func init() {
type ServerDetachDeviceOptions struct {
SERVER string `help:"ID or name of server"`
DEVICE string `help:"ID of isolated device to detach"`
INDEX int `help:"Index of isolated device to detach"`
IsForce bool `help:"Force detach isolated device"`
}
R(&ServerDetachDeviceOptions{}, "server-detach-isolated-device", "Detach a isolated device from a virtual server", func(s *mcclient.ClientSession, args *ServerDetachDeviceOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.DEVICE), "device")
params.Add(jsonutils.NewInt(int64(args.INDEX)), "index")
if args.IsForce {
params.Set("is_force", jsonutils.JSONTrue)
}

View File

@@ -96,7 +96,7 @@ func init() {
type ServerNetworkUpdateOptions struct {
SERVER string `help:"ID or Name of Server"`
NETWORK string `help:"ID or Name of Wire"`
NETWORK string `help:"ID or Name of Network"`
Mac string `help:"Mac of NIC"`
Driver string `help:"Driver model of vNIC" choices:"virtio|e1000|vmxnet3|rtl8139"`
Index int64 `help:"Index of NIC" default:"-1"`
@@ -205,7 +205,7 @@ func init() {
Reserve bool `help:"Put the release IP address into reserved address pool"`
Force bool `help:"detach server network by force"`
}
R(&ServerDetachNetworkOptions{}, "server-detach-network", "Detach the virtual network fron a virtual server", func(s *mcclient.ClientSession, args *ServerDetachNetworkOptions) error {
R(&ServerDetachNetworkOptions{}, "server-detach-network", "Detach the virtual network from a virtual server", func(s *mcclient.ClientSession, args *ServerDetachNetworkOptions) error {
params := jsonutils.NewDict()
// params.Add(jsonutils.NewString(args.NETWORK), "net_id")
if args.Reserve {

View File

@@ -127,8 +127,9 @@ func init() {
cmd.Perform("update-sub-ips", &options.ServerUpdateSubIpsOptions{})
cmd.BatchPerform("restore-virtual-isolated-devices", &options.ServerIdsOptions{})
cmd.BatchPerform("set-os-info", &options.ServerSetOSInfoOptions{})
cmd.BatchPerform("start-rescue", &options.ServerStartOptions{})
cmd.BatchPerform("stop-rescue", &options.ServerStartOptions{})
// 与 server-start 复用参数时单独包一层,避免继承 mcp-desc 被注册为 MCP tool
cmd.BatchPerform("start-rescue", &options.ServerStartRescueOptions{})
cmd.BatchPerform("stop-rescue", &options.ServerStopRescueOptions{})
cmd.BatchPerform("sync-os-info", &options.ServerIdsOptions{})
cmd.BatchPerform("set-root-disk-matcher", &options.ServerSetRootDiskMatcher{})
cmd.Perform("disable-auto-merge-snapshot", &options.ServerDisableAutoMergeSnapshot{})
@@ -293,7 +294,7 @@ func init() {
type ServerDiskSnapshotOptions struct {
SERVER string `help:"server ID or Name"`
DISK string `help:"create snapshot disk id"`
DISK string `help:"Disk ID for snapshot creation"`
SNAPSHOTNAME string `help:"Snapshot name"`
}
R(&ServerDiskSnapshotOptions{}, "server-disk-create-snapshot", "Task server disk snapshot", func(s *mcclient.ClientSession, args *ServerDiskSnapshotOptions) error {
@@ -733,7 +734,7 @@ func init() {
type ServerSnapshotAndClone struct {
ID string `help:"ID or name of VM" json:"-"`
NAME string `help:"Newly instance name" json:"name"`
NAME string `help:"New instance name" json:"name"`
AutoStart bool `help:"Auto start new guest"`
AllowDelete bool `help:"Allow new guest delete" json:"-"`
Count int `help:"Guest count"`
@@ -772,7 +773,7 @@ func init() {
ID string `help:"ID or name of server"`
Save string `help:"save xml into this file"`
}
R(&ServerJnlpOptions{}, "server-jnlp", "Get baremetal server jnlp file contentn", func(s *mcclient.ClientSession, args *ServerJnlpOptions) error {
R(&ServerJnlpOptions{}, "server-jnlp", "Get baremetal server jnlp file content", func(s *mcclient.ClientSession, args *ServerJnlpOptions) error {
spec, err := modules.Servers.GetSpecific(s, args.ID, "jnlp", nil)
if err != nil {
return err

View File

@@ -32,7 +32,7 @@ func init() {
HostType string `help:"Host type filter" choices:"baremetal|hypervisor|esxi|kubelet|hyperv"`
Gpu bool `help:"Only show gpu devices"`
Zone string `help:"Filter by zone id or name"`
Occupied bool `help:"show occupid host" json:"-"`
Occupied bool `help:"show occupied host" json:"-"`
}
R(&ListOptions{}, "spec", "List all kinds of model specs", func(s *mcclient.ClientSession, args *ListOptions) error {
var params *jsonutils.JSONDict
@@ -76,7 +76,7 @@ func init() {
DiskSpec []string `help:"Disk spec string, like 'Linux_adapter0_HDD_111Gx4'"`
Nic int64 `help:"#Nics count of host" metavar:"<NIC_COUNT>"`
GpuModel []string `help:"GPU model, like 'GeForce GTX 1050 Ti'"`
Occupied bool `help:"Show occupid host" json:"-"`
Occupied bool `help:"Show occupied host" json:"-"`
Manufacture string `help:"Manufacture of host"`
Model string `help:"Model of host"`
}

View File

@@ -25,15 +25,15 @@ import (
)
type BaseEventListOptions struct {
Scope string `help:"scope" choices:"project|domain|system"`
Scope string `help:"RBAC scope" choices:"project|domain|system"`
Since string `help:"Show logs since specific date" metavar:"DATETIME"`
Until string `help:"Show logs until specific date" metavar:"DATETIME"`
Limit int64 `help:"Limit number of logs" default:"20"`
Offset int64 `help:"Offset"`
Offset int64 `help:"Offset for pagination"`
Ascending bool `help:"Ascending order"`
Descending bool `help:"Descending order"`
OrderBy string `help:"order by specific field"`
Action []string `help:"Log action"`
Action []string `help:"Filter by log action"`
User string `help:"filter by operator user"`
Project string `help:"filter by operator user's project"`

View File

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

View File

@@ -28,7 +28,7 @@ import (
func init() {
type CredentialListOptions struct {
Scope string `help:"scope" choices:"project|domain|system"`
Scope string `help:"RBAC scope" choices:"project|domain|system"`
Type string `help:"credential type" choices:"totp|recovery_secret|aksk|enc_key|container_image"`
User string `help:"filter by user"`
UserDomain string `help:"the domain of user"`

View File

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

View File

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

View File

@@ -37,11 +37,11 @@ func init() {
RoleDomain string `help:"Domain for role"`
Limit int64 `help:"maximal returned number of rows"`
Offset int64 `help:"offset index of returned results"`
Users []string `help:"fitler by user id or name"`
Groups []string `help:"fitler by group id or name"`
Roles []string `help:"fitler by role id or name"`
Projects []string `help:"fitler by project id or name"`
Domains []string `help:"fitler by domain id or name"`
Users []string `help:"filter by user id or name"`
Groups []string `help:"filter by group id or name"`
Roles []string `help:"filter by role id or name"`
Projects []string `help:"filter by project id or name"`
Domains []string `help:"filter by domain id or name"`
ProjectDomainId string
ProjectDomains []string `help:"filter by project's domain id or name"`

View File

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

View File

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

View File

@@ -56,10 +56,10 @@ type ImageOptionalOptions struct {
OsCodename string `help:"Codename of OS"`
OsArch string `help:"Os hardware architecture" choices:"x86|x86_64|aarch32|aarch64|riscv32|riscv64"`
OsLang string `help:"OS Language" choices:"zh_CN|en_US"`
Preference int64 `help:"Disk preferences"`
Preference int64 `help:"Image disk preference weight"`
Notes string `help:"Notes about the image"`
Hypervisor []string `help:"Prefer hypervisor type" choices:"kvm|esxi|baremetal|container|openstack|ctyun"`
DiskDriver string `help:"Perfer disk driver" choices:"virtio|scsi|pvscsi|ide|sata"`
DiskDriver string `help:"Prefer disk driver" choices:"virtio|scsi|pvscsi|ide|sata"`
NetDriver string `help:"Preferred network driver" choices:"virtio|e1000|vmxnet3"`
DisableUsbKbd bool `help:"Disable usb keyboard on this image(for hypervisor kvm)"`
BootMode string `help:"UEFI support" choices:"UEFI|BIOS"`

View File

@@ -24,15 +24,15 @@ import (
)
type BaseActionListOptions struct {
Scope string `help:"scope" choices:"project|domain|system"`
Scope string `help:"RBAC scope" choices:"project|domain|system"`
Since string `help:"Show logs since specific date" metavar:"DATETIME"`
Until string `help:"Show logs until specific date" metavar:"DATETIME"`
Limit int64 `help:"Limit number of logs" default:"20"`
Offset int64 `help:"Offset"`
Offset int64 `help:"Offset for pagination"`
Ascending bool `help:"Ascending order"`
Descending bool `help:"Descending order"`
Field []string `help:"field options"`
Action []string `help:"Log action"`
Field []string `help:"Fields to return"`
Action []string `help:"Filter by log action"`
Search string `help:"Filter action logs by obj_name, using 'like' syntax."`
Admin bool `help:"admin mode"`
Succ bool `help:"Show success action log only"`
@@ -47,15 +47,17 @@ 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"`
}
type TypeActionListOptions struct {
BaseActionListOptions
ID string `help:"" metavar:"OBJ_ID"`
ID string `help:"Object ID" metavar:"OBJ_ID"`
}
func doActionList(s *mcclient.ClientSession, args *ActionListOptions) error {

View File

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

View File

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

View File

@@ -126,7 +126,7 @@ func printQuotaList(result jsonutils.JSONObject) {
func init() {
type QuotaOptions struct {
Scope string `help:"scope" choices:"domain|project"`
Scope string `help:"RBAC scope" choices:"domain|project"`
Project string `help:"Tenant name or ID" json:"tenant"`
Domain string `help:"Domain name or ID" json:"domain"`
Refresh bool `help:"refresh" json:"refresh,omitfalse"`
@@ -413,7 +413,7 @@ func init() {
})
type CleanPendingUsageOptions struct {
Scope string `help:"scope" choices:"domain|project"`
Scope string `help:"RBAC scope" choices:"domain|project"`
Project string `help:"Tenant name or ID" json:"tenant"`
Domain string `help:"Domain name or ID" json:"domain"`
}

View File

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

View File

@@ -15,10 +15,15 @@
package shell
import (
"bytes"
"fmt"
"io"
"os"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/util/printutils"
@@ -37,45 +42,111 @@ const (
var outputFormat = OUTPUT_FORMAT_TABLE
// goroutine 本地输出MCP 并发 tools/call 时避免劫持全局 os.Stdout。
type outputState struct {
writer io.Writer
format string
}
var outputStates sync.Map // uint64(goid) -> *outputState
func OutputFormat(s string) {
outputFormat = s
}
// PushOutput 将当前 goroutine 的 shell 输出重定向到 w并可选覆盖格式。
// 返回的 restore 必须在同一 goroutine 调用。
func PushOutput(w io.Writer, format string) (restore func()) {
id := goroutineID()
prev, _ := outputStates.Load(id)
outputStates.Store(id, &outputState{writer: w, format: format})
return func() {
if prev != nil {
outputStates.Store(id, prev)
} else {
outputStates.Delete(id)
}
}
}
func currentWriter() io.Writer {
if v, ok := outputStates.Load(goroutineID()); ok {
if s := v.(*outputState); s != nil && s.writer != nil {
return s.writer
}
}
return os.Stdout
}
func currentFormat() string {
if v, ok := outputStates.Load(goroutineID()); ok {
if s := v.(*outputState); s != nil && s.format != "" {
return s.format
}
}
return outputFormat
}
func goroutineID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
i := bytes.IndexByte(b, ' ')
if i <= 0 {
return 0
}
n, _ := strconv.ParseUint(string(b[:i]), 10, 64)
return n
}
func PrintList(list *printutils.ListResult, columns []string) {
switch outputFormat {
w := currentWriter()
switch currentFormat() {
case OUTPUT_FORMAT_TABLE:
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) {

View File

@@ -32,10 +32,10 @@ import (
func init() {
type ParametersListOptions struct {
Name string `help:"List parameter of specificated name"`
NamespaceId string `help:"List parameter of specificated namespace id, ADMIN only"`
User string `help:"List parameter of specificated user id, ADMIN only" token:"user-id"`
Service string `help:"List parameter of specificated service id, ADMIN only"`
Name string `help:"List parameter of specified name"`
NamespaceId string `help:"List parameter of specified namespace id, ADMIN only"`
User string `help:"List parameter of specified user id, ADMIN only" token:"user-id"`
Service string `help:"List parameter of specified service id, ADMIN only"`
options.BaseListOptions
}
@@ -72,9 +72,9 @@ func init() {
})
type ParametersShowOptions struct {
NamespaceId string `help:"Show parameter of specificated namespace id, ADMIN only"`
User string `help:"Show parameter of specificated user id, ADMIN only"`
Service string `help:"Show parameter of specificated service id, ADMIN only"`
NamespaceId string `help:"Show parameter of specified namespace id, ADMIN only"`
User string `help:"Show parameter of specified user id, ADMIN only"`
Service string `help:"Show parameter of specified service id, ADMIN only"`
NAME string `help:"The name of parameter"`
}
@@ -109,8 +109,8 @@ func init() {
})
type ParametersCreateOptions struct {
User string `help:"Create parameter for specificated user id, ADMIN only"`
Service string `help:"Create parameter for specificated service id, ADMIN only"`
User string `help:"Create parameter for specified user id, ADMIN only"`
Service string `help:"Create parameter for specified service id, ADMIN only"`
NAME string `help:"The name of parameter"`
VALUE string `help:"The content of parameter"`
}
@@ -140,9 +140,9 @@ func init() {
})
type ParametersEditOptions struct {
NamespaceId string `help:"List parameter of specificated namespace id, ADMIN only"`
User string `help:"Update parameter of specificated user id, ADMIN only"`
Service string `help:"Update parameter of specificated service id, ADMIN only"`
NamespaceId string `help:"List parameter of specified namespace id, ADMIN only"`
User string `help:"Update parameter of specified user id, ADMIN only"`
Service string `help:"Update parameter of specified service id, ADMIN only"`
NAME string `help:"The name of parameter"`
}
@@ -222,8 +222,8 @@ func init() {
})
type ParametersDeleteOptions struct {
User string `help:"Delete parameter of specificated user id, ADMIN only"`
Service string `help:"Delete parameter of specificated service id, ADMIN only"`
User string `help:"Delete parameter of specified user id, ADMIN only"`
Service string `help:"Delete parameter of specified service id, ADMIN only"`
NAME string `help:"The name of parameter"`
}
@@ -248,8 +248,8 @@ func init() {
})
type ParameterCloneOptions struct {
User string `help:"Clone parameter of specificated user id"`
Service string `help:"Clone parameter of specificated service id"`
User string `help:"Clone parameter of specified user id"`
Service string `help:"Clone parameter of specified service id"`
NAME string `help:"The name of parameter"`
DestUser string `help:"destination user id of clone action"`
DestService string `help:"destination service id of clone action"`

View File

@@ -81,7 +81,7 @@ func newExecutor(options *BaseOptions) (ipmitool.IPMIExecutor, error) {
if options.Port > 0 {
port = options.Port
}
return ipmitool.NewLanPlusIPMIWithPort(options.HOST, options.User, options.PASSWD, port), nil
return ipmitool.NewLanPlusIPMIWithPort(options.HOST, options.User, options.PASSWD, port)
}
return nil, fmt.Errorf("Unsupported mode: %s", options.MODE)
}

View File

@@ -31,6 +31,7 @@ import (
"yunion.io/x/pkg/util/signalutils"
"yunion.io/x/pkg/utils"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/hostman/isolated_device"
"yunion.io/x/onecloud/pkg/hostman/isolated_device/container_device"
"yunion.io/x/onecloud/pkg/hostman/options"
@@ -67,7 +68,7 @@ func NewDaemon() (*Daemon, error) {
}
for i := range devCfg.Devices {
if devCfg.Devices[i].Type == isolated_device.ContainerDeviceTypeNvidiaMps {
dev, err := container_device.NewPCIGPURenderBaseDevice(devCfg.Devices[i].Path, 0, isolated_device.ContainerDeviceTypeNvidiaMps)
dev, err := container_device.NewPCIGPURenderBaseDevice(devCfg.Devices[i].Path, 0, api.GPU_TYPE, api.DEVICE_SHARING_MODE_MPS)
if err != nil {
return nil, errors.Wrapf(err, "parse pci device %s", devCfg.Devices[i].Path)
}

View File

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

View File

@@ -25,6 +25,12 @@ definitions:
description: 权限Id
type: string
x-go-name: CloudpolicyId
cloudpolicy_ids:
description: 权限Id列表
items:
type: string
type: array
x-go-name: CloudpolicyIds
type: object
x-go-package: yunion.io/x/onecloud/pkg/apis/cloudid
CloudgroupCreateInput:
@@ -108,6 +114,12 @@ definitions:
description: 权限Id
type: string
x-go-name: CloudpolicyId
cloudpolicy_ids:
description: 权限Id列表
items:
type: string
type: array
x-go-name: CloudpolicyIds
type: object
x-go-package: yunion.io/x/onecloud/pkg/apis/cloudid
CloudgroupDetails:

15
go.mod
View File

@@ -9,6 +9,10 @@ require (
github.com/Microsoft/go-winio v0.6.2
github.com/aliyun/alibaba-cloud-sdk-go v1.61.684
github.com/anacrolix/torrent v1.57.0
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/credentials v1.18.16
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.4
github.com/aws/smithy-go v1.24.2
github.com/benbjohnson/clock v1.0.0
github.com/bitly/go-simplejson v0.5.0
github.com/c-bata/go-prompt v0.2.4
@@ -90,6 +94,7 @@ require (
golang.org/x/net v0.43.0
golang.org/x/sync v0.16.0
golang.org/x/sys v0.35.0
golang.org/x/term v0.34.0
golang.org/x/text v0.28.0
golang.org/x/time v0.5.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
@@ -108,12 +113,12 @@ require (
k8s.io/cri-api v0.28.15
k8s.io/klog/v2 v2.90.1
moul.io/http2curl/v2 v2.3.0
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260616113329-0b3b4fbed80a
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260722023554-952117f57446
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005
yunion.io/x/jsonutils v1.0.1-0.20260715075349-615cfb44ff7c
yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900
yunion.io/x/pkg v1.10.4-0.20260422030155-01b100134978
yunion.io/x/pkg v1.10.4-0.20260720021919-5e34f63c5815
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1
yunion.io/x/sqlchemy v1.1.3-0.20251231025938-b0a38f6e9fab
yunion.io/x/structarg v0.0.0-20231017124457-df4d5009457c
@@ -169,10 +174,8 @@ require (
github.com/aokoli/goutils v1.0.1 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/aws/aws-sdk-go v1.44.194 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
@@ -182,12 +185,9 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 // indirect
github.com/aws/aws-sdk-go-v2/service/organizations v1.51.1-0.20260401181752-9074b3ddace9 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/basgys/goxml2json v1.1.1-0.20181031222924-996d9fc8d313 // indirect
github.com/beevik/etree v1.1.0 // indirect
@@ -403,7 +403,6 @@ require (
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/oauth2 v0.17.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/tools v0.35.0 // indirect
google.golang.org/api v0.167.0 // indirect
google.golang.org/appengine v1.6.8 // indirect

12
go.sum
View File

@@ -230,8 +230,6 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 h1:wuZ5uW2uhJR63zwNlqWH2W4aL4ZjeJP3o92/W+odDY4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9/go.mod h1:/G58M2fGszCrOzvJUkDdY8O9kycodunH4VdT5oBAqls=
github.com/aws/aws-sdk-go-v2/service/organizations v1.51.1-0.20260401181752-9074b3ddace9 h1:qi11riT4zVjV41Nh74zbBgK0EQbkbZG8rPm4shDFlJk=
github.com/aws/aws-sdk-go-v2/service/organizations v1.51.1-0.20260401181752-9074b3ddace9/go.mod h1:urLFj1twuR/h5T0wN/2/kmY1gxBFa1tTKr+c60lZ2fA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.4 h1:mUI3b885qJgfqKDUSj6RgbRqLdX0wGmg8ruM03zNfQA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.4/go.mod h1:6v8ukAxc7z4x4oBjGUsLnH7KGLY9Uhcgij19UJNkiMg=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 h1:A1oRkiSQOWstGh61y4Wc/yQ04sqrQZr1Si/oAXj20/s=
@@ -1783,13 +1781,11 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260616113329-0b3b4fbed80a h1:6R6t1tPpU/aR61wj5zCSC95KMglfM1ES85nbTneZSrk=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260616113329-0b3b4fbed80a/go.mod h1:rCz5Nds6Zi/5uaj51oRDYdj5fZ5uULmKbuBAgNLuI+w=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260722023554-952117f57446 h1:qDbFA6dfB2uxmOIx0kGiJUMg+xt4TiooEKx3URaHbN8=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260722023554-952117f57446/go.mod h1:DaGxV/EVSzixqiNcVwXMvCcNGcVf8K+GzmNIjnjtJS8=
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005 h1:3sWwcjGXGjG9mLBWa7AyLq+QSi0udTAx21pfVQRFMBE=
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005/go.mod h1:Uxuou9WQIeJXNpy7t2fPLL0BYLvLiMvGQwY7Qc6aSws=
yunion.io/x/jsonutils v0.0.0-20190625054549-a964e1e8a051/go.mod h1:4N0/RVzsYL3kH3WE/H1BjUQdFiWu50JGCFQuuy+Z634=
yunion.io/x/jsonutils v1.0.1-0.20250507052344-1abcf4f443b1 h1:/+THlvf/MvgCW+7KeCDCr33e81KSRa5JmdZ1IIyLOXQ=
yunion.io/x/jsonutils v1.0.1-0.20250507052344-1abcf4f443b1/go.mod h1:VK4Z93dgiKgAijcSqbMKmGaBMJuHulR16Hz4K015ZPo=
yunion.io/x/jsonutils v1.0.1-0.20260715075349-615cfb44ff7c h1:FthZM1upg4/yF4vmYheo/J7a738sGPjWPKk5kY88sH0=
yunion.io/x/jsonutils v1.0.1-0.20260715075349-615cfb44ff7c/go.mod h1:VK4Z93dgiKgAijcSqbMKmGaBMJuHulR16Hz4K015ZPo=
yunion.io/x/log v0.0.0-20190514041436-04ce53b17c6b/go.mod h1:+gauLs73omeJAPlsXcevLsJLKixV+sR/E7WSYTSx1fE=
@@ -1800,8 +1796,8 @@ yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900 h1:Hu/4ERvoWaN6aiFs4h4/yvVB
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900/go.mod h1:0vLkNEhlmA64HViPBAnSTUMrx5QP1CLsxXmxDKQ80tc=
yunion.io/x/pkg v0.0.0-20190620104149-945c25821dbf/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v1.10.4-0.20260422030155-01b100134978 h1:+BS9DValwi0xHa92xrB+RR6Cxp7bDbXVysa/KiC18rY=
yunion.io/x/pkg v1.10.4-0.20260422030155-01b100134978/go.mod h1:0Bwxqd9MA3ACi119/l02FprY/o9gHahmYC2bsSbnVpM=
yunion.io/x/pkg v1.10.4-0.20260720021919-5e34f63c5815 h1:EkdBOh0J26NeFlgEh0Shrg+S+4Y86Msx6TQtaayVoZo=
yunion.io/x/pkg v1.10.4-0.20260720021919-5e34f63c5815/go.mod h1:0Bwxqd9MA3ACi119/l02FprY/o9gHahmYC2bsSbnVpM=
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1 h1:1KJ3YYinydPHpDEQRXdr/T8SYcKZ5Er+m489H+PnaQ4=
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo=
yunion.io/x/sqlchemy v1.1.3-0.20251231025938-b0a38f6e9fab h1:5m/bSzW3uTAk83rp9eethbYsxJFYInFVeU1RDkteW4E=

View File

@@ -1609,6 +1609,7 @@ var messageKeyToIndex = map[string]int{
"loadbalancer is already associated with eip": 780,
"loadbalancer is locked, cannot delete": 1366,
"local host storage is not empty": 1177,
"local_path SKU with HAMi requires per-GPU VRAM: set devices[].memory_mb on the LLM SKU": 2131,
"locally stored disks do not support detaching": 394,
"locally stored system disks do not support changing configuration": 391,
"login_account exceeds 32 characters": 820,
@@ -2172,7 +2173,7 @@ var messageKeyToIndex = map[string]int{
"zone_ids must be in the same cloudregion": 1582,
}
var en_USIndex = []uint32{ // 2132 elements
var en_USIndex = []uint32{ // 2133 elements
// Entry 0 - 1F
0x00000000, 0x0000001b, 0x00000034, 0x00000049,
0x00000067, 0x0000007a, 0x00000099, 0x000000d4,
@@ -2773,9 +2774,10 @@ var en_USIndex = []uint32{ // 2132 elements
0x00011c60, 0x00011c78, 0x00011c8e, 0x00011c99,
0x00011ca7, 0x00011cba, 0x00011ccf, 0x00011ce4,
0x00011cf3, 0x00011d12, 0x00011d2c, 0x00011d41,
} // Size: 8552 bytes
0x00011d98,
} // Size: 8532 bytes
const en_USData string = "" + // Size: 73025 bytes
const en_USData string = "" + // Size: 73112 bytes
"\x02not found tenantId in body\x02failed to change project\x02get passwo" +
"rd in body\x02username or password is empty\x02missing credential\x02inc" +
"orrect username or password\x02The user has been locked, please contact " +
@@ -3881,9 +3883,10 @@ const en_USData string = "" + // Size: 73025 bytes
"vn controller configuration\x02unmarshal input fail %s\x02invalid charac" +
"ters %s\x02bad config\x02network error\x02ssh protocol error\x02invalid " +
"vpc provider\x02invalid ovn database\x02user not found\x02check name dup" +
"lication fail %s\x02getReferenceCount fail %s\x02policy is referenced"
"lication fail %s\x02getReferenceCount fail %s\x02policy is referenced" +
"\x02local_path SKU with HAMi requires per-GPU VRAM: set devices[].memory_mb on the LLM SKU"
var zh_CNIndex = []uint32{ // 2132 elements
var zh_CNIndex = []uint32{ // 2133 elements
// Entry 0 - 1F
0x00000000, 0x0000001b, 0x0000002e, 0x00000049,
0x00000062, 0x00000075, 0x00000094, 0x000000bf,
@@ -4484,9 +4487,10 @@ var zh_CNIndex = []uint32{ // 2132 elements
0x00010c9c, 0x00010cb1, 0x00010cc7, 0x00010cd7,
0x00010ce4, 0x00010cf4, 0x00010d0a, 0x00010d20,
0x00010d30, 0x00010d4e, 0x00010d71, 0x00010d8a,
} // Size: 8552 bytes
0x00010e12,
} // Size: 8532 bytes
const zh_CNData string = "" + // Size: 69002 bytes
const zh_CNData string = "" + // Size: 69138 bytes
"\x02获取tenantId参数失败\x02切换项目失败\x02获取password参数失败\x02用户名或密码为空\x02缺少认证信息\x02" +
"用户名或者密码不正确\x02用户已经被锁定请联系管理员\x02用户已经被禁用请联系管理员\x02用户已经被禁用或处于不可用状态\x02无" +
"效的认证信息\x02获取请求的JSON内容失败%v\x02此用户禁止从Web控制台登录\x02请求内容为空\x02缺少id参数\x02fe" +
@@ -5070,6 +5074,7 @@ const zh_CNData string = "" + // Size: 69002 bytes
"告警条件类型: %s\x02解析URL %q出错%v\x02无效的IP\x02停止dap搜索\x02ovn controller\x02o" +
"vn controller配置\x02解析input失败 %s\x02不支持的字符 %s\x02无效的配置\x02网络错误\x02ssh协议错误" +
"\x02无效的vpc provider\x02无效的ovn database\x02找不到用户\x02检查重复名称失败%s\x02getRef" +
"erenceCount调用出错%s\x02权限定义正被使用"
"erenceCount调用出错%s\x02权限定义正被使用" +
"\x02主机本地模型local_path推理模板使用 HAMi 时必须设置每卡显存:请到推理模板中填写 GPU 显存MiB"
// Total table size 159131 bytes (155KiB); checksum: A1EC1008

View File

@@ -9951,6 +9951,11 @@
"message": "auto_gpu_memory_utilization requires GPU devices: configure GPU on the LLM SKU",
"translation": "开启 auto_gpu_memory_utilization 需要先在推理模板中配置 GPU 设备"
},
{
"id": "local_path SKU with HAMi requires per-GPU VRAM: set devices[].memory_mb on the LLM SKU",
"message": "local_path SKU with HAMi requires per-GPU VRAM: set devices[].memory_mb on the LLM SKU",
"translation": "主机本地模型local_path推理模板使用 HAMi 时必须设置每卡显存:请到推理模板中填写 GPU 显存MiB"
},
{
"id": "LLM is not running",
"message": "LLM is not running",

View File

@@ -33,6 +33,13 @@ type aiKeyHealthState struct {
cooldownUntil time.Time
}
// aiKeyHealthSnapshot is a read-only view for error messages (not exported as API).
type aiKeyHealthSnapshot struct {
score int
inCooldown bool
remainingSec int
}
var (
aiKeyHealthMu sync.RWMutex
aiKeyHealth = map[string]*aiKeyHealthState{}
@@ -57,6 +64,27 @@ func getAiKeyHealth(keyId string) *aiKeyHealthState {
return st
}
// aiKeyHealthInfo returns a read-only snapshot for diagnostics / error text.
func aiKeyHealthInfo(keyId string) aiKeyHealthSnapshot {
if keyId == "" {
return aiKeyHealthSnapshot{score: aiKeyHealthMaxScore}
}
st := getAiKeyHealth(keyId)
now := time.Now()
aiKeyHealthMu.RLock()
defer aiKeyHealthMu.RUnlock()
info := aiKeyHealthSnapshot{score: st.score}
if !st.cooldownUntil.IsZero() && now.Before(st.cooldownUntil) {
info.inCooldown = true
sec := int(st.cooldownUntil.Sub(now).Seconds())
if sec < 1 {
sec = 1
}
info.remainingSec = sec
}
return info
}
// dynamicAiKeyWeightMultiplier returns 0-100 applied to configured ai_key.weight (100 = full weight).
func dynamicAiKeyWeightMultiplier(keyId string) int {
if keyId == "" {
@@ -75,7 +103,12 @@ func dynamicAiKeyWeightMultiplier(keyId string) int {
st.score = aiKeyHealthMaxScore / 2
}
}
// score<=0 without an active cooldown would permanently exclude the key;
// start a cooldown so it can recover via the path above after the period.
if st.score <= 0 {
if st.cooldownUntil.IsZero() {
st.cooldownUntil = now.Add(aiKeyHealthCooldownPeriod)
}
return 0
}
if st.score > aiKeyHealthMaxScore {
@@ -113,7 +146,8 @@ func RecordAiKeyFailure(keyId string, statusCode int) {
if st.score < 0 {
st.score = 0
}
if st.consecutiveFails >= aiKeyHealthCooldownAfter {
// Enter cooldown on streak or when score is exhausted (avoids permanent blacklist).
if st.consecutiveFails >= aiKeyHealthCooldownAfter || st.score <= 0 {
st.cooldownUntil = time.Now().Add(aiKeyHealthCooldownPeriod)
st.score = 0
}

View File

@@ -0,0 +1,215 @@
// 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"
"time"
api "yunion.io/x/onecloud/pkg/apis/aiproxy"
)
func resetAiKeyHealthForTest() {
aiKeyHealthMu.Lock()
aiKeyHealth = map[string]*aiKeyHealthState{}
aiKeyHealthMu.Unlock()
}
func TestRecordAiKeyFailure_ConsecutiveTriggersCooldown(t *testing.T) {
resetAiKeyHealthForTest()
const id = "key-consec"
for i := 0; i < aiKeyHealthCooldownAfter; i++ {
RecordAiKeyFailure(id, 429)
}
if mul := dynamicAiKeyWeightMultiplier(id); mul != 0 {
t.Fatalf("expected multiplier 0 during cooldown, got %d", mul)
}
info := aiKeyHealthInfo(id)
if !info.inCooldown {
t.Fatal("expected inCooldown after consecutive failures")
}
if info.score != 0 {
t.Fatalf("expected score 0, got %d", info.score)
}
}
func TestRecordAiKeyFailure_IntermittentScoreExhaustionEntersCooldown(t *testing.T) {
resetAiKeyHealthForTest()
const id = "key-intermittent"
// fail -25 / success +10 with consecutiveFails reset: score can hit 0 without 3 consecutive fails.
for {
info := aiKeyHealthInfo(id)
if info.score <= 0 || info.inCooldown {
break
}
RecordAiKeyFailure(id, 500)
info = aiKeyHealthInfo(id)
if info.score <= 0 || info.inCooldown {
break
}
RecordAiKeySuccess(id)
if dynamicAiKeyWeightMultiplier(id) <= 0 {
t.Fatal("unexpected zero multiplier after success")
}
}
info := aiKeyHealthInfo(id)
if !info.inCooldown {
t.Fatalf("expected cooldown after score exhaustion, got score=%d inCooldown=%v", info.score, info.inCooldown)
}
if mul := dynamicAiKeyWeightMultiplier(id); mul != 0 {
t.Fatalf("expected multiplier 0 during cooldown, got %d", mul)
}
}
func TestDynamicAiKeyWeightMultiplier_RecoversAfterCooldown(t *testing.T) {
resetAiKeyHealthForTest()
const id = "key-recover"
RecordAiKeyFailure(id, 429)
RecordAiKeyFailure(id, 429)
RecordAiKeyFailure(id, 429)
st := getAiKeyHealth(id)
aiKeyHealthMu.Lock()
st.cooldownUntil = time.Now().Add(-time.Second)
aiKeyHealthMu.Unlock()
mul := dynamicAiKeyWeightMultiplier(id)
if mul < aiKeyHealthMaxScore/2 {
t.Fatalf("expected recovered multiplier >= %d, got %d", aiKeyHealthMaxScore/2, mul)
}
info := aiKeyHealthInfo(id)
if info.inCooldown {
t.Fatal("expected cooldown cleared after expiry")
}
if info.score < aiKeyHealthMaxScore/2 {
t.Fatalf("expected score >= %d, got %d", aiKeyHealthMaxScore/2, info.score)
}
}
func TestDynamicAiKeyWeightMultiplier_StuckScoreStartsCooldown(t *testing.T) {
resetAiKeyHealthForTest()
const id = "key-stuck"
st := getAiKeyHealth(id)
aiKeyHealthMu.Lock()
st.score = 0
st.cooldownUntil = time.Time{}
aiKeyHealthMu.Unlock()
if mul := dynamicAiKeyWeightMultiplier(id); mul != 0 {
t.Fatalf("expected 0, got %d", mul)
}
info := aiKeyHealthInfo(id)
if !info.inCooldown {
t.Fatal("expected fallback cooldown for stuck score=0")
}
aiKeyHealthMu.Lock()
st.cooldownUntil = time.Now().Add(-time.Second)
aiKeyHealthMu.Unlock()
mul := dynamicAiKeyWeightMultiplier(id)
if mul < aiKeyHealthMaxScore/2 {
t.Fatalf("expected recovery after fallback cooldown, got %d", mul)
}
}
func TestRecordAiKeySuccess_ClearsCooldown(t *testing.T) {
resetAiKeyHealthForTest()
const id = "key-success"
RecordAiKeyFailure(id, 401)
RecordAiKeyFailure(id, 401)
RecordAiKeyFailure(id, 401)
RecordAiKeySuccess(id)
info := aiKeyHealthInfo(id)
if info.inCooldown {
t.Fatal("success should clear cooldown")
}
if mul := dynamicAiKeyWeightMultiplier(id); mul <= 0 {
t.Fatalf("expected positive multiplier after success, got %d", mul)
}
}
func TestAiKeySkipReason(t *testing.T) {
resetAiKeyHealthForTest()
t.Run("already tried", func(t *testing.T) {
k := &SAiKey{Secret: "sk-test"}
k.Id = "id-tried"
k.Name = "tried-key"
reason := aiKeySkipReason(k, "deepseek-v4-pro", map[string]bool{"id-tried": true})
if !strings.Contains(reason, "already tried") {
t.Fatalf("got %q", reason)
}
})
t.Run("cooldown", func(t *testing.T) {
k := &SAiKey{Secret: "sk-test", Weight: 1}
k.Id = "id-cd"
k.Name = "cd-key"
RecordAiKeyFailure(k.Id, 429)
RecordAiKeyFailure(k.Id, 429)
RecordAiKeyFailure(k.Id, 429)
reason := aiKeySkipReason(k, "deepseek-v4-pro", nil)
if !strings.Contains(reason, "cooldown") || !strings.Contains(reason, "remaining") {
t.Fatalf("got %q", reason)
}
})
t.Run("routing", func(t *testing.T) {
resetAiKeyHealthForTest()
k := &SAiKey{
Secret: "sk-test",
Weight: 1,
Routing: &api.SAiKeyRouting{
AllowedModelKeys: []string{"other-model"},
},
}
k.Id = "id-route"
k.Name = "route-key"
reason := aiKeySkipReason(k, "deepseek-v4-pro", nil)
if !strings.Contains(reason, "model not allowed by routing") {
t.Fatalf("got %q", reason)
}
})
t.Run("empty secret", func(t *testing.T) {
k := &SAiKey{Secret: " "}
k.Name = "empty-key"
reason := aiKeySkipReason(k, "m", nil)
if !strings.Contains(reason, "empty secret") {
t.Fatalf("got %q", reason)
}
})
t.Run("usable", func(t *testing.T) {
resetAiKeyHealthForTest()
k := &SAiKey{Secret: "sk-ok", Weight: 1}
k.Id = "id-ok"
k.Name = "ok-key"
if reason := aiKeySkipReason(k, "deepseek-v4-pro", nil); reason != "" {
t.Fatalf("expected empty reason, got %q", reason)
}
})
}
func TestFormatAiKeySkipReasons_Truncates(t *testing.T) {
reasons := make([]string, maxAiKeySkipReasonsInError+3)
for i := range reasons {
reasons[i] = "r"
}
out := formatAiKeySkipReasons(reasons)
if !strings.Contains(out, "and 3 more") {
t.Fatalf("got %q", out)
}
}

View File

@@ -16,6 +16,7 @@ package models
import (
"crypto/rand"
"fmt"
"math/big"
"strings"
@@ -25,6 +26,8 @@ import (
"yunion.io/x/onecloud/pkg/httperrors"
)
const maxAiKeySkipReasonsInError = 8
func effectiveAiKeyRoutingWeight(r *api.SAiKeyRouting) int {
if r == nil || r.Weight <= 0 {
return 0
@@ -84,6 +87,58 @@ func aiKeyRoutingAcceptsModel(r *api.SAiKeyRouting, reqModel string) bool {
return true
}
func aiKeyLabel(k *SAiKey) string {
if k == nil {
return "?"
}
if n := strings.TrimSpace(k.Name); n != "" {
return n
}
if id := strings.TrimSpace(k.Id); id != "" {
return id
}
return "?"
}
// aiKeySkipReason returns why an ai_key cannot be used for modelKey, or "" if usable.
func aiKeySkipReason(k *SAiKey, modelKey string, exclude map[string]bool) string {
if k == nil {
return "?: nil ai_key"
}
label := aiKeyLabel(k)
if strings.TrimSpace(k.Secret) == "" {
return label + ": empty secret"
}
if exclude != nil && exclude[k.Id] {
return label + ": already tried"
}
if baseAiKeyWeight(k) <= 0 {
return label + ": weight=0"
}
if effectiveAiKeyWeight(k) <= 0 {
info := aiKeyHealthInfo(k.Id)
if info.inCooldown {
return fmt.Sprintf("%s: cooldown %ds remaining (health_score=%d)", label, info.remainingSec, info.score)
}
return fmt.Sprintf("%s: health_score=%d", label, info.score)
}
if !aiKeyRoutingAcceptsModel(k.Routing, modelKey) {
return label + ": model not allowed by routing (allowed_model_keys/blocked_model_keys)"
}
return ""
}
func formatAiKeySkipReasons(reasons []string) string {
if len(reasons) == 0 {
return ""
}
if len(reasons) <= maxAiKeySkipReasonsInError {
return strings.Join(reasons, "; ")
}
shown := strings.Join(reasons[:maxAiKeySkipReasonsInError], "; ")
return fmt.Sprintf("%s; and %d more", shown, len(reasons)-maxAiKeySkipReasonsInError)
}
func pickWeightedAiKey(candidates []*SAiKey) *SAiKey {
if len(candidates) == 0 {
return nil
@@ -145,21 +200,18 @@ func resolveUpstreamAPIKeyExcluding(prov *SAiProvider, modelKey string, exclude
candidates := make([]*SAiKey, 0, len(keys))
hasSecretKey := false
skipReasons := make([]string, 0, len(keys))
for i := range keys {
k := &keys[i]
if strings.TrimSpace(k.Secret) == "" {
continue
}
hasSecretKey = true
if exclude != nil && exclude[k.Id] {
if reason := aiKeySkipReason(k, modelKey, exclude); reason != "" {
skipReasons = append(skipReasons, reason)
continue
}
if effectiveAiKeyWeight(k) <= 0 {
continue
}
if aiKeyRoutingAcceptsModel(k.Routing, modelKey) {
candidates = append(candidates, k)
}
candidates = append(candidates, k)
}
if len(candidates) > 0 {
chosen := pickWeightedAiKey(candidates)
@@ -173,7 +225,11 @@ func resolveUpstreamAPIKeyExcluding(prov *SAiProvider, modelKey string, exclude
}, nil
}
if hasSecretKey {
return nil, errors.Wrapf(httperrors.ErrInvalidStatus, "no available ai_key for catalog model %q (check weight, cooldown, allowed_model_keys)", modelKey)
detail := formatAiKeySkipReasons(skipReasons)
if detail != "" {
return nil, errors.Wrapf(httperrors.ErrInvalidStatus, "no available ai_key for catalog model %q: %s", modelKey, detail)
}
return nil, errors.Wrapf(httperrors.ErrInvalidStatus, "no available ai_key for catalog model %q", modelKey)
}
return nil, errors.Wrap(httperrors.ErrInvalidStatus, "add an enabled ai_key with secret for this provider")
}

View File

@@ -225,14 +225,90 @@ func TestResponsesStreamConverterText(t *testing.T) {
t.Fatal(err)
}
foundCompleted := false
foundOutputItemDone := false
for _, e := range end {
if e.Event == "response.completed" {
foundCompleted = true
}
if e.Event == "response.output_item.done" {
foundOutputItemDone = true
}
}
if !foundCompleted {
t.Fatalf("events = %+v", end)
}
if !foundOutputItemDone {
t.Fatalf("expected response.output_item.done in %v", responsesEventNames(end))
}
}
func TestResponsesStreamConverterReasoningAndToolDoneEvents(t *testing.T) {
conv := NewResponsesStreamConverter("deepseek-v4-flash", nil)
chunks := [][]byte{
[]byte(`{"id":"chatcmpl-1","model":"deepseek-v4-flash","choices":[{"delta":{"role":"assistant","reasoning_content":"think"}}]}`),
[]byte(`{"id":"chatcmpl-1","model":"deepseek-v4-flash","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"exec_command","arguments":"{\"cmd\":\"ls\"}"}}]}}]}`),
[]byte(`{"id":"chatcmpl-1","model":"deepseek-v4-flash","choices":[{"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}`),
}
var events []ResponsesStreamEvent
for _, chunk := range chunks {
evs, err := conv.Feed(chunk, false)
if err != nil {
t.Fatal(err)
}
events = append(events, evs...)
}
end, err := conv.Feed(nil, true)
if err != nil {
t.Fatal(err)
}
events = append(events, end...)
counts := map[string]int{}
for _, e := range events {
counts[e.Event]++
}
if counts["response.reasoning_summary_part.done"] != 1 {
t.Fatalf("reasoning_summary_part.done = %d, events=%v", counts["response.reasoning_summary_part.done"], responsesEventNames(events))
}
if counts["response.function_call_arguments.done"] != 1 {
t.Fatalf("function_call_arguments.done = %d, events=%v", counts["response.function_call_arguments.done"], responsesEventNames(events))
}
if counts["response.output_item.done"] < 2 {
t.Fatalf("output_item.done = %d, want at least 2, events=%v", counts["response.output_item.done"], responsesEventNames(events))
}
if counts["response.completed"] != 1 {
t.Fatalf("completed = %d, events=%v", counts["response.completed"], responsesEventNames(events))
}
// output_item.done for tools must appear before response.completed
lastToolDone := -1
completedAt := -1
for i, e := range events {
if e.Event == "response.output_item.done" {
var payload map[string]interface{}
if err := json.Unmarshal(e.Data, &payload); err != nil {
t.Fatal(err)
}
item, _ := payload["item"].(map[string]interface{})
if item["type"] == "function_call" {
lastToolDone = i
}
}
if e.Event == "response.completed" {
completedAt = i
}
}
if lastToolDone < 0 || completedAt < 0 || lastToolDone >= completedAt {
t.Fatalf("tool output_item.done must precede completed: toolDone=%d completed=%d", lastToolDone, completedAt)
}
}
func responsesEventNames(events []ResponsesStreamEvent) []string {
out := make([]string, len(events))
for i, e := range events {
out[i] = e.Event
}
return out
}
func TestNonStreamChatCompletionToStreamPayloads(t *testing.T) {

View File

@@ -17,6 +17,7 @@ package openai
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/google/uuid"
@@ -32,23 +33,27 @@ type ResponsesStreamEvent struct {
// ResponsesStreamConverter converts OpenAI chat.completion.chunk SSE to Responses SSE events.
type ResponsesStreamConverter struct {
requestModel string
toolMap CodexToolMap
created bool
completed bool
seq int64
responseID string
model string
outputIndex int
textItemID string
reasonItemID string
textStarted bool
reasonStarted bool
textBuf strings.Builder
reasonBuf strings.Builder
inputTokens int
outputTokens int
activeTools map[int]*responsesStreamToolState
requestModel string
toolMap CodexToolMap
created bool
completed bool
seq int64
responseID string
model string
outputIndex int
textItemID string
reasonItemID string
textOutputIndex int
reasonOutputIndex int
textStarted bool
reasonStarted bool
textDone bool
reasonDone bool
textBuf strings.Builder
reasonBuf strings.Builder
inputTokens int
outputTokens int
activeTools map[int]*responsesStreamToolState
}
type responsesStreamToolState struct {
@@ -59,6 +64,7 @@ type responsesStreamToolState struct {
namespace string
argsBuf strings.Builder
added bool
finalized bool
}
// NewResponsesStreamConverter creates stream state for one Responses response.
@@ -190,6 +196,7 @@ func (s *ResponsesStreamConverter) appendText(text string) ([]ResponsesStreamEve
var out []ResponsesStreamEvent
if !s.textStarted {
s.textStarted = true
s.textOutputIndex = s.outputIndex
s.textItemID = fmt.Sprintf("msg_%d", s.outputIndex)
added, err := s.outputItemAdded("message", s.textItemID, map[string]interface{}{
"type": "message",
@@ -222,6 +229,7 @@ func (s *ResponsesStreamConverter) appendReasoning(text string) ([]ResponsesStre
var out []ResponsesStreamEvent
if !s.reasonStarted {
s.reasonStarted = true
s.reasonOutputIndex = s.outputIndex
s.reasonItemID = fmt.Sprintf("rs_%d", s.outputIndex)
added, err := s.outputItemAdded("reasoning", s.reasonItemID, map[string]interface{}{
"type": "reasoning",
@@ -362,11 +370,182 @@ func (s *ResponsesStreamConverter) textDelta(text string) (ResponsesStreamEvent,
return ResponsesStreamEvent{Event: "response.output_text.delta", Data: b}, nil
}
func (s *ResponsesStreamConverter) emitCompleted() ([]ResponsesStreamEvent, error) {
if s.completed {
func (s *ResponsesStreamConverter) emitFinalizeEvents() ([]ResponsesStreamEvent, error) {
var out []ResponsesStreamEvent
if s.reasonStarted && !s.reasonDone {
events, err := s.emitReasoningDone()
if err != nil {
return nil, err
}
out = append(out, events...)
}
if s.textStarted && !s.textDone {
events, err := s.emitTextDone()
if err != nil {
return nil, err
}
out = append(out, events...)
}
toolIndexes := make([]int, 0, len(s.activeTools))
for idx := range s.activeTools {
toolIndexes = append(toolIndexes, idx)
}
sort.Ints(toolIndexes)
for _, idx := range toolIndexes {
st := s.activeTools[idx]
if st == nil || !st.added || st.finalized {
continue
}
events, err := s.emitToolDone(st)
if err != nil {
return nil, err
}
out = append(out, events...)
}
return out, nil
}
func (s *ResponsesStreamConverter) emitReasoningDone() ([]ResponsesStreamEvent, error) {
if !s.reasonStarted || s.reasonDone {
return nil, nil
}
s.completed = true
s.reasonDone = true
var out []ResponsesStreamEvent
partDone := map[string]interface{}{
"type": "response.reasoning_summary_part.done",
"sequence_number": s.nextSeq(),
"item_id": s.reasonItemID,
"output_index": s.reasonOutputIndex,
"summary_index": 0,
}
b, err := json.Marshal(partDone)
if err != nil {
return nil, err
}
out = append(out, ResponsesStreamEvent{Event: "response.reasoning_summary_part.done", Data: b})
item := map[string]interface{}{
"type": "reasoning",
"id": s.reasonItemID,
"status": "completed",
"summary": []map[string]interface{}{
{"type": "text", "text": s.reasonBuf.String()},
},
}
itemDone, err := s.outputItemDoneAt(s.reasonOutputIndex, item)
if err != nil {
return nil, err
}
out = append(out, itemDone)
return out, nil
}
func (s *ResponsesStreamConverter) emitTextDone() ([]ResponsesStreamEvent, error) {
if !s.textStarted || s.textDone {
return nil, nil
}
s.textDone = true
text := s.textBuf.String()
var out []ResponsesStreamEvent
textDone := map[string]interface{}{
"type": "response.output_text.done",
"sequence_number": s.nextSeq(),
"item_id": s.textItemID,
"output_index": s.textOutputIndex,
"content_index": 0,
"text": text,
}
b, err := json.Marshal(textDone)
if err != nil {
return nil, err
}
out = append(out, ResponsesStreamEvent{Event: "response.output_text.done", Data: b})
partDone := map[string]interface{}{
"type": "response.content_part.done",
"sequence_number": s.nextSeq(),
"item_id": s.textItemID,
"output_index": s.textOutputIndex,
"content_index": 0,
"part": map[string]interface{}{"type": "output_text", "text": text},
}
b, err = json.Marshal(partDone)
if err != nil {
return nil, err
}
out = append(out, ResponsesStreamEvent{Event: "response.content_part.done", Data: b})
item := map[string]interface{}{
"type": "message",
"id": s.textItemID,
"status": "completed",
"role": "assistant",
"content": []map[string]interface{}{
{"type": "output_text", "text": text},
},
}
itemDone, err := s.outputItemDoneAt(s.textOutputIndex, item)
if err != nil {
return nil, err
}
out = append(out, itemDone)
return out, nil
}
func (s *ResponsesStreamConverter) emitToolDone(st *responsesStreamToolState) ([]ResponsesStreamEvent, error) {
if st == nil || !st.added || st.finalized {
return nil, nil
}
st.finalized = true
args := st.argsBuf.String()
var out []ResponsesStreamEvent
argsDone := map[string]interface{}{
"type": "response.function_call_arguments.done",
"sequence_number": s.nextSeq(),
"item_id": st.itemID,
"output_index": st.outputIndex,
"arguments": args,
}
b, err := json.Marshal(argsDone)
if err != nil {
return nil, err
}
out = append(out, ResponsesStreamEvent{Event: "response.function_call_arguments.done", Data: b})
item := map[string]interface{}{
"type": "function_call",
"id": st.itemID,
"status": "completed",
"call_id": st.callID,
"name": st.name,
"arguments": args,
}
if st.namespace != "" {
item["namespace"] = st.namespace
}
itemDone, err := s.outputItemDoneAt(st.outputIndex, item)
if err != nil {
return nil, err
}
out = append(out, itemDone)
return out, nil
}
func (s *ResponsesStreamConverter) outputItemDoneAt(outputIndex int, item map[string]interface{}) (ResponsesStreamEvent, error) {
data := map[string]interface{}{
"type": "response.output_item.done",
"sequence_number": s.nextSeq(),
"output_index": outputIndex,
"item": item,
}
b, err := json.Marshal(data)
if err != nil {
return ResponsesStreamEvent{}, err
}
return ResponsesStreamEvent{Event: "response.output_item.done", Data: b}, nil
}
func (s *ResponsesStreamConverter) buildCompletedOutput() []interface{} {
output := make([]interface{}, 0)
if s.reasonBuf.Len() > 0 {
output = append(output, map[string]interface{}{
@@ -385,7 +564,13 @@ func (s *ResponsesStreamConverter) emitCompleted() ([]ResponsesStreamEvent, erro
"content": []map[string]interface{}{{"type": "text", "text": s.textBuf.String()}},
})
}
for _, st := range s.activeTools {
toolIndexes := make([]int, 0, len(s.activeTools))
for idx := range s.activeTools {
toolIndexes = append(toolIndexes, idx)
}
sort.Ints(toolIndexes)
for _, idx := range toolIndexes {
st := s.activeTools[idx]
if st == nil || !st.added {
continue
}
@@ -401,14 +586,26 @@ func (s *ResponsesStreamConverter) emitCompleted() ([]ResponsesStreamEvent, erro
}
output = append(output, item)
}
status := "completed"
return output
}
func (s *ResponsesStreamConverter) emitCompleted() ([]ResponsesStreamEvent, error) {
if s.completed {
return nil, nil
}
finalizeEvents, err := s.emitFinalizeEvents()
if err != nil {
return nil, err
}
s.completed = true
output := s.buildCompletedOutput()
data := map[string]interface{}{
"type": "response.completed",
"sequence_number": s.nextSeq(),
"response": map[string]interface{}{
"id": s.responseID,
"object": "response",
"status": status,
"status": "completed",
"model": s.model,
"output": output,
"output_text": s.textBuf.String(),
@@ -423,7 +620,8 @@ func (s *ResponsesStreamConverter) emitCompleted() ([]ResponsesStreamEvent, erro
if err != nil {
return nil, err
}
return []ResponsesStreamEvent{{Event: "response.completed", Data: b}}, nil
out := append(finalizeEvents, ResponsesStreamEvent{Event: "response.completed", Data: b})
return out, nil
}
// ToProviderChunks maps internal events to providerapi chunks.

View File

@@ -65,6 +65,10 @@ func (h *SBackendServiceProxyHandler) requestManipulator(ctx context.Context, r
if slashPos <= 0 {
return r, httperrors.NewBadRequestError("invalid request URL %s", r.URL.Path)
}
serviceName := path[:slashPos]
// Tell upstream (e.g. mcp-server SSE) the external path prefix so endpoint
// events point clients back through the gateway: /api/s/<service>/message
r.Header.Set("X-Forwarded-Prefix", "/api/s/"+serviceName)
path = path[slashPos:]
if strings.HasPrefix(path, "/r/") {
path = path[len("/r/"):]

View File

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

View File

@@ -46,6 +46,7 @@ type IModelSet interface {
NewModel() db.IModel
AddModel(db.IModel)
Copy() IModelSet
IncludeDetails() bool
}
type IDBModelSet interface {
@@ -107,7 +108,7 @@ func syncModelSets(mssOld IModelSets, s *mcclient.ClientSession, opt *Options) (
ModelSet: msNew,
BatchListSize: opt.ListBatchSize,
IncludeDetails: opt.IncludeDetails,
IncludeDetails: msNew.IncludeDetails(),
IncludeEmulated: includeEmulated,
InCludeOtherCloudEnv: opt.IncludeOtherCloudEnv,
})
@@ -134,7 +135,7 @@ func SyncDBModelSets(mssOld IModelSets, s *mcclient.ClientSession, opt *Options)
ModelSet: msNew,
BatchListSize: opt.ListBatchSize,
IncludeDetails: opt.IncludeDetails,
IncludeDetails: msNew.IncludeDetails(),
IncludeEmulated: includeEmulated,
InCludeOtherCloudEnv: opt.IncludeOtherCloudEnv,
}

View File

@@ -122,6 +122,8 @@ type CloudgroupAttachPolicyInput struct {
// 权限Id
CloudpolicyId string `json:"cloudpolicy_id"`
// 权限Id列表
CloudpolicyIds []string `json:"cloudpolicy_ids"`
}
type CloudgroupSetUsersInput struct {
@@ -140,6 +142,8 @@ type CloudgroupDetachPolicyInput struct {
// 权限Id
CloudpolicyId string `json:"cloudpolicy_id"`
// 权限Id列表
CloudpolicyIds []string `json:"cloudpolicy_ids"`
}
type CloudgroupSyncstatusInput struct {

View File

@@ -315,17 +315,22 @@ type IsolatedDeviceConfig struct {
Id string `json:"id"`
DevType string `json:"dev_type"`
Model string `json:"model"`
SharingMode string `json:"sharing_mode"`
Vendor string `json:"vendor"`
NetworkIndex *int `json:"network_index"`
WireId string `json:"wire_id"`
DiskIndex *int8 `json:"disk_index"`
DevicePath string `json:"device_path"`
GpuType string `json:"gpu_type"`
// MemoryMb is the minimum on-device memory in MiB required from the
// candidate isolated_device (e.g. NVIDIA GPU VRAM). 0 means no constraint.
// The scheduler excludes devices whose memory_size > 0 and is below this
// threshold; devices with memory_size == 0 are treated as unknown and
// allowed through to avoid penalising hosts that haven't reported yet.
MemoryMb int `json:"memory_mb,omitempty"`
// Memory request for Devices allocate by Memory size
MemoryRequest int `json:"memory_request,omitempty"`
SmUtilLimit int `json:"sm_util_limit,omitempty"`
}
type BaremetalDiskConfig struct {

View File

@@ -40,7 +40,9 @@ const (
CONTAINER_DEV_NVIDIA_GPU = "NVIDIA_GPU"
CONTAINER_DEV_NVIDIA_MPS = "NVIDIA_MPS"
CONTAINER_DEV_NVIDIA_GPU_SHARE = "NVIDIA_GPU_SHARE"
CONTAINER_DEV_NVIDIA_HAMI = "NVIDIA_HAMI"
CONTAINER_DEV_ASCEND_NPU = "ASCEND_NPU"
CONTAINER_DEV_ASCEND_NPU_HAMI = "ASCEND_NPU_HAMI"
CONTAINER_DEV_VASTAITECH_GPU = "VASTAITECH_GPU"
)
@@ -49,14 +51,16 @@ var (
CONTAINER_DEV_CPH_AMD_GPU,
CONTAINER_DEV_NVIDIA_GPU,
CONTAINER_DEV_NVIDIA_MPS,
CONTAINER_DEV_NVIDIA_HAMI,
CONTAINER_DEV_NVIDIA_GPU_SHARE,
CONTAINER_DEV_VASTAITECH_GPU,
}
)
var NVIDIA_GPU_TYPES = []string{
var CONTAINER_NVIDIA_GPU_TYPES = []string{
CONTAINER_DEV_NVIDIA_GPU,
CONTAINER_DEV_NVIDIA_MPS,
CONTAINER_DEV_NVIDIA_HAMI,
CONTAINER_DEV_NVIDIA_GPU_SHARE,
}
@@ -198,10 +202,11 @@ type ContainerHostDevice struct {
}
type ContainerIsolatedDevice struct {
Index *int `json:"index"`
Id string `json:"id"`
OnlyEnv []*apis.ContainerIsolatedDeviceOnlyEnv `json:"only_env"`
CDI *apis.ContainerIsolatedDeviceCDI `json:"cdi"`
Index *int `json:"index"`
Id string `json:"id"`
GuestIsolatedDeviceIndex int `json:"guest_isolated_device_index"`
OnlyEnv []*apis.ContainerIsolatedDeviceOnlyEnv `json:"only_env"`
CDI *apis.ContainerIsolatedDeviceCDI `json:"cdi"`
}
type ContainerDevice struct {

View File

@@ -97,3 +97,15 @@ const (
DISK_DRIVER_SATA = "sata"
DISK_DRIVER_VFIO = "vfio-pci"
)
const (
DISK_CACHE_MODE_WRITETHROGH = "writethrough"
DISK_CACHE_MODE_NONE = "none"
DISK_CACHE_MODE_WRITEBACK = "writeback"
DISK_CACHE_MODE_DIRECTSYNC = "directsync"
)
const (
DISK_AIO_MODE_NATIVE = "native"
DISK_AIO_MOD_THREADS = "threads"
)

View File

@@ -319,6 +319,7 @@ var HYPERVISORS = []string{
HYPERVISOR_SANGFOR,
HYPERVISOR_ZETTAKIT,
HYPERVISOR_UIS,
HYPERVISOR_CAS,
HYPERVISOR_CNWARE,
HYPERVISOR_ROCKBASE,
}

View File

@@ -56,11 +56,11 @@ type GuestdiskListInput struct {
type GuestdiskUpdateInput struct {
GuestJointBaseUpdateInput
Driver string `json:"driver"`
CacheMode string `json:"cache_mode"`
AioMode string `json:"aio_mode"`
//Driver string `json:"driver"`
//
//CacheMode string `json:"cache_mode"`
//
//AioMode string `json:"aio_mode"`
Iops *int `json:"iops"`

View File

@@ -131,6 +131,8 @@ type ServerListInput struct {
SnapshotpolicyId string `json:"snapshotpolicy_id"`
IsolatedDeviceId string `json:"isolated_device_id"`
// 是否调度到宿主机上
WithHost *bool `json:"with_host"`
@@ -1063,8 +1065,10 @@ type ServerChangeDiskStorageInput struct {
}
type ServerChangeDiskDriverInput struct {
DiskId string `json:"disk_id"`
Driver string `json:"driver"`
DiskId string `json:"disk_id"`
Driver string `json:"driver"`
CacheMode string `json:"cache_mode"`
AioMode string `json:"aio_mode"`
}
type ServerChangeDiskStorageInternalInput struct {
@@ -1569,8 +1573,25 @@ func (conf ServerChangeConfigSettings) AddedDisk() int {
}
type ServerReleasedIsolatedDevice struct {
DevType string `json:"dev_type"`
Model string `json:"model"`
DevType string `json:"dev_type"`
Model string `json:"model"`
GpuType string `json:"gpu_type"`
SharingMode string `json:"sharing_mode"`
MemoryRequest int `json:"memory_request"`
}
type ServerAttachIsolatedDeviceBase struct {
AutoStart bool `json:"auto_start"`
GpuType string `json:"gpu_type"`
MemoryRequest *int `json:"memory_request"`
SharingMode string `json:"sharing_mode"`
Count *int `json:"count"`
}
type ServerAttachIsolatedDeviceInput struct {
ServerAttachIsolatedDeviceBase
Device string `json:"device"`
Model string `json:"model"`
}
type ServerChangeBillingTypeInput struct {

View File

@@ -501,6 +501,8 @@ type HostIpmiAttributes struct {
IpmiPresent *bool `json:"ipmi_present"`
// lan channel
IpmiLanChannel *uint8 `json:"ipmi_lan_channel"`
// RMCP+ cipher suite for ipmitool -C
IpmiCipherSuite *int `json:"ipmi_cipher_suite"`
// verified
IpmiVerified *bool `json:"ipmi_verified"`
// Redfish API support
@@ -775,7 +777,7 @@ type HostUploadGuestsStatusInput struct {
}
type HostIsolatedDeviceNumaStatsInput struct {
DevType string
Model string
}
type GuestUploadContainerStatusResponse struct {

119
pkg/apis/compute/ipset.go Normal file
View File

@@ -0,0 +1,119 @@
// 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 (
"strings"
"yunion.io/x/pkg/util/regutils"
"yunion.io/x/onecloud/pkg/apis"
"yunion.io/x/onecloud/pkg/httperrors"
)
type TIpSetType string
const (
IpSetTypeIpv4CidrList TIpSetType = "ipv4_cidr_list"
IpSetTypeIpv6CidrList TIpSetType = "ipv6_cidr_list"
)
func (t TIpSetType) IsValid() bool {
switch t {
case IpSetTypeIpv4CidrList, IpSetTypeIpv6CidrList:
return true
default:
return false
}
}
type IpSetCreateInput struct {
apis.SharableVirtualResourceCreateInput
// IP集合类型
// enum: ["ipv4_cidr_list", "ipv6_cidr_list"]
// required: true
IpSetType TIpSetType `json:"ip_set_type"`
// IP/CIDR 列表,逗号分隔
// example: 192.168.1.0/24,10.0.0.1
// required: true
Data string `json:"data"`
}
type IpSetUpdateInput struct {
apis.SharableVirtualResourceBaseUpdateInput
// IP/CIDR 列表,逗号分隔
// example: 192.168.1.0/24,10.0.0.1
Data *string `json:"data"`
}
type IpSetListInput struct {
apis.SharableVirtualResourceListInput
// 按 IP 集合类型过滤
// enum: ["ipv4_cidr_list", "ipv6_cidr_list"]
IpSetType []TIpSetType `json:"ip_set_type"`
// 根据 IP/CIDR 模糊匹配
Ip string `json:"ip"`
}
type IpSetDetails struct {
apis.SharableVirtualResourceDetails
// 关联安全组数量
SecurityGroupCount int `json:"security_group_count"`
}
func ValidateIpSetData(ipSetType TIpSetType, data string) error {
if !ipSetType.IsValid() {
return httperrors.NewInputParameterError("invalid ip_set_type %s", ipSetType)
}
data = strings.TrimSpace(data)
if len(data) == 0 {
return httperrors.NewMissingParameterError("data")
}
parts := strings.Split(data, ",")
for i := range parts {
cidr := strings.TrimSpace(parts[i])
if len(cidr) == 0 {
return httperrors.NewInputParameterError("empty cidr in data")
}
switch ipSetType {
case IpSetTypeIpv4CidrList:
if !regutils.MatchCIDR(cidr) && !regutils.MatchIP4Addr(cidr) {
return httperrors.NewInputParameterError("invalid ipv4 cidr or address %s", cidr)
}
case IpSetTypeIpv6CidrList:
if !regutils.MatchCIDR6(cidr) && !regutils.MatchIP6Addr(cidr) {
return httperrors.NewInputParameterError("invalid ipv6 cidr or address %s", cidr)
}
}
}
return nil
}
func (input *IpSetCreateInput) Validate() error {
return ValidateIpSetData(input.IpSetType, input.Data)
}
func (input *IpSetUpdateInput) Validate(currentType TIpSetType) error {
if input.Data != nil {
return ValidateIpSetData(currentType, *input.Data)
}
return nil
}

View File

@@ -34,10 +34,13 @@ type IsolateDeviceDetails struct {
SIsolatedDevice
MemoryAllocated int
AllocatedCount int
// 云主机名称
Guest string `json:"guest"`
Guest []string `json:"guest"`
// 云主机状态
GuestStatus string `json:"guest_status"`
GuestStatus []string `json:"guest_status"`
}
type IsolatedDeviceListInput struct {
@@ -95,6 +98,9 @@ type IsolatedDeviceCreateInput struct {
// example: GPU
DevType string `json:"dev_type"`
// DEVICE sharing mode
SharingMode string `json:"sharing_mode"`
// 设备型号
// # Specific device name read from lspci command, e.g. `Tesla K40m` ...
Model string `json:"model"`
@@ -137,9 +143,28 @@ type IsolatedDeviceUpdateInput struct {
DeviceMinor int `json:"device_minor"`
}
type SDelIsolatedDeviceInput struct {
Device string
Index int
}
type SAddIsolatedDeviceInput struct {
Device string
GpuType string
MemoryRequest *int
}
type SetIsolatedDeviceInput struct {
AddDevices []SAddIsolatedDeviceInput
DelDevices []SDelIsolatedDeviceInput
AutoStart bool
}
type IsolatedDeviceJsonDesc struct {
Id string `json:"id"`
DevType string `json:"dev_type"`
GpuType string `json:"gpu_type"`
SharingMode string `json:"sharing_mode"`
Model string `json:"model"`
Addr string `json:"addr"`
VendorDeviceId string `json:"vendor_device_id"`
@@ -152,6 +177,8 @@ type IsolatedDeviceJsonDesc struct {
MemorySize int `json:"memory_size"`
MdevId string `json:"mdev_id"`
NumaNode int8 `json:"numa_node"`
MemoryLimit int `json:"memory_limit"`
SmUtilLimit int `json:"sm_util_limit"`
}
type IsolatedDeviceModelCreateInput struct {
@@ -368,3 +395,22 @@ type HostIsolatedDeviceModelDetails struct {
HotPluggable bool
DisableAutoDetect bool
}
type IsolatedDeviceFilterListInput struct {
IsolateDeviceIds []string `json:"isolate_device_ids"`
}
type GuestIsolatedDeviceListInput struct {
GuestJointsListInput
IsolatedDeviceListInput
IsolatedDeviceFilterListInput
}
type GuestIsolatedDeviceDetails struct {
GuestJointResourceDetails
SGuestIsolatedDevice
SIsolatedDevice
HostResourceInfo
apis.SharableResourceBaseInfo
}

View File

@@ -15,25 +15,83 @@
package compute
const (
DIRECT_PCI_TYPE = "PCI"
GPU_HPC_TYPE = "GPU-HPC" // # for compute
GPU_VGA_TYPE = "GPU-VGA" // # for display
SRIOV_VGPU_TYPE = "SRIOV-VGPU"
LEGACY_VGPU_TYPE = "LEGACY-VGPU"
USB_TYPE = "USB"
NIC_TYPE = "NIC" // nic sriov
NVME_PT_TYPE = "NVME-PT" // nvme passthrough
NVIDIA_VENDOR_ID = "10de"
AMD_VENDOR_ID = "1002"
DIRECT_PCI_TYPE = "PCI"
GPU_TYPE = "GPU"
USB_TYPE = "USB"
NIC_TYPE = "NIC" // nic sriov
NVME_PT_TYPE = "NVME-PT" // nvme passthrough
NETINT_TYPE = "NETINT"
NPU_TYPE = "NPU"
BINDER_TYPE = "BINDER"
)
var VALID_GPU_TYPES = []string{GPU_HPC_TYPE, GPU_VGA_TYPE}
var VALID_ATTACH_TYPES = []string{GPU_HPC_TYPE, GPU_VGA_TYPE, USB_TYPE, SRIOV_VGPU_TYPE, LEGACY_VGPU_TYPE}
var VALID_TYPES = []string{DIRECT_PCI_TYPE, GPU_TYPE, USB_TYPE, NIC_TYPE, NVME_PT_TYPE, NETINT_TYPE, NPU_TYPE, BINDER_TYPE}
const (
DEVICE_SHARING_MODE_EXCLUSIVE = "EXCLUSIVE"
DEVICE_SHARING_MODE_SRIOV = "SRIOV"
DEVICE_SHARING_MODE_MPS = "MPS"
DEVICE_SHARING_MODE_HAMI = "HAMI"
DEVICE_SHARING_MODE_UNLIMITED = "UNLIMITED"
DEVICE_SHARING_MODE_MDEV = "MDEV"
)
var VIRTUAL_SHARING_MODES = []string{DEVICE_SHARING_MODE_HAMI, DEVICE_SHARING_MODE_UNLIMITED, DEVICE_SHARING_MODE_MPS}
var VAILD_SHARING_MODES = []string{DEVICE_SHARING_MODE_EXCLUSIVE, DEVICE_SHARING_MODE_SRIOV, DEVICE_SHARING_MODE_MDEV, DEVICE_SHARING_MODE_HAMI, DEVICE_SHARING_MODE_UNLIMITED, DEVICE_SHARING_MODE_MPS}
const (
GPU_HPC = "HPC"
GPU_VGA = "VGA"
)
const (
NVIDIA_VENDOR_ID = "10de"
AMD_VENDOR_ID = "1002"
VASTAITECH_VENDOR_ID = "1ec6"
)
var ID_VENDOR_MAP = map[string]string{
NVIDIA_VENDOR_ID: "NVIDIA",
AMD_VENDOR_ID: "AMD",
VASTAITECH_VENDOR_ID: "VASTAITECH",
}
var VENDOR_ID_MAP = map[string]string{
"NVIDIA": NVIDIA_VENDOR_ID,
"AMD": AMD_VENDOR_ID,
}
const (
ISOLATED_DEVICE_MODEL_METADATA_BANDWIDTH = "bandwidth"
ISOLATED_DEVICE_MODEL_METADATA_MEMORY_MB = "memory_mb"
ISOLATED_DEVICE_MODEL_METADATA_TFLOPS = "tflops"
)
////////////// deprecated
const (
GPU_HPC_TYPE = "GPU-HPC" // # for compute
GPU_VGA_TYPE = "GPU-VGA" // # for display
SRIOV_VGPU_TYPE = "SRIOV-VGPU" //done
LEGACY_VGPU_TYPE = "LEGACY-VGPU" //done
)
var VALID_GPU_TYPES = []string{GPU_HPC_TYPE, GPU_VGA_TYPE} //done
var VALID_ATTACH_TYPES = []string{GPU_HPC_TYPE, GPU_VGA_TYPE, USB_TYPE, SRIOV_VGPU_TYPE, LEGACY_VGPU_TYPE} //done
var GPU_TYPES = []string{
GPU_HPC_TYPE, GPU_VGA_TYPE, SRIOV_VGPU_TYPE, LEGACY_VGPU_TYPE,
CONTAINER_DEV_CPH_AMD_GPU, CONTAINER_DEV_NVIDIA_GPU, CONTAINER_DEV_NVIDIA_MPS, CONTAINER_DEV_NVIDIA_GPU_SHARE,
CONTAINER_DEV_NVIDIA_HAMI, CONTAINER_DEV_VASTAITECH_GPU,
}
var NETINT_TYPES = []string{
CONTAINER_DEV_NETINT_CA_QUADRA, CONTAINER_DEV_NETINT_CA_ASIC,
}
var VALID_CONTAINER_DEVICE_TYPES = []string{
CONTAINER_DEV_CPH_AMD_GPU, CONTAINER_DEV_CPH_AOSP_BINDER, CONTAINER_DEV_NETINT_CA_QUADRA,
CONTAINER_DEV_NETINT_CA_ASIC, CONTAINER_DEV_NVIDIA_GPU, CONTAINER_DEV_NVIDIA_MPS, CONTAINER_DEV_NVIDIA_GPU_SHARE,
CONTAINER_DEV_NETINT_CA_ASIC, CONTAINER_DEV_NVIDIA_GPU, CONTAINER_DEV_NVIDIA_MPS, CONTAINER_DEV_NVIDIA_GPU_SHARE, CONTAINER_DEV_NVIDIA_HAMI,
CONTAINER_DEV_ASCEND_NPU, CONTAINER_DEV_VASTAITECH_GPU,
}
@@ -49,27 +107,12 @@ var VITRUAL_DEVICE_TYPES = []string{
CONTAINER_DEV_NETINT_CA_QUADRA,
CONTAINER_DEV_NETINT_CA_ASIC,
CONTAINER_DEV_NVIDIA_MPS,
CONTAINER_DEV_NVIDIA_HAMI,
CONTAINER_DEV_ASCEND_NPU,
CONTAINER_DEV_NVIDIA_GPU_SHARE,
CONTAINER_DEV_VASTAITECH_GPU,
}
var ID_VENDOR_MAP = map[string]string{
NVIDIA_VENDOR_ID: "NVIDIA",
AMD_VENDOR_ID: "AMD",
}
var VENDOR_ID_MAP = map[string]string{
"NVIDIA": NVIDIA_VENDOR_ID,
"AMD": AMD_VENDOR_ID,
}
const (
ISOLATED_DEVICE_MODEL_METADATA_BANDWIDTH = "bandwidth"
ISOLATED_DEVICE_MODEL_METADATA_MEMORY_MB = "memory_mb"
ISOLATED_DEVICE_MODEL_METADATA_TFLOPS = "tflops"
)
func init() {
VALID_PASSTHROUGH_TYPES = append(VALID_PASSTHROUGH_TYPES, VALID_CONTAINER_DEVICE_TYPES...)
VALID_ATTACH_TYPES = append(VALID_ATTACH_TYPES, VALID_CONTAINER_DEVICE_TYPES...)

View File

@@ -15,7 +15,6 @@
package compute
import (
"fmt"
"strings"
"yunion.io/x/pkg/errors"
@@ -71,6 +70,11 @@ type SSecgroupRuleResource struct {
// example: 192.168.222.121
CIDR string `json:"cidr"`
// 目标类型
// enum: ["cidr", "ip_set", "security_group"]
// required: true
TargetType TSecgroupTargetType `json:"target_type"`
// 行为
// deny: 拒绝
// allow: 允许
@@ -135,6 +139,9 @@ type SSecgroupRuleUpdateInput struct {
// requried: false
// example: test to create rule
Description string `json:"description"`
// swagger:ignore
TargetType TSecgroupTargetType `json:"target_type"`
}
func IsValidSecgroupRuleCIDR(cidr string) bool {
@@ -177,13 +184,28 @@ func (input *SSecgroupRuleResource) Check() error {
}
}
if len(input.CIDR) > 0 {
if !IsValidSecgroupRuleCIDR(input.CIDR) {
return fmt.Errorf("invalid cidr: %s", input.CIDR)
switch input.TargetType {
case SecurityGroupRuleTargetTypeCidr:
if len(input.CIDR) > 0 {
if !IsValidSecgroupRuleCIDR(input.CIDR) {
return errors.Wrapf(errors.ErrInvalidFormat, "invalid cidr: %s", input.CIDR)
}
} else {
// empty CIDR means both IPv4 and IPv6
// input.CIDR = "0.0.0.0/0"
}
} else {
// empty CIDR means both IPv4 and IPv6
// input.CIDR = "0.0.0.0/0"
case SecurityGroupRuleTargetTypeIpSet:
if len(input.CIDR) > 0 {
} else {
return errors.Wrap(errors.ErrEmpty, "empty ip set id")
}
case SecurityGroupRuleTargetTypeIpSetGroup:
fallthrough
case SecurityGroupRuleTargetTypeSecurityGroup:
fallthrough
default:
return errors.Wrapf(errors.ErrNotSupported, "unsupported target type %s", input.TargetType)
}
return rule.ValidateRule()
@@ -239,6 +261,9 @@ type SecgroupListInput struct {
LoadbalancerId string `json:"loadbalancer_id"`
RegionalFilterListInput
ManagedResourceListInput
// 指定过滤规则中含有指定ip集的安全组
IpSetId []string `json:"ip_set_id"`
}
type SecurityGroupRuleListInput struct {
@@ -258,6 +283,8 @@ type SecurityGroupRuleListInput struct {
Ports string `json:"ports"`
// 根据ip模糊匹配安全组规则
Ip string `json:"ip"`
// 根据target_type字段过滤安全组规则
TargetType []string `json:"target_type"`
}
type SecgroupResourceInput struct {

View File

@@ -16,6 +16,15 @@ package compute
import "yunion.io/x/onecloud/pkg/apis"
type TSecgroupTargetType string
const (
SecurityGroupRuleTargetTypeIpSet TSecgroupTargetType = "ip_set"
SecurityGroupRuleTargetTypeIpSetGroup TSecgroupTargetType = "ip_set_group"
SecurityGroupRuleTargetTypeSecurityGroup TSecgroupTargetType = "security_group"
SecurityGroupRuleTargetTypeCidr TSecgroupTargetType = "cidr"
)
type SecgroupRuleDetails struct {
apis.ResourceBaseDetails
apis.ProjectizedResourceInfo
@@ -23,4 +32,7 @@ type SecgroupRuleDetails struct {
SecurityGroupResourceInfo
ProjectId string `json:"tenant_id"`
TargetIpSet string `json:"target_ip_set"`
TargetSecurityGroup string `json:"target_security_group"`
}

View File

@@ -23,6 +23,13 @@ import (
"yunion.io/x/onecloud/pkg/apis/cloudprovider"
)
// IsolatedDeviceAllocateStat is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.IsolatedDeviceAllocateStat.
type IsolatedDeviceAllocateStat struct {
SIsolatedDevice
GuestCount int `json:"guest_count"`
MemoryAllocated int `json:"memory_allocated"`
}
// SAccessGroup is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SAccessGroup.
type SAccessGroup struct {
apis.SStatusInfrasResourceBase
@@ -167,6 +174,7 @@ type SBucket struct {
SizeBytesLimit int64 `json:"size_bytes_limit"`
ObjectCntLimit int `json:"object_cnt_limit"`
AccessUrls jsonutils.JSONObject `json:"access_urls"`
EnablePerfMon bool `json:"enable_perf_mon"`
}
// SCDNDomain is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SCDNDomain.
@@ -650,6 +658,8 @@ type SDnsRecord struct {
DnsValue string `json:"dns_value"`
TTL int64 `json:"ttl"`
MxPriority int64 `json:"mx_priority"`
// cloudflare 特有
Proxied *bool `json:"proxied,omitempty"`
// 解析线路类型
PolicyType string `json:"policy_type"`
// 解析线路
@@ -940,6 +950,10 @@ type SElasticip struct {
BgpType string `json:"bgp_type"`
// 是否跟随主机删除而自动释放
AutoDellocate *bool `json:"auto_dellocate,omitempty"`
// 下行带宽限制单位mbps
RxBwLimit int `json:"rx_bw_limit"`
// 上行带宽限制单位mbps
TxBwLimit int `json:"tx_bw_limit"`
}
// SExternalProject is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SExternalProject.
@@ -1114,6 +1128,23 @@ type SGuest struct {
ResourcePool string `json:"resource_pool"`
}
// SGuestIsolatedDevice is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestIsolatedDevice.
type SGuestIsolatedDevice struct {
SGuestJointsBase
SIsolatedDeviceResourceBase
// guest isolated device memory size limit
DeviceMemorySize int `json:"device_memory_size"`
// guest isolated device Streaming Multiprocessor Utilization limit
SmUtilLimit int `json:"sm_util_limit"`
// gpu device work type: HPC VGA
GpuType string `json:"gpu_type"`
// guest network index
NetworkIndex int `json:"network_index"`
// guest disk index
DiskIndex byte `json:"disk_index"`
Index byte `json:"index"`
}
// SGuestJointsBase is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestJointsBase.
type SGuestJointsBase struct {
apis.SVirtualJointResourceBase
@@ -1125,11 +1156,13 @@ type SGuestNetworkTrafficLog struct {
apis.SLogBase
apis.SProjectizedResourceBase
SGuestResourceBase
SNetworkResourceBase
// MAC地址
Mac string `json:"mac"`
// IPv4地址
IpAddr string `json:"ip_addr"`
// IPv6地址
Ip6Addr string `json:"ip6_addr"`
RowId int64 `json:"row_id"`
// 下行流量,单位 bytes
RxBytes int64 `json:"rx_bytes"`
// 上行流量,单位 bytes
@@ -1144,7 +1177,7 @@ type SGuestResourceBase struct {
}
// SGuestScreenDump is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestScreenDump.
/* type SGuestScreenDump struct {
type SGuestScreenDump struct {
apis.SResourceBase
RowId int64 `json:"row_id"`
GuestId string `json:"guest_id"`
@@ -1155,7 +1188,7 @@ type SGuestResourceBase struct {
S3Endpoint string `json:"s3_endpoint"`
S3BucketName string `json:"s3_bucket_name"`
S3UseSsl bool `json:"s3_use_ssl"`
} */
}
// SGuestTemplate is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestTemplate.
type SGuestTemplate struct {
@@ -1248,6 +1281,20 @@ type SGuestnetwork struct {
PortMappings []*GuestPortMapping `json:"port_mappings"`
SBillingTypeBase
SBillingChargeTypeBase
// 下行带宽限制单位mbps
RxBwLimit int `json:"rx_bw_limit"`
// 上行带宽限制单位mbps
TxBwLimit int `json:"tx_bw_limit"`
}
// SGuestnetworksecgroup is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestnetworksecgroup.
type SGuestnetworksecgroup struct {
apis.SResourceBase
RowId int64 `json:"row_id"`
GuestId string `json:"guest_id"`
SSecurityGroupResourceBase
NetworkIndex int `json:"network_index"`
Admin bool `json:"admin"`
}
// SGuestsecgroup is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestsecgroup.
@@ -1523,7 +1570,7 @@ type SInstanceSnapshot struct {
// 套餐名称
InstanceType string `json:"instance_type"`
// 主机快照磁盘容量和
SizeMb int `json:"size_mb"`
// SizeMb int `nullable:"false" list:"user"`
// 镜像ID
ImageId string `json:"image_id"`
// 是否保存内存
@@ -1575,15 +1622,15 @@ type SIsolatedDevice struct {
apis.SExternalizedResourceBase
apis.SSharableBaseResource
SHostResourceBase
// # PCI / GPU-HPC / GPU-VGA / USB / NIC
// # PCI / GPU / USB / NIC ...
// 设备类型
DevType string `json:"dev_type"`
// EXCLUSIVE / SRIOV / MPS / HAMI / SHARE / MIG
SharingMode string `json:"sharing_mode"`
// Device is hot pluggable
HotPluggable bool `json:"hot_pluggable"`
// # Specific device name read from lspci command, e.g. `Tesla K40m` ...
Model string `json:"model"`
// 云主机Id
GuestId string `json:"guest_id"`
// guest network index
NetworkIndex int `json:"network_index"`
// Nic wire id
WireId string `json:"wire_id"`
// Offload interface name
@@ -1592,9 +1639,7 @@ type SIsolatedDevice struct {
IsInfinibandNic bool `json:"is_infiniband_nic"`
// NVME disk size
NvmeSizeMB int `json:"nvme_size_mb"`
// guest disk index
DiskIndex byte `json:"disk_index"`
// # pci address of `Bus:Device.Function` format, or usb bus address of `bus.addr`
// # pci address of `Bus:Device.Function` format, or usb bus address of `bus:addr:port`
Addr string `json:"addr"`
DevicePath string `json:"device_path"`
// GPU card path, like /dev/dri/cardX
@@ -1640,6 +1685,11 @@ type SIsolatedDevice struct {
PcieInfo *IsolatedDevicePCIEInfo `json:"pcie_info"`
// device numa node
NumaNode byte `json:"numa_node"`
// On-device memory in MiB (NVIDIA GPU VRAM via `nvidia-smi memory.total`,
// or per-slice quota for MPS share mode). 0 means unknown / not applicable.
MemorySize int `json:"memory_size"`
// some of isolated device type support virtual num, like NVIDIA_GPU_SHARE, NVIDIA_MPS
VirtualNum int `json:"virtual_num"`
}
// SIsolatedDeviceModel is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SIsolatedDeviceModel.
@@ -1654,6 +1704,11 @@ type SIsolatedDeviceModel struct {
DisableAutoDetect *bool `json:"disable_auto_detect,omitempty"`
}
// SIsolatedDeviceResourceBase is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SIsolatedDeviceResourceBase.
type SIsolatedDeviceResourceBase struct {
IsolatedDeviceId string `json:"isolated_device_id"`
}
// SKafka is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SKafka.
type SKafka struct {
apis.SVirtualResourceBase
@@ -1763,7 +1818,7 @@ type SLoadbalancer struct {
NetworkType string `json:"network_type"`
SLoadbalancerClusterResourceBase
// 计费类型
ChargeType string `json:"charge_type"`
SBillingChargeTypeBase
// 套餐名称
LoadbalancerSpec string `json:"loadbalancer_spec"`
// 默认后端服务器组Id
@@ -2716,6 +2771,8 @@ type SSecurityGroupRule struct {
CIDR string `json:"cidr"`
Action string `json:"action"`
Description string `json:"description"`
TargetType TSecgroupTargetType `json:"target_type"`
}
// SServerSku is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SServerSku.
@@ -2724,7 +2781,6 @@ type SServerSku struct {
apis.SExternalizedResourceBase
SCloudregionResourceBase
SZoneResourceBase
// SkuId string `width:"64" charset:"ascii" nullable:"false" list:"user" create:"admin_required"` // x2.large
InstanceTypeFamily string `json:"instance_type_family"`
// x2
InstanceTypeCategory string `json:"instance_type_category"`
@@ -2780,7 +2836,9 @@ type SSnapshot struct {
CreatedBy string `json:"created_by"`
Location string `json:"location"`
// 快照大小,单位Mb
Size int `json:"size"`
Size int `json:"size"`
// Virtual size, for kvm is origin disk size
VirtualSize int `json:"virtual_size"`
OutOfChain bool `json:"out_of_chain"`
FakeDeleted bool `json:"fake_deleted"`
DiskType string `json:"disk_type"`
@@ -2799,18 +2857,15 @@ type SSnapshotPolicy struct {
apis.SExternalizedResourceBase
SManagedResourceBase
SCloudregionResourceBase
// 快照保留天数, -1: 表示永久保留
// 快照保留天数
RetentionDays int `json:"retention_days"`
// 快照保留数量, 优先级高于 RetentionDays, 且仅对本地IDC资源有效
RetentionCount int `json:"retention_count"`
// 快照类型, 目前支持 disk, server
// disk: 自动磁盘快照策略, 只能关联磁盘
// server: 自动主机快照策略, 只能关联主机
Type string `json:"type"`
// 1~7, 1 is Monday, 7 is Sunday
// 1~7, 1 is Monday
RepeatWeekdays []int `json:"repeat_weekdays"`
// 0~23, 每小时
// 创建自动快照策略的时间必须与 RepeatWeekdays 对应的创建周期相一致
// 0~23
TimePoints []int `json:"time_points"`
}

View File

@@ -93,8 +93,11 @@ type ContainerIsolatedDevice struct {
Addr string `json:"addr"`
Path string `json:"path"`
DeviceType string `json:"device_type"`
SharingMode string `json:"sharing_mode"`
CardPath string `json:"card_path"`
RenderPath string `json:"render_path"`
MemoryLimit int `json:"memory_limit"`
SmUtilLimit int `json:"sm_util_limit"`
Index int `json:"index"`
DeviceMinor int `json:"device_minor"`
OnlyEnv []*apis.ContainerIsolatedDeviceOnlyEnv `json:"only_env"`

View File

@@ -17,6 +17,7 @@ type InstantModelListInput struct {
ModelTag string `json:"model_tag"`
ModelId string `json:"model_id"`
LlmType string `json:"llm_type"`
Source string `json:"source"`
Image string `json:"image"`
Mounts string `json:"mounts"`

View File

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

View File

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

View File

@@ -8,5 +8,5 @@ const (
LLM_SGLANG_HF_ENDPOINT = LLM_VLLM_HF_ENDPOINT
LLM_SGLANG_CACHE_DIR = "/root/.cache/huggingface"
LLM_SGLANG_BASE_PATH = "/data/models"
LLM_SGLANG_MODELS_PATH = "/data/models/huggingface"
LLM_SGLANG_MODELS_PATH = LLM_SGLANG_BASE_PATH
)

View File

@@ -78,9 +78,14 @@ func (s PortMappings) IsZero() bool {
}
type Device struct {
DevType string `json:"dev_type"`
Model string `json:"model"`
DevicePath string `json:"device_path"`
DevType string `json:"dev_type"`
SharingMode string `json:"sharing_mode,omitempty"`
Model string `json:"model"`
DevicePath string `json:"device_path"`
// MemoryMb is optional per-device VRAM (MiB) for HAMI. When > 0 it is used
// as MemoryMb/MemoryRequest on pod create; otherwise claim is split evenly.
MemoryMb int `json:"memory_mb,omitempty"`
SmUtilLimit int `json:"sm_util_limit,omitempty"`
}
type Devices []Device
@@ -139,6 +144,10 @@ type LLMSkuDetails struct {
// Inference backend version and parameters
BackendVersion string `json:"backend_version"`
BackendParameters []string `json:"backend_parameters,omitempty"`
// VramClaimMb is computed from mounted InstantModel weight_size_bytes
// (EstimateClaimMb). Not persisted on the SKU row.
VramClaimMb int `json:"vram_claim_mb"`
}
type MountedAppResourceDetails struct {
@@ -151,10 +160,6 @@ type LLMSKuBaseCreateInput struct {
Cpu int `json:"cpu"`
Memory int `json:"memory"`
Bandwidth int `json:"bandwidth"`
// VramClaimMb is the estimated VRAM (MiB) the inference instance will
// require. Optional — if 0, the deployment create task will auto-fill it
// from the mounted InstantModel's weight_size_bytes.
VramClaimMb int `json:"vram_claim_mb,omitempty"`
Volumes *Volumes `json:"volumes"`
HostPaths *HostPaths `json:"host_paths"`

View File

@@ -8,5 +8,5 @@ const (
LLM_VLLM_HF_ENDPOINT = "https://hf-mirror.com"
LLM_VLLM_CACHE_DIR = "/root/.cache/huggingface"
LLM_VLLM_BASE_PATH = "/data/models"
LLM_VLLM_MODELS_PATH = "/data/models/huggingface"
LLM_VLLM_MODELS_PATH = LLM_VLLM_BASE_PATH
)

View File

@@ -18,8 +18,10 @@ import (
"bufio"
"context"
"fmt"
"mime"
"net"
"net/http"
"strings"
"yunion.io/x/onecloud/pkg/httperrors"
)
@@ -36,6 +38,8 @@ type responseWriterChannel struct {
bodyResp chan responseWriterResponse
statusChan chan int
statusResp chan bool
flushChan chan struct{}
flushResp chan struct{}
isClosed bool
}
@@ -47,6 +51,8 @@ func newResponseWriterChannel(backend http.ResponseWriter) responseWriterChannel
bodyResp: make(chan responseWriterResponse),
statusChan: make(chan int),
statusResp: make(chan bool),
flushChan: make(chan struct{}),
flushResp: make(chan struct{}),
isClosed: false,
}
}
@@ -76,16 +82,34 @@ func (w *responseWriterChannel) WriteHeader(status int) {
<-w.statusResp
}
// implent http.Flusher
// Flush implements http.Flusher. Flush must run on the same goroutine as Write
// (wait loop); otherwise SSE via ReverseProxy never reaches the client.
func (w *responseWriterChannel) Flush() {
if w.isClosed {
return
}
w.flushChan <- struct{}{}
<-w.flushResp
}
func (w *responseWriterChannel) flushBackend() {
if f, ok := w.backend.(http.Flusher); ok {
f.Flush()
}
}
func (w *responseWriterChannel) isEventStream() bool {
ct := w.backend.Header().Get("Content-Type")
if ct == "" {
return false
}
baseCT, _, err := mime.ParseMediaType(ct)
if err != nil {
return strings.HasPrefix(ct, "text/event-stream")
}
return baseCT == "text/event-stream"
}
// Hijack implements the Hijacker.Hijack method. Our response is both a ResponseWriter
// and a Hijacker.
func (w *responseWriterChannel) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) {
@@ -121,6 +145,11 @@ func (w *responseWriterChannel) wait(ctx context.Context, workerChan chan *SWork
// log.Infof("Recive body: %s, more: %v", len(bytes), more)
if more {
c, e := w.backend.Write(bytes)
// SSE / streaming: flush immediately after each chunk so clients
// see events without waiting for handler completion.
if e == nil && w.isEventStream() {
w.flushBackend()
}
w.bodyResp <- responseWriterResponse{count: c, err: e}
} else {
stop = true
@@ -133,6 +162,13 @@ func (w *responseWriterChannel) wait(ctx context.Context, workerChan chan *SWork
} else {
stop = true
}
case _, more := <-w.flushChan:
if more {
w.flushBackend()
w.flushResp <- struct{}{}
} else {
stop = true
}
}
}
return err
@@ -148,4 +184,6 @@ func (w *responseWriterChannel) closeChannels() {
close(w.bodyResp)
close(w.statusChan)
close(w.statusResp)
close(w.flushChan)
close(w.flushResp)
}

View File

@@ -205,7 +205,10 @@ func handleBaremetalValidateIPMI() appsrv.FilterHandler {
if redfishCli == nil {
resp.IsRedfishSupported = false
// use ipmitool to validate
tool := ipmitool.NewLanPlusIPMI(input.Ip, input.Username, input.Password)
tool, err := ipmitool.NewLanPlusIPMI(input.Ip, input.Username, input.Password)
if err != nil {
return nil, errors.Wrap(err, "NewLanPlusIPMI")
}
info, err := ipmitool.GetSysInfo(tool)
if err != nil {
return nil, errors.Wrap(err, "GetSysInfo by ipmitool")

View File

@@ -498,7 +498,10 @@ func (m *SBaremetalManager) checkSshInfo(input *BmRegisterInput) (*ssh.Client, e
}
func (m *SBaremetalManager) checkIpmiInfo(ctx context.Context, username, password, ipAddr string) (uint8, net.HardwareAddr, error) {
lanPlusTool := ipmitool.NewLanPlusIPMI(ipAddr, username, password)
lanPlusTool, err := ipmitool.NewLanPlusIPMI(ipAddr, username, password)
if err != nil {
return 0, nil, errors.Wrap(err, "NewLanPlusIPMI")
}
sysInfo, err := ipmitool.GetSysInfo(lanPlusTool)
if err != nil {
return 0, nil, errors.Wrap(err, "GetSysInfo")
@@ -1823,7 +1826,12 @@ func (b *SBaremetalInstance) GetIPMITool() *ipmitool.LanPlusIPMI {
log.Debugf("GetIPMIConfig is nil")
return nil
}
return ipmitool.NewLanPlusIPMI(conf.IpAddr, conf.Username, conf.Password)
tool, err := ipmitool.NewLanPlusIPMIWithCipher(conf.IpAddr, conf.Username, conf.Password, 623, conf.CipherSuite)
if err != nil {
log.Errorf("NewLanPlusIPMIWithCipher for %s: %v", conf.IpAddr, err)
return nil
}
return tool
}
func (b *SBaremetalInstance) isRedfishCapable() bool {

View File

@@ -537,7 +537,11 @@ func (task *sBaremetalPrepareTask) tryLocalIpmiAddr(ctx context.Context, sshIPMI
log.Errorf("Failed to get lan config after %d tries", tried)
return false
}
rmcpIPMI := ipmitool.NewLanPlusIPMI(tryAddr, ipmiUser, ipmiPasswd)
rmcpIPMI, err := ipmitool.NewLanPlusIPMI(tryAddr, ipmiUser, ipmiPasswd)
if err != nil {
log.Errorf("NewLanPlusIPMI for %s: %v", tryAddr, err)
return false
}
for tried = 0; tried < maxTries; tried += 1 {
conf2, err := ipmitool.GetLanConfig(rmcpIPMI, lanChannel)
if err != nil {
@@ -849,7 +853,7 @@ func (task *sBaremetalPrepareTask) sendIsolatedDevicesInfo(
gpuDevs := make([]isolated_device.IDevice, len(devs))
for i := 0; i < len(devs); i++ {
gpuDevs[i] = isolated_device.NewGPUHPCDevice(devs[i])
gpuDevs[i] = isolated_device.NewGPUHPCDevice(devs[i], api.DEVICE_SHARING_MODE_EXCLUSIVE)
}
for _, obj := range objs {

View File

@@ -85,7 +85,10 @@ func (self *SBaremetalIpmiProbeTask) DoIpmiProbe(ctx context.Context, args inter
// else, redfish call fails, try IPMI
} */
log.Warningf("BMC not redfish-compatible for IPMI: %s, use raw probe", ipmiInfo.IpAddr)
ipmiTool := ipmitool.NewLanPlusIPMI(ipmiInfo.IpAddr, ipmiInfo.Username, ipmiInfo.Password)
ipmiTool, err := ipmitool.NewLanPlusIPMI(ipmiInfo.IpAddr, ipmiInfo.Username, ipmiInfo.Password)
if err != nil {
return errors.Wrap(err, "NewLanPlusIPMI")
}
return self.doRawIpmiProbe(ctx, ipmiTool)
}
@@ -254,6 +257,9 @@ func (self *SBaremetalIpmiProbeTask) doRawIpmiProbe(ctx context.Context, cli ipm
ipmiInfo.CdromBoot = false
ipmiInfo.PxeBoot = o.Options.EnablePxeBoot
ipmiInfo.LanChannel = channel
if lanPlus, ok := cli.(*ipmitool.LanPlusIPMI); ok {
ipmiInfo.CipherSuite = lanPlus.GetCipherSuite()
}
updateData := jsonutils.Marshal(updateInfo)
updateData.(*jsonutils.JSONDict).Update(ipmiInfo.ToPrepareParams())
_, err = modules.Hosts.Update(self.Baremetal.GetClientSession(), self.Baremetal.GetId(), updateData)

View File

@@ -89,31 +89,60 @@ func (ipmi *SSHIPMI) ExecuteCommand(args ...string) ([]string, error) {
return ipmi.sshClient.Run(cmd.String())
}
// DefaultCipherSuites is the probe order for RMCP+ cipher suites.
// 0 means do not pass -C (ipmitool default); 3 and 17 are common BMC requirements.
var DefaultCipherSuites = []int{0, 3, 17}
type LanPlusIPMI struct {
IPMIParser
host string
user string
password string
port int
host string
user string
password string
port int
cipherSuite int // 0 = no -C; >0 = pass -C N
cipherResolved bool // true after Ensure/Detect or constructed with known suite > 0
}
func NewLanPlusIPMI(host, user, password string) *LanPlusIPMI {
func NewLanPlusIPMI(host, user, password string) (*LanPlusIPMI, error) {
return NewLanPlusIPMIWithPort(host, user, password, 623)
}
func NewLanPlusIPMIWithPort(host, user, password string, port int) *LanPlusIPMI {
return &LanPlusIPMI{
host: host,
user: user,
password: password,
port: port,
func NewLanPlusIPMIWithPort(host, user, password string, port int) (*LanPlusIPMI, error) {
return NewLanPlusIPMIWithCipher(host, user, password, port, 0)
}
func NewLanPlusIPMIWithCipher(host, user, password string, port, cipherSuite int) (*LanPlusIPMI, error) {
ipmi := &LanPlusIPMI{
host: host,
user: user,
password: password,
port: port,
cipherSuite: cipherSuite,
}
// Known non-default suite from persisted config: skip re-detect.
if cipherSuite > 0 {
ipmi.cipherResolved = true
return ipmi, nil
}
if err := ipmi.ensureCipherSuite(); err != nil {
return nil, err
}
return ipmi, nil
}
func (ipmi *LanPlusIPMI) GetMode() string {
return "rmcp"
}
func (ipmi *LanPlusIPMI) SetCipherSuite(suite int) {
ipmi.cipherSuite = suite
ipmi.cipherResolved = true
}
func (ipmi *LanPlusIPMI) GetCipherSuite() int {
return ipmi.cipherSuite
}
func (ipmi *LanPlusIPMI) GetCommand(args ...string) (*procutils.Command, context.CancelFunc) {
nArgs := []string{
"-I", "lanplus", "-H", ipmi.host,
@@ -121,6 +150,9 @@ func (ipmi *LanPlusIPMI) GetCommand(args ...string) (*procutils.Command, context
"-U", ipmi.user,
"-P", ipmi.password,
}
if ipmi.cipherSuite > 0 {
nArgs = append(nArgs, "-C", strconv.Itoa(ipmi.cipherSuite))
}
nArgs = append(nArgs, args...)
ctx, cancel := context.WithTimeout(context.Background(), ipmi.GetDefaultTimeout())
return procutils.NewCommandContext(ctx, "ipmitool", nArgs...), cancel
@@ -137,6 +169,34 @@ func (ipmi *LanPlusIPMI) ExecuteCommand(args ...string) ([]string, error) {
return ssh.ParseOutput(out), nil
}
// DetectCipherSuite tries DefaultCipherSuites with a single chassis power status each.
// On success it sets the working suite on the receiver and returns it.
func (ipmi *LanPlusIPMI) DetectCipherSuite() (int, error) {
var errs []error
for _, suite := range DefaultCipherSuites {
ipmi.cipherSuite = suite
ipmi.cipherResolved = false
_, err := ipmi.ExecuteCommand("chassis", "power", "status")
if err == nil {
ipmi.SetCipherSuite(suite)
log.Infof("[LanPlusIPMI] detected cipher suite %d for %s", suite, ipmi.host)
return suite, nil
}
errs = append(errs, errors.Wrapf(err, "cipher suite %d", suite))
log.Debugf("[LanPlusIPMI] cipher suite %d failed for %s: %v", suite, ipmi.host, err)
}
return 0, errors.Wrapf(errors.NewAggregate(errs), "detect cipher suite for %s", ipmi.host)
}
// ensureCipherSuite uses a known suite when already resolved; otherwise runs DetectCipherSuite.
func (ipmi *LanPlusIPMI) ensureCipherSuite() error {
if ipmi.cipherResolved {
return nil
}
_, err := ipmi.DetectCipherSuite()
return err
}
func GetSysGuid(exector IPMIExecutor) string {
args := []string{"mc", "guid"}
// args := []string{"raw", "0x06", "0x37"}

View File

@@ -513,8 +513,7 @@ func ParseIsolatedDevice(desc string, idx int) (*compute.IsolatedDeviceConfig, e
}
dev := new(compute.IsolatedDeviceConfig)
parts := strings.Split(desc, ":")
devTypes := sets.NewString(compute.VALID_PASSTHROUGH_TYPES...)
devTypes.Insert(compute.VALID_CONTAINER_DEVICE_TYPES...)
devTypes := sets.NewString(compute.VALID_TYPES...)
for _, p := range parts {
if regutils.MatchUUIDExact(p) {
dev.Id = p

View File

@@ -175,6 +175,7 @@ type HostCommonOptions struct {
ImageDeployDriver string `help:"Image deploy driver" default:"qemu-kvm" choices:"qemu-kvm|nbd|libguestfs"`
DeployConcurrent int `help:"qemu-kvm deploy driver concurrent" default:"5"`
Qcow2Preallocation string `help:"Qcow2 image create preallocation" default:"metadata" choices:"disable|metadata|falloc|full"`
EnableNestedVirtualization bool `help:"Enable nested virtualization" default:"true"`
}
type S3CommonOptions struct {

View File

@@ -24,15 +24,16 @@ const (
)
type SIPMIInfo struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IpAddr string `json:"ip_addr,omitempty"`
Present bool `json:"present,omitempty"`
LanChannel uint8 `json:"lan_channel,omitzero"`
Verified bool `json:"verified,omitfalse"`
RedfishApi bool `json:"redfish_api,omitfalse"`
CdromBoot bool `json:"cdrom_boot,omitfalse"`
PxeBoot bool `json:"pxe_boot,omitfalse"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IpAddr string `json:"ip_addr,omitempty"`
Present bool `json:"present,omitempty"`
LanChannel uint8 `json:"lan_channel,omitzero"`
CipherSuite int `json:"cipher_suite,omitzero"`
Verified bool `json:"verified,omitfalse"`
RedfishApi bool `json:"redfish_api,omitfalse"`
CdromBoot bool `json:"cdrom_boot,omitfalse"`
PxeBoot bool `json:"pxe_boot,omitfalse"`
}
func (info SIPMIInfo) ToPrepareParams() jsonutils.JSONObject {
@@ -48,6 +49,9 @@ func (info SIPMIInfo) ToPrepareParams() jsonutils.JSONObject {
}
data.Add(jsonutils.NewBool(info.Present), "ipmi_present")
data.Add(jsonutils.NewInt(int64(info.LanChannel)), "ipmi_lan_channel")
if info.CipherSuite > 0 {
data.Add(jsonutils.NewInt(int64(info.CipherSuite)), "ipmi_cipher_suite")
}
if info.Verified {
data.Add(jsonutils.JSONTrue, "ipmi_verified")
}

View File

@@ -692,25 +692,34 @@ func (self *SCloudgroup) PerformAttachPolicy(ctx context.Context, userCred mccli
if self.Status != apis.STATUS_AVAILABLE {
return nil, httperrors.NewInvalidStatusError("Can not attach policy in status %s", self.Status)
}
policyObj, err := validators.ValidateModel(ctx, userCred, CloudpolicyManager, &input.CloudpolicyId)
if err != nil {
return nil, err
policyIds := input.CloudpolicyIds
if len(input.CloudpolicyId) > 0 {
policyIds = append(policyIds, input.CloudpolicyId)
}
policy := policyObj.(*SCloudpolicy)
if policy.ManagerId != self.ManagerId || policy.CloudaccountId != self.CloudaccountId {
return nil, httperrors.NewConflictError("policy and groups do not belong to the same account")
}
_, err = self.GetCloudpolicy(input.CloudpolicyId)
if err == nil || errors.Cause(err) == sqlchemy.ErrDuplicateEntry {
return nil, httperrors.NewDuplicateResourceError("policy %s has aleady in this group", input.CloudpolicyId)
if len(policyIds) == 0 {
return nil, httperrors.NewMissingParameterError("cloudpolicy_ids")
}
add := []api.SPolicy{
{
add := []api.SPolicy{}
for i := range policyIds {
policyObj, err := validators.ValidateModel(ctx, userCred, CloudpolicyManager, &policyIds[i])
if err != nil {
return nil, err
}
policy := policyObj.(*SCloudpolicy)
if policy.ManagerId != self.ManagerId || policy.CloudaccountId != self.CloudaccountId {
return nil, httperrors.NewConflictError("policy and groups do not belong to the same account")
}
_, err = self.GetCloudpolicy(policy.Id)
if err == nil || errors.Cause(err) == sqlchemy.ErrDuplicateEntry {
return nil, httperrors.NewDuplicateResourceError("policy %s has aleady in this group", policy.Id)
}
add = append(add, api.SPolicy{
Name: policy.Name,
ExternalId: policy.ExternalId,
PolicyType: policy.PolicyType,
},
})
}
return nil, self.StartSetPoliciesTask(ctx, userCred, add, nil, "")
@@ -723,23 +732,35 @@ func (self *SCloudgroup) PerformDetachPolicy(ctx context.Context, userCred mccli
return nil, httperrors.NewInvalidStatusError("Can not detach policy in status %s", self.Status)
}
policObj, err := validators.ValidateModel(ctx, userCred, CloudpolicyManager, &input.CloudpolicyId)
if err != nil {
return nil, err
policyIds := input.CloudpolicyIds
if len(input.CloudpolicyId) > 0 {
policyIds = append(policyIds, input.CloudpolicyId)
}
policy := policObj.(*SCloudpolicy)
_, err = self.GetCloudpolicy(input.CloudpolicyId)
if err != nil && errors.Cause(err) == sql.ErrNoRows {
return nil, nil
if len(policyIds) == 0 {
return nil, httperrors.NewMissingParameterError("cloudpolicy_ids")
}
del := []api.SPolicy{
{
del := []api.SPolicy{}
for i := range policyIds {
policObj, err := validators.ValidateModel(ctx, userCred, CloudpolicyManager, &policyIds[i])
if err != nil {
return nil, err
}
policy := policObj.(*SCloudpolicy)
_, err = self.GetCloudpolicy(policy.Id)
if err != nil && errors.Cause(err) == sql.ErrNoRows {
continue
}
del = append(del, api.SPolicy{
Name: policy.Name,
ExternalId: policy.ExternalId,
PolicyType: policy.PolicyType,
},
})
}
if len(del) == 0 {
return nil, nil
}
return nil, self.StartSetPoliciesTask(ctx, userCred, nil, del, "")

View File

@@ -49,6 +49,10 @@ func (set ProxyEndpoints) Copy() apihelper.IModelSet {
return setCopy
}
func (set ProxyEndpoints) IncludeDetails() bool {
return false
}
func (ms ProxyEndpoints) joinForwards(subEntries Forwards) bool {
correct := true
for _, subEntry := range subEntries {
@@ -88,3 +92,7 @@ func (set Forwards) Copy() apihelper.IModelSet {
}
return setCopy
}
func (set Forwards) IncludeDetails() bool {
return false
}

View File

@@ -70,7 +70,7 @@ func (h hostDevice) ValidateCreateData(ctx context.Context, userCred mcclient.To
return dev, nil
}
func (h hostDevice) ToHostDevice(dev *api.ContainerDevice) (*hostapi.ContainerDevice, error) {
func (h hostDevice) ToHostDevice(dev *api.ContainerDevice, guestId string) (*hostapi.ContainerDevice, error) {
return &hostapi.ContainerDevice{
Type: apis.CONTAINER_DEVICE_TYPE_HOST,
ContainerPath: dev.Host.ContainerPath,

View File

@@ -19,7 +19,6 @@ import (
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
"yunion.io/x/onecloud/pkg/apis"
api "yunion.io/x/onecloud/pkg/apis/compute"
@@ -87,7 +86,7 @@ func (i isolatedDevice) ValidateCreateData(ctx context.Context, userCred mcclien
return nil, errors.Wrapf(err, "validate create data %s", jsonutils.Marshal(dev))
}
isoDev := dev.IsolatedDevice
podDevs, err := pod.GetIsolatedDevices()
podDevs, err := pod.GetGuestIsolatedDevices()
if err != nil {
return nil, errors.Wrap(err, "get isolated devices")
}
@@ -96,7 +95,8 @@ func (i isolatedDevice) ValidateCreateData(ctx context.Context, userCred mcclien
if index >= len(podDevs) {
return nil, httperrors.NewInputParameterError("index %d is large than isolated device size %d", index, len(podDevs))
}
isoDev.Id = podDevs[index].GetId()
isoDev.Id = podDevs[index].IsolatedDeviceId
isoDev.GuestIsolatedDeviceIndex = int(podDevs[index].Index)
// remove index
isoDev.Index = nil
} else {
@@ -104,13 +104,15 @@ func (i isolatedDevice) ValidateCreateData(ctx context.Context, userCred mcclien
return nil, httperrors.NewNotEmptyError("id is empty")
}
foundDisk := false
for _, d := range podDevs {
for i := range podDevs {
d := podDevs[i].GetIsolatedDevice()
if d.GetId() == isoDev.Id || d.GetName() == isoDev.Id {
isoDev.Id = d.GetId()
isoDev.GuestIsolatedDeviceIndex = int(podDevs[i].Index)
foundDisk = true
devType := d.DevType
if !sets.NewString(api.VALID_CONTAINER_DEVICE_TYPES...).Has(devType) {
return nil, httperrors.NewInputParameterError("device type %s is not supported by container", devType)
host := d.GetHost()
if host.HostType != api.HOST_TYPE_CONTAINER {
return nil, httperrors.NewInputParameterError("device %s is not supported by container", isoDev.Id)
}
break
}
@@ -123,13 +125,17 @@ func (i isolatedDevice) ValidateCreateData(ctx context.Context, userCred mcclien
return dev, nil
}
func (i isolatedDevice) ToHostDevice(dev *api.ContainerDevice) (*hostapi.ContainerDevice, error) {
func (i isolatedDevice) ToHostDevice(dev *api.ContainerDevice, guestId string) (*hostapi.ContainerDevice, error) {
input := dev.IsolatedDevice
isoDevObj, err := models.IsolatedDeviceManager.FetchById(input.Id)
if err != nil {
return nil, errors.Wrapf(err, "Fetch isolated device by id %s", input.Id)
}
isoDev := isoDevObj.(*models.SIsolatedDevice)
gdev, err := isoDev.GetGuestIsolatedDevice(guestId, input.GuestIsolatedDeviceIndex)
if err != nil {
return nil, errors.Wrap(err, "GetGuestIsolatedDevice")
}
return &hostapi.ContainerDevice{
Type: dev.Type,
IsolatedDevice: &hostapi.ContainerIsolatedDevice{
@@ -138,7 +144,10 @@ func (i isolatedDevice) ToHostDevice(dev *api.ContainerDevice) (*hostapi.Contain
Path: isoDev.DevicePath,
CardPath: isoDev.CardPath,
DeviceType: isoDev.DevType,
SharingMode: isoDev.SharingMode,
RenderPath: isoDev.RenderPath,
MemoryLimit: gdev.DeviceMemorySize,
SmUtilLimit: gdev.SmUtilLimit,
Index: isoDev.Index,
DeviceMinor: isoDev.DeviceMinor,
OnlyEnv: input.OnlyEnv,

View File

@@ -710,11 +710,11 @@ func (base *SBaseGuestDriver) ValidateGuestHotChangeConfigInput(ctx context.Cont
return confs, nil
}
func (base *SBaseGuestDriver) BeforeDetachIsolatedDevice(ctx context.Context, cred mcclient.TokenCredential, guest *models.SGuest, dev *models.SIsolatedDevice) error {
func (base *SBaseGuestDriver) BeforeDetachIsolatedDevice(ctx context.Context, cred mcclient.TokenCredential, guest *models.SGuest, dev *models.SGuestIsolatedDevice) error {
return nil
}
func (base *SBaseGuestDriver) BeforeAttachIsolatedDevice(ctx context.Context, cred mcclient.TokenCredential, guest *models.SGuest, dev *models.SIsolatedDevice) error {
func (base *SBaseGuestDriver) BeforeAttachIsolatedDevice(ctx context.Context, cred mcclient.TokenCredential, guest *models.SGuest, dev *models.SGuestIsolatedDevice) error {
return nil
}

View File

@@ -851,7 +851,7 @@ func (self *SKVMGuestDriver) CheckMigrate(ctx context.Context, guest *models.SGu
}
}
}
devices, err := guest.GetIsolatedDevices()
devices, err := guest.GetGuestIsolatedDevices()
if err != nil {
return errors.Wrapf(err, "GetIsolatedDevices")
}
@@ -879,7 +879,7 @@ func (self *SKVMGuestDriver) CheckLiveMigrate(ctx context.Context, guest *models
if cdrom != nil && len(cdrom.ImageId) > 0 {
return httperrors.NewBadRequestError("Cannot live migrate with cdrom")
}
devices, err := guest.GetIsolatedDevices()
devices, err := guest.GetGuestIsolatedDevices()
if err != nil {
return errors.Wrapf(err, "GetIsolatedDevices")
}

View File

@@ -646,12 +646,13 @@ func (drv *SManagedVirtualizedGuestDriver) RemoteDeployGuestForCreate(ctx contex
}
}
devs, err := guest.GetIsolatedDevices()
devs, err := guest.GetGuestIsolatedDevices()
if err != nil {
return nil, errors.Wrapf(err, "GetIsolatedDevices")
}
desc.IsolateDevices = []cloudprovider.SIsolateDevice{}
for _, dev := range devs {
for i := range devs {
dev := devs[i].GetIsolatedDevice()
desc.IsolateDevices = append(desc.IsolateDevices, cloudprovider.SIsolateDevice{
Id: dev.ExternalId,
Name: dev.Name,

View File

@@ -608,7 +608,7 @@ func (p *SPodDriver) RequestDeleteSnapshot(ctx context.Context, guest *models.SG
return p.SKVMGuestDriver.RequestDeleteSnapshot(ctx, guest, task, params)
}
func (p *SPodDriver) BeforeDetachIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, dev *models.SIsolatedDevice) error {
func (p *SPodDriver) BeforeDetachIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, gdev *models.SGuestIsolatedDevice) error {
ctrs, err := models.GetContainerManager().GetContainersByPod(guest.GetId())
if err != nil {
return errors.Wrapf(err, "get containers by pod %s", guest.GetId())
@@ -620,10 +620,11 @@ func (p *SPodDriver) BeforeDetachIsolatedDevice(ctx context.Context, userCred mc
newDevs := make([]*api.ContainerDevice, 0)
releasedDevs := make(map[string]models.ContainerReleasedDevice)
for _, curDev := range devs {
if curDev.IsolatedDevice == nil || curDev.IsolatedDevice.Id != dev.GetId() {
if curDev.IsolatedDevice == nil || (curDev.IsolatedDevice.Id != gdev.IsolatedDeviceId && curDev.IsolatedDevice.GuestIsolatedDeviceIndex == int(gdev.Index)) {
tmpDev := curDev
newDevs = append(newDevs, tmpDev)
} else {
dev := gdev.GetIsolatedDevice()
releasedDevs[curDev.IsolatedDevice.Id] = *models.NewContainerReleasedDevice(curDev, dev.DevType, dev.Model)
}
}
@@ -640,7 +641,7 @@ func (p *SPodDriver) BeforeDetachIsolatedDevice(ctx context.Context, userCred mc
return nil
}
func (p *SPodDriver) BeforeAttachIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, dev *models.SIsolatedDevice) error {
func (p *SPodDriver) BeforeAttachIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, dev *models.SGuestIsolatedDevice) error {
ctrs, err := models.GetContainerManager().GetContainersByPod(guest.GetId())
if err != nil {
return errors.Wrapf(err, "get containers by pod %s", guest.GetId())
@@ -654,7 +655,7 @@ func (p *SPodDriver) BeforeAttachIsolatedDevice(ctx context.Context, userCred mc
return nil
}
func (p *SPodDriver) attachIsolatedDeviceToContainer(ctx context.Context, userCred mcclient.TokenCredential, ctrPtr *models.SContainer, dev *models.SIsolatedDevice) error {
func (p *SPodDriver) attachIsolatedDeviceToContainer(ctx context.Context, userCred mcclient.TokenCredential, ctrPtr *models.SContainer, gdev *models.SGuestIsolatedDevice) error {
rlsDevs, err := ctrPtr.GetReleasedDevices(ctx, userCred)
if err != nil {
return errors.Wrapf(err, "get release devices for container %s", ctrPtr.GetId())
@@ -672,7 +673,7 @@ func (p *SPodDriver) attachIsolatedDeviceToContainer(ctx context.Context, userCr
if curDev.IsolatedDevice == nil {
continue
}
if curDev.IsolatedDevice.Id == dev.GetId() {
if curDev.IsolatedDevice.Id == gdev.IsolatedDeviceId && curDev.IsolatedDevice.GuestIsolatedDeviceIndex == int(gdev.Index) {
shouldUpdate = false
break
}
@@ -681,7 +682,8 @@ func (p *SPodDriver) attachIsolatedDeviceToContainer(ctx context.Context, userCr
spec.Devices = append(spec.Devices, &api.ContainerDevice{
Type: apis.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
IsolatedDevice: &api.ContainerIsolatedDevice{
Id: dev.GetId(),
Id: gdev.IsolatedDeviceId,
GuestIsolatedDeviceIndex: int(gdev.Index),
},
})
if _, err := db.Update(ctrPtr, func() error {
@@ -695,6 +697,7 @@ func (p *SPodDriver) attachIsolatedDeviceToContainer(ctx context.Context, userCr
if rlsDev.IsolatedDevice == nil {
continue
}
dev := gdev.GetIsolatedDevice()
if rlsDev.DeviceModel == dev.Model && rlsDev.DeviceType == dev.DevType {
delete(rlsDevs, id)
if err := ctrPtr.SaveReleasedDevices(ctx, userCred, rlsDevs); err != nil {

View File

@@ -119,9 +119,9 @@ func (self *SVirtualizedGuestDriver) Attach2RandomNetwork(guest *models.SGuest,
dev, _ := idev.(*models.SIsolatedDevice)
sriovWires = []string{dev.WireId}
} else {
wires, err := models.IsolatedDeviceManager.FindUnusedNicWiresByModel(netConfig.SriovDevice.Model)
wires, err := models.IsolatedDeviceManager.FindAvailableNicWiresByModel(netConfig.SriovDevice.Model)
if err != nil {
return nil, errors.Wrap(err, "FindUnusedNicWiresByModel")
return nil, errors.Wrap(err, "FindAvailableNicWiresByModel")
}
sriovWires = wires
}

View File

@@ -61,7 +61,7 @@ type SAccessGroupRule struct {
// 云上Id, 对应云上资源自身Id
ExternalId string `width:"256" charset:"utf8" index:"true" list:"user" create:"domain_optional" update:"admin" json:"external_id"`
Priority int `default:"1" list:"user" update:"user" list:"user"`
Priority int `default:"1" list:"user" update:"user"`
Source string `width:"16" charset:"ascii" list:"user" update:"user" create:"required"`
RWAccessType string `width:"16" charset:"ascii" list:"user" update:"user" create:"required"`
UserAccessType string `width:"16" charset:"ascii" list:"user" update:"user" create:"required"`

View File

@@ -869,9 +869,11 @@ func getStorageTypes(
}
type PCIDevModelTypes struct {
Model string
DevType string
SizeMB int
Model string
DevType string
SharingMode string
NvmeSizeMB int
DevMemorySize int
VirtualDev bool
Hypervisor string
@@ -885,7 +887,7 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
hostQuery = StorageManager.FilterByOwner(ctx, hostQuery, StorageManager, userCred, ownerId, rbacscope.ScopeDomain)
}
if len(tenantId) > 0 {
devicesQ = devicesQ.IsNullOrEmpty("guest_id")
devicesQ = IsolatedDeviceManager.GetAvailableIsolatedDeviceQuery(devicesQ)
subq := db.SharedResourceManager.Query("resource_id")
subq = subq.Equals("resource_type", IsolatedDeviceManager.Keyword())
subq = subq.Equals("target_project_id", tenantId)
@@ -916,7 +918,7 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
devices := devicesQ.SubQuery()
hosts := hostQuery.SubQuery()
q := devices.Query(hosts.Field("host_type"), devices.Field("model"), devices.Field("dev_type"), devices.Field("nvme_size_mb"))
q := devices.Query(hosts.Field("host_type"), devices.Field("model"), devices.Field("dev_type"), devices.Field("sharing_mode"), devices.Field("nvme_size_mb"), devices.Field("memory_size"))
q = q.Filter(sqlchemy.NotIn(devices.Field("dev_type"), []string{api.USB_TYPE, api.NIC_TYPE}))
if zone != nil {
q = q.Join(hosts, sqlchemy.Equals(devices.Field("host_id"), hosts.Field("id")))
@@ -935,7 +937,7 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
sqlchemy.IsNullOrEmpty(hosts.Field("manager_id")),
))
}*/
q = q.GroupBy(hosts.Field("host_type"), devices.Field("model"), devices.Field("dev_type"), devices.Field("nvme_size_mb"))
q = q.GroupBy(hosts.Field("host_type"), devices.Field("model"), devices.Field("dev_type"), devices.Field("sharing_mode"), devices.Field("nvme_size_mb"), devices.Field("memory_size"))
rows, err := q.Rows()
if err != nil {
@@ -946,20 +948,21 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
gpus := make([]PCIDevModelTypes, 0)
gpuModels := make([]string, 0)
for rows.Next() {
var m, t string
var sizeMB int
var m, t, sharingMode string
var nvmeSizeMB int
var memSizeMB int
var vdev bool
var hypervisor string
var hostType string
rows.Scan(&hostType, &m, &t, &sizeMB)
rows.Scan(&hostType, &m, &t, &sharingMode, &nvmeSizeMB, &memSizeMB)
if m == "" {
continue
}
if utils.IsInStringArray(t, api.VITRUAL_DEVICE_TYPES) {
if utils.IsInStringArray(sharingMode, api.VIRTUAL_SHARING_MODES) {
vdev = true
}
if utils.IsInStringArray(t, api.VALID_CONTAINER_DEVICE_TYPES) {
if hostType == api.HOST_TYPE_CONTAINER {
hypervisor = api.HYPERVISOR_POD
} else {
hypervisor = api.HYPERVISOR_KVM
@@ -969,7 +972,7 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
hypervisor = api.HYPERVISOR_ZETTAKIT
}
gpus = append(gpus, PCIDevModelTypes{m, t, sizeMB, vdev, hypervisor})
gpus = append(gpus, PCIDevModelTypes{m, t, sharingMode, nvmeSizeMB, memSizeMB, vdev, hypervisor})
if !utils.IsInStringArray(m, gpuModels) {
gpuModels = append(gpuModels, m)

Some files were not shown because too many files have changed in this diff Show More