初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -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 实现 Client;availabilityDomain 为空时列出全区域块卷。
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user