初始提交:OCI 面板后端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:04 +08:00
commit b9a3e97e84
168 changed files with 31794 additions and 0 deletions
+452
View File
@@ -0,0 +1,452 @@
package oci
import (
"context"
"encoding/base64"
"fmt"
"strings"
"sync"
"time"
"github.com/oracle/oci-go-sdk/v65/core"
"github.com/oracle/oci-go-sdk/v65/identity"
)
// Instance 是一个计算实例;IP 字段来自 VNIC 补全查询。
type Instance struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
LifecycleState string `json:"lifecycleState"`
Shape string `json:"shape"`
Ocpus float32 `json:"ocpus"`
MemoryInGBs float32 `json:"memoryInGBs"`
AvailabilityDomain string `json:"availabilityDomain"`
Region string `json:"region"`
CompartmentID string `json:"compartmentId"`
ImageID string `json:"imageId"`
SubnetID string `json:"subnetId,omitempty"`
PrivateIP string `json:"privateIp,omitempty"`
PublicIP string `json:"publicIp,omitempty"`
Ipv6Addresses []string `json:"ipv6Addresses,omitempty"`
FreeformTags map[string]string `json:"freeformTags,omitempty"`
DefinedTags map[string]map[string]any `json:"definedTags,omitempty"`
TimeCreated *time.Time `json:"timeCreated"`
}
// CreateInstanceInput 是创建实例的输入:镜像与引导卷二选一作为启动源,
// Flex shape 必须提供 ocpus 与 memoryInGBs。
type CreateInstanceInput struct {
Region string
CompartmentID string // 目标 compartment,空为租户根
AvailabilityDomain string
DisplayName string
Shape string
Ocpus float32
MemoryInGBs float32
ImageID string
BootVolumeID string
BootVolumeSizeGBs int64
BootVolumeVpusPerGB int64 // 引导卷性能,10 均衡 / 20 高性能,仅镜像启动源生效
SubnetID string // 为空时自动创建 VCN 与子网
AssignPublicIP bool
AssignIpv6 bool
SSHPublicKey string // 与 RootPassword 互斥
RootPassword string // 与 SSHPublicKey 互斥,非空时生成开启 root 密码登录的 cloud-init
GenerateRootPassword bool // 由服务端生成随机 root 密码并写入 FreeformTags["RootPassword"]
UserData string // 显式 cloud-initbase64 编码;RootPassword 非空时被其生成脚本覆盖
FreeformTags map[string]string // 实例自由标签
}
// UpdateInstanceInput 是更新实例的输入;零值字段不更新。
type UpdateInstanceInput struct {
Region string
DisplayName string
Shape string // 更换 shape;运行中实例 OCI 会自动重启,换 Flex 时须同时给 ocpus
Ocpus float32
MemoryInGBs float32
}
// instanceActions 是允许的电源操作集合。
var instanceActions = map[string]core.InstanceActionActionEnum{
"START": core.InstanceActionActionStart,
"STOP": core.InstanceActionActionStop,
"RESET": core.InstanceActionActionReset,
"SOFTSTOP": core.InstanceActionActionSoftstop,
"SOFTRESET": core.InstanceActionActionSoftreset,
}
func (c *RealClient) computeClient(cred Credentials, region string) (core.ComputeClient, error) {
cc, err := core.NewComputeClientWithConfigurationProvider(provider(cred))
if err != nil {
return cc, fmt.Errorf("new compute client: %w", err)
}
applyProxy(&cc.BaseClient, cred)
if region != "" {
cc.SetRegion(normalizeRegion(region))
}
return cc, nil
}
// ListAvailabilityDomains 实现 Client:列出区域内的可用域名称。
func (c *RealClient) ListAvailabilityDomains(ctx context.Context, cred Credentials, region string) ([]string, error) {
ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred))
if err != nil {
return nil, fmt.Errorf("new identity client: %w", err)
}
applyProxy(&ic.BaseClient, cred)
if region != "" {
ic.SetRegion(normalizeRegion(region))
}
resp, err := ic.ListAvailabilityDomains(ctx, identity.ListAvailabilityDomainsRequest{
CompartmentId: &cred.TenancyOCID,
})
if err != nil {
return nil, fmt.Errorf("list availability domains: %w", err)
}
names := make([]string, 0, len(resp.Items))
for _, ad := range resp.Items {
names = append(names, deref(ad.Name))
}
return names, nil
}
// defaultAvailabilityDomain 返回区域内第一个可用域(即 AD-1)。
func (c *RealClient) defaultAvailabilityDomain(ctx context.Context, cred Credentials, region string) (string, error) {
ads, err := c.ListAvailabilityDomains(ctx, cred, region)
if err != nil {
return "", fmt.Errorf("resolve default availability domain: %w", err)
}
if len(ads) == 0 {
return "", fmt.Errorf("resolve default availability domain: region has no availability domain")
}
return ads[0], nil
}
// ListInstances 实现 Client:列出实例并补全网络 IP 信息。
func (c *RealClient) ListInstances(ctx context.Context, cred Credentials, region string) ([]Instance, error) {
cc, err := c.computeClient(cred, region)
if err != nil {
return nil, err
}
var instances []Instance
var page *string
for {
resp, err := cc.ListInstances(ctx, core.ListInstancesRequest{
CompartmentId: cred.EffectiveCompartment(),
Page: page,
})
if err != nil {
return nil, fmt.Errorf("list instances: %w", err)
}
for _, item := range resp.Items {
// 已终止实例的 VNIC 已不存在,保留在列表中仅供展示
instances = append(instances, toInstance(item))
}
if resp.OpcNextPage == nil {
break
}
page = resp.OpcNextPage
}
c.attachInstanceIPs(ctx, cred, region, instances)
// 列表瘦身:标签体积大且列表页不消费(详情接口仍完整返回)
for i := range instances {
instances[i].FreeformTags = nil
instances[i].DefinedTags = nil
}
return orEmpty(instances), nil
}
// LaunchInstance 实现 ClientavailabilityDomain 为空时默认使用第一个
// 可用域(ad-1),subnetId 为空时自动准备默认网络。
func (c *RealClient) LaunchInstance(ctx context.Context, cred Credentials, in CreateInstanceInput) (Instance, error) {
cc, err := c.computeClient(cred, in.Region)
if err != nil {
return Instance{}, err
}
if in.AvailabilityDomain == "" {
ad, err := c.defaultAvailabilityDomain(ctx, cred, in.Region)
if err != nil {
return Instance{}, err
}
in.AvailabilityDomain = ad
}
if in.SubnetID == "" {
subnetID, err := c.ensureDefaultSubnet(ctx, cred, in.Region)
if err != nil {
return Instance{}, fmt.Errorf("auto provision subnet: %w", err)
}
in.SubnetID = subnetID
}
details, err := buildLaunchDetails(*cred.EffectiveCompartment(), in)
if err != nil {
return Instance{}, err
}
resp, err := cc.LaunchInstance(ctx, core.LaunchInstanceRequest{LaunchInstanceDetails: details})
if err != nil {
return Instance{}, fmt.Errorf("launch instance: %w", err)
}
return toInstance(resp.Instance), nil
}
func buildLaunchDetails(compartmentID string, in CreateInstanceInput) (core.LaunchInstanceDetails, error) {
details := core.LaunchInstanceDetails{
CompartmentId: &compartmentID,
AvailabilityDomain: &in.AvailabilityDomain,
Shape: &in.Shape,
DisplayName: &in.DisplayName,
SourceDetails: buildInstanceSource(in),
CreateVnicDetails: &core.CreateVnicDetails{
SubnetId: &in.SubnetID,
AssignPublicIp: &in.AssignPublicIP,
},
}
if details.SourceDetails == nil {
return details, fmt.Errorf("launch instance: imageId or bootVolumeId is required")
}
if in.AssignIpv6 {
details.CreateVnicDetails.AssignIpv6Ip = &in.AssignIpv6
}
if in.Ocpus > 0 {
details.ShapeConfig = &core.LaunchInstanceShapeConfigDetails{Ocpus: &in.Ocpus}
if in.MemoryInGBs > 0 {
details.ShapeConfig.MemoryInGBs = &in.MemoryInGBs
}
}
if len(in.FreeformTags) > 0 {
details.FreeformTags = in.FreeformTags
}
details.Metadata = buildInstanceMetadata(in)
return details, nil
}
func buildInstanceSource(in CreateInstanceInput) core.InstanceSourceDetails {
if in.BootVolumeID != "" {
return core.InstanceSourceViaBootVolumeDetails{BootVolumeId: &in.BootVolumeID}
}
if in.ImageID != "" {
src := core.InstanceSourceViaImageDetails{ImageId: &in.ImageID}
if in.BootVolumeSizeGBs > 0 {
src.BootVolumeSizeInGBs = &in.BootVolumeSizeGBs
}
if in.BootVolumeVpusPerGB > 0 {
src.BootVolumeVpusPerGB = &in.BootVolumeVpusPerGB
}
return src
}
return nil
}
func buildInstanceMetadata(in CreateInstanceInput) map[string]string {
metadata := make(map[string]string, 2)
if in.SSHPublicKey != "" {
metadata["ssh_authorized_keys"] = in.SSHPublicKey
}
// RootPassword 生成的脚本优先作为 user_data,其次才是显式 UserData
if in.RootPassword != "" {
metadata["user_data"] = rootPasswordUserData(in.RootPassword)
} else if in.UserData != "" {
metadata["user_data"] = in.UserData
}
if len(metadata) == 0 {
return nil
}
return metadata
}
// rootPasswordUserData 生成开启 root 密码登录的 cloud-init 脚本并 base64 编码。
// 密码用单引号安全转义,避免 shell 注入。
func rootPasswordUserData(password string) string {
script := "#!/bin/bash\n" +
"echo root:" + shellSingleQuote(password) + " | chpasswd\n" +
"sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin yes/g' /etc/ssh/sshd_config\n" +
"sed -i 's/^#\\?PasswordAuthentication.*/PasswordAuthentication yes/g' /etc/ssh/sshd_config\n" +
"rm -f /etc/ssh/sshd_config.d/* /etc/ssh/ssh_config.d/*\n" +
"systemctl restart sshd\n"
return base64.StdEncoding.EncodeToString([]byte(script))
}
// shellSingleQuote 用单引号包裹字符串,内部单引号转义为 '\”。
func shellSingleQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}
// GetInstance 实现 Client:查询实例并补全网络 IP 信息。
func (c *RealClient) GetInstance(ctx context.Context, cred Credentials, region, instanceID string) (Instance, error) {
cc, err := c.computeClient(cred, region)
if err != nil {
return Instance{}, err
}
resp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID})
if err != nil {
return Instance{}, fmt.Errorf("get instance %s: %w", instanceID, err)
}
instances := []Instance{toInstance(resp.Instance)}
c.attachInstanceIPs(ctx, cred, region, instances)
return instances[0], nil
}
// UpdateInstance 实现 Client:改名或调整 Flex 规格。
func (c *RealClient) UpdateInstance(ctx context.Context, cred Credentials, instanceID string, in UpdateInstanceInput) (Instance, error) {
cc, err := c.computeClient(cred, in.Region)
if err != nil {
return Instance{}, err
}
details := core.UpdateInstanceDetails{}
if in.DisplayName != "" {
details.DisplayName = &in.DisplayName
}
if in.Shape != "" {
details.Shape = &in.Shape
}
if in.Ocpus > 0 {
details.ShapeConfig = &core.UpdateInstanceShapeConfigDetails{Ocpus: &in.Ocpus}
if in.MemoryInGBs > 0 {
details.ShapeConfig.MemoryInGBs = &in.MemoryInGBs
}
}
resp, err := cc.UpdateInstance(ctx, core.UpdateInstanceRequest{
InstanceId: &instanceID,
UpdateInstanceDetails: details,
})
if err != nil {
return Instance{}, fmt.Errorf("update instance %s: %w", instanceID, err)
}
return toInstance(resp.Instance), nil
}
// TerminateInstance 实现 ClientpreserveBootVolume 为 true 时保留引导卷。
func (c *RealClient) TerminateInstance(ctx context.Context, cred Credentials, region, instanceID string, preserveBootVolume bool) error {
cc, err := c.computeClient(cred, region)
if err != nil {
return err
}
_, err = cc.TerminateInstance(ctx, core.TerminateInstanceRequest{
InstanceId: &instanceID,
PreserveBootVolume: &preserveBootVolume,
})
if err != nil {
return fmt.Errorf("terminate instance %s: %w", instanceID, err)
}
return nil
}
// InstanceAction 实现 Client:执行电源操作。
func (c *RealClient) InstanceAction(ctx context.Context, cred Credentials, region, instanceID, action string) (Instance, error) {
actionEnum, ok := instanceActions[strings.ToUpper(strings.TrimSpace(action))]
if !ok {
return Instance{}, fmt.Errorf("instance action: unsupported action %q", action)
}
cc, err := c.computeClient(cred, region)
if err != nil {
return Instance{}, err
}
resp, err := cc.InstanceAction(ctx, core.InstanceActionRequest{
InstanceId: &instanceID,
Action: actionEnum,
})
if err != nil {
return Instance{}, fmt.Errorf("instance action %s on %s: %w", action, instanceID, err)
}
return toInstance(resp.Instance), nil
}
// hostCompartment 返回宿主资源自身所在 compartment 的请求指针:OCI 的
// 关联查询要求 compartmentId 与资源实际所在 compartment 一致,传租户根
// 会漏掉子 compartment 里的资源;id 缺失时保底用生效 compartment。
func hostCompartment(cred Credentials, id string) *string {
if id != "" {
return &id
}
return cred.EffectiveCompartment()
}
// attachInstanceIPs 批量补全实例的子网与 IP:按实例所在 compartment 分组
// 列出 VNIC 挂载,再逐 VNIC 查询地址;直接用实例自带的 compartment
// 避免为定位挂载再逐台调用 GetInstance。
func (c *RealClient) attachInstanceIPs(ctx context.Context, cred Credentials, region string, instances []Instance) {
active := make(map[string]*Instance, len(instances))
compartments := make(map[string]struct{})
for i := range instances {
if instances[i].LifecycleState == string(core.InstanceLifecycleStateTerminated) {
continue
}
active[instances[i].ID] = &instances[i]
compartments[deref(hostCompartment(cred, instances[i].CompartmentID))] = struct{}{}
}
if len(active) == 0 {
return
}
cc, err := c.computeClient(cred, region)
if err != nil {
return
}
vn, err := c.vcnClient(cred, region)
if err != nil {
return
}
for compartmentID := range compartments {
fillInstanceIPs(ctx, cc, vn, compartmentID, active)
}
}
// fillInstanceIPs 列出单个 compartment 内的 VNIC 挂载,为命中的实例补全地址。
// 逐 VNIC 的 GetVnic 是列表接口的主要耗时点,限并发 8 路并行拉取。
func fillInstanceIPs(ctx context.Context, cc core.ComputeClient, vn core.VirtualNetworkClient, compartmentID string, active map[string]*Instance) {
attResp, err := cc.ListVnicAttachments(ctx, core.ListVnicAttachmentsRequest{CompartmentId: &compartmentID})
if err != nil {
return
}
var (
wg sync.WaitGroup
mu sync.Mutex
sem = make(chan struct{}, 8)
)
for _, att := range attResp.Items {
inst, ok := active[deref(att.InstanceId)]
if !ok || att.VnicId == nil {
continue
}
wg.Add(1)
sem <- struct{}{}
go func(vnicID *string, inst *Instance) {
defer wg.Done()
defer func() { <-sem }()
vnicResp, err := vn.GetVnic(ctx, core.GetVnicRequest{VnicId: vnicID})
if err != nil {
return
}
mu.Lock()
inst.SubnetID = deref(vnicResp.SubnetId)
inst.PrivateIP = deref(vnicResp.PrivateIp)
inst.PublicIP = deref(vnicResp.PublicIp)
inst.Ipv6Addresses = vnicResp.Ipv6Addresses
mu.Unlock()
}(att.VnicId, inst)
}
wg.Wait()
}
func toInstance(inst core.Instance) Instance {
out := Instance{
ID: deref(inst.Id),
DisplayName: deref(inst.DisplayName),
LifecycleState: string(inst.LifecycleState),
Shape: deref(inst.Shape),
AvailabilityDomain: deref(inst.AvailabilityDomain),
Region: deref(inst.Region),
CompartmentID: deref(inst.CompartmentId),
ImageID: deref(inst.ImageId),
FreeformTags: inst.FreeformTags,
DefinedTags: inst.DefinedTags,
TimeCreated: sdkTime(inst.TimeCreated),
}
if inst.ShapeConfig != nil {
if inst.ShapeConfig.Ocpus != nil {
out.Ocpus = *inst.ShapeConfig.Ocpus
}
if inst.ShapeConfig.MemoryInGBs != nil {
out.MemoryInGBs = *inst.ShapeConfig.MemoryInGBs
}
}
return out
}