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 }