293 lines
9.6 KiB
Go
293 lines
9.6 KiB
Go
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
|
||
}
|