Files

329 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}