329 lines
11 KiB
Go
329 lines
11 KiB
Go
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
|
||
}
|