初始提交: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
+328
View File
@@ -0,0 +1,328 @@
package oci
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/core"
)
// BootVolumeAttachment 是引导卷与实例的挂载关系。
type BootVolumeAttachment struct {
ID string `json:"id"`
InstanceID string `json:"instanceId"`
BootVolumeID string `json:"bootVolumeId"`
LifecycleState string `json:"lifecycleState"`
DisplayName string `json:"displayName"`
AvailabilityDomain string `json:"availabilityDomain"`
}
// VolumeAttachment 是块存储卷与实例的挂载关系。
type VolumeAttachment struct {
ID string `json:"id"`
InstanceID string `json:"instanceId"`
VolumeID string `json:"volumeId"`
VolumeName string `json:"volumeName,omitempty"` // 卷本身的名称(attachment 名是自动生成的)
LifecycleState string `json:"lifecycleState"`
DisplayName string `json:"displayName"`
Device string `json:"device"`
IsReadOnly bool `json:"isReadOnly"`
AvailabilityDomain string `json:"availabilityDomain"`
}
// BlockVolume 是一个块存储卷。
type BlockVolume struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
LifecycleState string `json:"lifecycleState"`
AvailabilityDomain string `json:"availabilityDomain"`
SizeInGBs int64 `json:"sizeInGBs"`
VpusPerGB int64 `json:"vpusPerGB"`
TimeCreated *time.Time `json:"timeCreated"`
}
// ---- 引导卷挂载 ----
// ListBootVolumeAttachments 实现 Client:列出实例的引导卷挂载。
func (c *RealClient) ListBootVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]BootVolumeAttachment, error) {
cc, err := c.computeClient(cred, region)
if err != nil {
return nil, err
}
instResp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID})
if err != nil {
return nil, fmt.Errorf("get instance %s: %w", instanceID, err)
}
// 挂载记录在实例所在 compartment,复用上面查到的实例定位,传租户根会漏查
resp, err := cc.ListBootVolumeAttachments(ctx, core.ListBootVolumeAttachmentsRequest{
AvailabilityDomain: instResp.AvailabilityDomain,
CompartmentId: hostCompartment(cred, deref(instResp.CompartmentId)),
InstanceId: &instanceID,
})
if err != nil {
return nil, fmt.Errorf("list boot volume attachments: %w", err)
}
atts := make([]BootVolumeAttachment, 0, len(resp.Items))
for _, item := range resp.Items {
atts = append(atts, toBootVolumeAttachment(item))
}
return atts, nil
}
// AttachBootVolume 实现 Client;实例需处于 STOPPED,引导卷与实例同可用域。
func (c *RealClient) AttachBootVolume(ctx context.Context, cred Credentials, region, instanceID, bootVolumeID string) (BootVolumeAttachment, error) {
cc, err := c.computeClient(cred, region)
if err != nil {
return BootVolumeAttachment{}, err
}
resp, err := cc.AttachBootVolume(ctx, core.AttachBootVolumeRequest{
AttachBootVolumeDetails: core.AttachBootVolumeDetails{
InstanceId: &instanceID,
BootVolumeId: &bootVolumeID,
},
})
if err != nil {
return BootVolumeAttachment{}, fmt.Errorf("attach boot volume: %w", err)
}
return toBootVolumeAttachment(resp.BootVolumeAttachment), nil
}
// DetachBootVolume 实现 Client;实例需处于 STOPPED。
func (c *RealClient) DetachBootVolume(ctx context.Context, cred Credentials, region, attachmentID string) error {
cc, err := c.computeClient(cred, region)
if err != nil {
return err
}
if _, err := cc.DetachBootVolume(ctx, core.DetachBootVolumeRequest{BootVolumeAttachmentId: &attachmentID}); err != nil {
return fmt.Errorf("detach boot volume %s: %w", attachmentID, err)
}
return nil
}
// ReplaceBootVolume 实现 Client:分离实例当前引导卷,挂载新引导卷。
// 实例必须处于 STOPPED,全程等待分离完成后再挂载。
func (c *RealClient) ReplaceBootVolume(ctx context.Context, cred Credentials, region, instanceID, newBootVolumeID string) (BootVolumeAttachment, error) {
cc, err := c.computeClient(cred, region)
if err != nil {
return BootVolumeAttachment{}, err
}
instResp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID})
if err != nil {
return BootVolumeAttachment{}, fmt.Errorf("get instance %s: %w", instanceID, err)
}
if instResp.LifecycleState != core.InstanceLifecycleStateStopped {
return BootVolumeAttachment{}, fmt.Errorf("replace boot volume: instance must be STOPPED, current %s", instResp.LifecycleState)
}
atts, err := c.ListBootVolumeAttachments(ctx, cred, region, instanceID)
if err != nil {
return BootVolumeAttachment{}, err
}
for _, att := range atts {
if att.LifecycleState != string(core.BootVolumeAttachmentLifecycleStateDetached) {
if err := c.DetachBootVolume(ctx, cred, region, att.ID); err != nil {
return BootVolumeAttachment{}, err
}
if err := waitBootVolumeDetached(ctx, cc, att.ID); err != nil {
return BootVolumeAttachment{}, err
}
}
}
return c.AttachBootVolume(ctx, cred, region, instanceID, newBootVolumeID)
}
// waitBootVolumeDetached 轮询等待引导卷挂载进入 DETACHED;
// OCI 可能在分离完成后回收挂载记录,404 同样视为已分离。
func waitBootVolumeDetached(ctx context.Context, cc core.ComputeClient, attachmentID string) error {
for i := 0; i < 60; i++ {
resp, err := cc.GetBootVolumeAttachment(ctx, core.GetBootVolumeAttachmentRequest{BootVolumeAttachmentId: &attachmentID})
if err != nil {
var svcErr common.ServiceError
if errors.As(err, &svcErr) && svcErr.GetHTTPStatusCode() == 404 {
return nil
}
return fmt.Errorf("poll boot volume attachment: %w", err)
}
if resp.LifecycleState == core.BootVolumeAttachmentLifecycleStateDetached {
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("poll boot volume detached: %w", ctx.Err())
case <-time.After(3 * time.Second):
}
}
return fmt.Errorf("poll boot volume detached: timed out")
}
// ---- 块卷挂载 ----
// ListVolumeAttachments 实现 Client:列出实例的块卷挂载。
func (c *RealClient) ListVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]VolumeAttachment, error) {
cc, err := c.computeClient(cred, region)
if err != nil {
return nil, err
}
req := core.ListVolumeAttachmentsRequest{CompartmentId: cred.EffectiveCompartment()}
if instanceID != "" {
// 挂载记录在实例所在 compartment,先取实例定位,传租户根会漏查
instResp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID})
if err != nil {
return nil, fmt.Errorf("get instance %s: %w", instanceID, err)
}
req.CompartmentId = hostCompartment(cred, deref(instResp.CompartmentId))
req.InstanceId = &instanceID
}
resp, err := cc.ListVolumeAttachments(ctx, req)
if err != nil {
return nil, fmt.Errorf("list volume attachments: %w", err)
}
atts := make([]VolumeAttachment, 0, len(resp.Items))
for _, item := range resp.Items {
atts = append(atts, toVolumeAttachment(item))
}
c.attachVolumeNames(ctx, cred, region, atts)
return atts, nil
}
// attachVolumeNames 补全每条挂载的卷名(attachment 名是自动生成的无意义串);
// 引导卷可作数据卷附加,按 OCID 类型分别查询,查询失败留空不影响列表。
func (c *RealClient) attachVolumeNames(ctx context.Context, cred Credentials, region string, atts []VolumeAttachment) {
bc, err := c.blockstorageClient(cred, region)
if err != nil {
return
}
names := map[string]string{}
for i := range atts {
id := atts[i].VolumeID
if id == "" {
continue
}
name, ok := names[id]
if !ok {
name = lookupVolumeName(ctx, bc, id)
names[id] = name
}
atts[i].VolumeName = name
}
}
func lookupVolumeName(ctx context.Context, bc core.BlockstorageClient, id string) string {
if strings.Contains(id, ".bootvolume.") {
resp, err := bc.GetBootVolume(ctx, core.GetBootVolumeRequest{BootVolumeId: &id})
if err != nil {
return ""
}
return deref(resp.DisplayName)
}
resp, err := bc.GetVolume(ctx, core.GetVolumeRequest{VolumeId: &id})
if err != nil {
return ""
}
return deref(resp.DisplayName)
}
// AttachVolume 实现 Client:以半虚拟化方式挂载块卷(无需 iSCSI 配置)。
func (c *RealClient) AttachVolume(ctx context.Context, cred Credentials, region, instanceID, volumeID string, readOnly bool) (VolumeAttachment, error) {
cc, err := c.computeClient(cred, region)
if err != nil {
return VolumeAttachment{}, err
}
resp, err := cc.AttachVolume(ctx, core.AttachVolumeRequest{
AttachVolumeDetails: core.AttachParavirtualizedVolumeDetails{
InstanceId: &instanceID,
VolumeId: &volumeID,
IsReadOnly: &readOnly,
},
})
if err != nil {
return VolumeAttachment{}, fmt.Errorf("attach volume: %w", err)
}
return toVolumeAttachment(resp.VolumeAttachment), nil
}
// DetachVolume 实现 Client。
func (c *RealClient) DetachVolume(ctx context.Context, cred Credentials, region, attachmentID string) error {
cc, err := c.computeClient(cred, region)
if err != nil {
return err
}
if _, err := cc.DetachVolume(ctx, core.DetachVolumeRequest{VolumeAttachmentId: &attachmentID}); err != nil {
return fmt.Errorf("detach volume %s: %w", attachmentID, err)
}
return nil
}
// ListBlockVolumes 实现 ClientavailabilityDomain 为空时列出全区域块卷。
func (c *RealClient) ListBlockVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BlockVolume, error) {
bc, err := c.blockstorageClient(cred, region)
if err != nil {
return nil, err
}
var volumes []BlockVolume
var page *string
for {
req := core.ListVolumesRequest{CompartmentId: cred.EffectiveCompartment(), Page: page}
if availabilityDomain != "" {
req.AvailabilityDomain = &availabilityDomain
}
resp, err := bc.ListVolumes(ctx, req)
if err != nil {
return nil, fmt.Errorf("list volumes: %w", err)
}
for _, item := range resp.Items {
volumes = append(volumes, toBlockVolume(item))
}
if resp.OpcNextPage == nil {
return orEmpty(volumes), nil
}
page = resp.OpcNextPage
}
}
func toBootVolumeAttachment(a core.BootVolumeAttachment) BootVolumeAttachment {
return BootVolumeAttachment{
ID: deref(a.Id),
InstanceID: deref(a.InstanceId),
BootVolumeID: deref(a.BootVolumeId),
LifecycleState: string(a.LifecycleState),
DisplayName: deref(a.DisplayName),
AvailabilityDomain: deref(a.AvailabilityDomain),
}
}
func toVolumeAttachment(a core.VolumeAttachment) VolumeAttachment {
out := VolumeAttachment{
ID: deref(a.GetId()),
InstanceID: deref(a.GetInstanceId()),
VolumeID: deref(a.GetVolumeId()),
LifecycleState: string(a.GetLifecycleState()),
DisplayName: deref(a.GetDisplayName()),
Device: deref(a.GetDevice()),
AvailabilityDomain: deref(a.GetAvailabilityDomain()),
}
if ro := a.GetIsReadOnly(); ro != nil {
out.IsReadOnly = *ro
}
return out
}
func toBlockVolume(v core.Volume) BlockVolume {
out := BlockVolume{
ID: deref(v.Id),
DisplayName: deref(v.DisplayName),
LifecycleState: string(v.LifecycleState),
AvailabilityDomain: deref(v.AvailabilityDomain),
TimeCreated: sdkTime(v.TimeCreated),
}
if v.SizeInGBs != nil {
out.SizeInGBs = *v.SizeInGBs
}
if v.VpusPerGB != nil {
out.VpusPerGB = *v.VpusPerGB
}
return out
}