Files

293 lines
9.6 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"
"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 实现 ClientavailabilityDomain 为空时列出全区域引导卷。
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
}