package oci import ( "context" "fmt" "strings" "time" "github.com/oracle/oci-go-sdk/v65/core" ) // BootVolume 是一个引导卷;引导卷由创建实例产生,本面板不单独创建。 // AttachedInstanceID / AttachedInstanceName 与 ImageName 在列表与详情查询时都会补全。 type BootVolume struct { ID string `json:"id"` DisplayName string `json:"displayName"` LifecycleState string `json:"lifecycleState"` AvailabilityDomain string `json:"availabilityDomain"` CompartmentID string `json:"compartmentId"` SizeInGBs int64 `json:"sizeInGBs"` VpusPerGB int64 `json:"vpusPerGB"` ImageID string `json:"imageId"` ImageName string `json:"imageName,omitempty"` AttachedInstanceID string `json:"attachedInstanceId,omitempty"` AttachedInstanceName string `json:"attachedInstanceName,omitempty"` TimeCreated *time.Time `json:"timeCreated"` } // UpdateBootVolumeInput 是更新引导卷的输入;零值字段不更新。 // SizeInGBs 只能扩容,VpusPerGB 调整卷性能(10 均衡 / 20 高性能)。 type UpdateBootVolumeInput struct { Region string DisplayName string SizeInGBs int64 VpusPerGB int64 } func (c *RealClient) blockstorageClient(cred Credentials, region string) (core.BlockstorageClient, error) { bc, err := core.NewBlockstorageClientWithConfigurationProvider(provider(cred)) if err != nil { return bc, fmt.Errorf("new blockstorage client: %w", err) } applyProxy(&bc.BaseClient, cred) if region != "" { bc.SetRegion(normalizeRegion(region)) } return bc, nil } // ListBootVolumes 实现 Client;availabilityDomain 为空时列出全区域引导卷。 func (c *RealClient) ListBootVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BootVolume, error) { bc, err := c.blockstorageClient(cred, region) if err != nil { return nil, err } var volumes []BootVolume var page *string for { req := core.ListBootVolumesRequest{CompartmentId: cred.EffectiveCompartment(), Page: page} if availabilityDomain != "" { req.AvailabilityDomain = &availabilityDomain } resp, err := bc.ListBootVolumes(ctx, req) if err != nil { return nil, fmt.Errorf("list boot volumes: %w", err) } for _, item := range resp.Items { volumes = append(volumes, toBootVolume(item)) } if resp.OpcNextPage == nil { c.attachBootVolumeInstances(ctx, cred, region, volumes) c.attachBootVolumeImages(ctx, cred, region, volumes) return orEmpty(volumes), nil } page = resp.OpcNextPage } } // attachBootVolumeImages 批量补全来源镜像名:按 imageId 去重后逐个查询, // 镜像已删除或查询失败时保持为空。 func (c *RealClient) attachBootVolumeImages(ctx context.Context, cred Credentials, region string, volumes []BootVolume) { names := map[string]string{} for _, v := range volumes { if v.ImageID != "" { names[v.ImageID] = "" } } for id := range names { img, err := c.GetImage(ctx, cred, region, id) if err != nil { continue } names[id] = img.DisplayName } for i := range volumes { volumes[i].ImageName = names[volumes[i].ImageID] } } // attachBootVolumeInstances 批量补全挂载实例:按 AD + compartment 分组各拉一次 // 全量挂载记录(免逐卷查询),构建 bootVolumeId → instanceId 映射后回填。 func (c *RealClient) attachBootVolumeInstances(ctx context.Context, cred Credentials, region string, volumes []BootVolume) { cc, err := c.computeClient(cred, region) if err != nil { return } type scope struct{ ad, comp string } attached := map[string]string{} seen := map[scope]bool{} for _, v := range volumes { s := scope{v.AvailabilityDomain, v.CompartmentID} if seen[s] { continue } seen[s] = true collectBootVolumeAttachments(ctx, cc, s.ad, hostCompartment(cred, s.comp), attached) collectVolumeAttachedBootVolumes(ctx, cc, s.ad, hostCompartment(cred, s.comp), attached) } names := instanceNames(ctx, cc, attached) for i := range volumes { id := attached[volumes[i].ID] volumes[i].AttachedInstanceID = id volumes[i].AttachedInstanceName = names[id] } } // instanceNames 按去重后的实例 ID 批量查询显示名;查询失败的留空。 func instanceNames(ctx context.Context, cc core.ComputeClient, attached map[string]string) map[string]string { names := map[string]string{} for _, instID := range attached { if instID == "" || names[instID] != "" { continue } resp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instID}) if err != nil { continue } names[instID] = deref(resp.DisplayName) } return names } // collectVolumeAttachedBootVolumes 补齐「引导卷作数据卷附加」的挂载:这类挂载 // 记录在块卷挂载列表(volumeId 为引导卷 OCID),引导卷挂载列表查不到。 func collectVolumeAttachedBootVolumes(ctx context.Context, cc core.ComputeClient, ad string, compartmentID *string, out map[string]string) { var page *string for { resp, err := cc.ListVolumeAttachments(ctx, core.ListVolumeAttachmentsRequest{ AvailabilityDomain: &ad, CompartmentId: compartmentID, Page: page, }) if err != nil { return } for _, att := range resp.Items { id := deref(att.GetVolumeId()) if att.GetLifecycleState() == core.VolumeAttachmentLifecycleStateAttached && strings.Contains(id, ".bootvolume.") { out[id] = deref(att.GetInstanceId()) } } if resp.OpcNextPage == nil { return } page = resp.OpcNextPage } } // collectBootVolumeAttachments 把一个 AD + compartment 下 ATTACHED 状态的挂载写入映射。 func collectBootVolumeAttachments(ctx context.Context, cc core.ComputeClient, ad string, compartmentID *string, out map[string]string) { var page *string for { resp, err := cc.ListBootVolumeAttachments(ctx, core.ListBootVolumeAttachmentsRequest{ AvailabilityDomain: &ad, CompartmentId: compartmentID, Page: page, }) if err != nil { return } for _, att := range resp.Items { if att.LifecycleState == core.BootVolumeAttachmentLifecycleStateAttached { out[deref(att.BootVolumeId)] = deref(att.InstanceId) } } if resp.OpcNextPage == nil { return } page = resp.OpcNextPage } } // GetBootVolume 实现 Client:查询引导卷并补全挂载的实例。 func (c *RealClient) GetBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) (BootVolume, error) { bc, err := c.blockstorageClient(cred, region) if err != nil { return BootVolume{}, err } resp, err := bc.GetBootVolume(ctx, core.GetBootVolumeRequest{BootVolumeId: &bootVolumeID}) if err != nil { return BootVolume{}, fmt.Errorf("get boot volume %s: %w", bootVolumeID, err) } volume := toBootVolume(resp.BootVolume) volume.AttachedInstanceID = c.bootVolumeAttachedInstance(ctx, cred, region, volume) if volume.ImageID != "" { if img, err := c.GetImage(ctx, cred, region, volume.ImageID); err == nil { volume.ImageName = img.DisplayName } } return volume, nil } // bootVolumeAttachedInstance 查询引导卷当前挂载的实例;查询失败视为未挂载。 func (c *RealClient) bootVolumeAttachedInstance(ctx context.Context, cred Credentials, region string, volume BootVolume) string { cc, err := c.computeClient(cred, region) if err != nil { return "" } // 挂载记录在引导卷所在 compartment,传租户根会漏掉子 compartment 的挂载 resp, err := cc.ListBootVolumeAttachments(ctx, core.ListBootVolumeAttachmentsRequest{ AvailabilityDomain: &volume.AvailabilityDomain, CompartmentId: hostCompartment(cred, volume.CompartmentID), BootVolumeId: &volume.ID, }) if err != nil { return "" } for _, att := range resp.Items { if att.LifecycleState == core.BootVolumeAttachmentLifecycleStateAttached { return deref(att.InstanceId) } } return "" } // UpdateBootVolume 实现 Client。 func (c *RealClient) UpdateBootVolume(ctx context.Context, cred Credentials, bootVolumeID string, in UpdateBootVolumeInput) (BootVolume, error) { bc, err := c.blockstorageClient(cred, in.Region) if err != nil { return BootVolume{}, err } details := core.UpdateBootVolumeDetails{} if in.DisplayName != "" { details.DisplayName = &in.DisplayName } if in.SizeInGBs > 0 { details.SizeInGBs = &in.SizeInGBs } if in.VpusPerGB > 0 { details.VpusPerGB = &in.VpusPerGB } resp, err := bc.UpdateBootVolume(ctx, core.UpdateBootVolumeRequest{ BootVolumeId: &bootVolumeID, UpdateBootVolumeDetails: details, }) if err != nil { return BootVolume{}, fmt.Errorf("update boot volume %s: %w", bootVolumeID, err) } return toBootVolume(resp.BootVolume), nil } // DeleteBootVolume 实现 Client;仅未挂载(detached)的引导卷可删除。 func (c *RealClient) DeleteBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) error { bc, err := c.blockstorageClient(cred, region) if err != nil { return err } if _, err := bc.DeleteBootVolume(ctx, core.DeleteBootVolumeRequest{BootVolumeId: &bootVolumeID}); err != nil { return fmt.Errorf("delete boot volume %s: %w", bootVolumeID, err) } return nil } func toBootVolume(v core.BootVolume) BootVolume { out := BootVolume{ ID: deref(v.Id), DisplayName: deref(v.DisplayName), LifecycleState: string(v.LifecycleState), AvailabilityDomain: deref(v.AvailabilityDomain), CompartmentID: deref(v.CompartmentId), ImageID: deref(v.ImageId), TimeCreated: sdkTime(v.TimeCreated), } if v.SizeInGBs != nil { out.SizeInGBs = *v.SizeInGBs } if v.VpusPerGB != nil { out.VpusPerGB = *v.VpusPerGB } return out }