初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -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-init,base64 编码;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 实现 Client;availabilityDomain 为空时默认使用第一个
|
||||
// 可用域(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 实现 Client;preserveBootVolume 为 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
|
||||
}
|
||||
Reference in New Issue
Block a user