Files
oci-portal/internal/service/instance.go
T

197 lines
7.1 KiB
Go

package service
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"oci-portal/internal/oci"
)
// AvailabilityDomains 列出区域内可用域。
func (s *OciConfigService) AvailabilityDomains(ctx context.Context, id uint, region string) ([]string, error) {
cred, err := s.credentialsByID(ctx, id)
if err != nil {
return nil, err
}
return s.client.ListAvailabilityDomains(ctx, cred, region)
}
// Instances 列出实例(含 IP 补全);compartmentID 为空表示租户根。
func (s *OciConfigService) Instances(ctx context.Context, id uint, region, compartmentID string) ([]oci.Instance, error) {
cred, err := s.credentialsInCompartment(ctx, id, compartmentID)
if err != nil {
return nil, err
}
return s.client.ListInstances(ctx, cred, region)
}
// maxBatchCreate 是单次批量创建实例的数量上限。
const maxBatchCreate = 10
// CreateInstances 批量创建实例,逐台尝试互不影响;count 大于 1 时
// 名称自动追加 -1、-2 等序号。返回成功实例与逐台失败信息。
// in.CompartmentID 为空时建在租户根。
func (s *OciConfigService) CreateInstances(ctx context.Context, id uint, in oci.CreateInstanceInput, count int) ([]oci.Instance, []string, error) {
if count < 1 || count > maxBatchCreate {
return nil, nil, fmt.Errorf("create instances: count must be between 1 and %d", maxBatchCreate)
}
if err := validateCreateInstance(in); err != nil {
return nil, nil, err
}
cred, err := s.credentialsInCompartment(ctx, id, in.CompartmentID)
if err != nil {
return nil, nil, err
}
instances := make([]oci.Instance, 0, count)
var failures []string
for i := 1; i <= count; i++ {
one := in
if count > 1 {
one.DisplayName = fmt.Sprintf("%s-%d", in.DisplayName, i)
}
if err := applyGeneratedRootPassword(&one); err != nil {
return nil, nil, err
}
instance, err := s.client.LaunchInstance(ctx, cred, one)
if err != nil {
failures = append(failures, fmt.Sprintf("%s: %s", one.DisplayName, oci.CompactError(err)))
continue
}
instances = append(instances, instance)
}
return instances, failures, nil
}
// applyGeneratedRootPassword 在开启随机密码时为单台实例生成 root 密码,
// 同时写入 FreeformTags["RootPassword"] 便于面板回显;批量创建时每台独立。
func applyGeneratedRootPassword(one *oci.CreateInstanceInput) error {
if !one.GenerateRootPassword {
return nil
}
pwd, err := randomPassword(16)
if err != nil {
return fmt.Errorf("generate root password: %w", err)
}
one.RootPassword = pwd
tags := make(map[string]string, len(one.FreeformTags)+1)
for k, v := range one.FreeformTags {
tags[k] = v
}
tags["RootPassword"] = pwd
one.FreeformTags = tags
return nil
}
// randomPassword 生成含大小写字母、数字与安全符号的随机密码;
// 字符集避开引号、冒号等会破坏 cloud-init chpasswd 语法的字符。
func randomPassword(length int) (string, error) {
const charset = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789@#%^*-_=+"
out := make([]byte, length)
for i := range out {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
out[i] = charset[n.Int64()]
}
return string(out), nil
}
func validateCreateInstance(in oci.CreateInstanceInput) error {
switch {
case in.DisplayName == "":
return fmt.Errorf("create instance: displayName is required")
case in.Shape == "":
return fmt.Errorf("create instance: shape is required")
case in.ImageID == "" && in.BootVolumeID == "":
return fmt.Errorf("create instance: imageId or bootVolumeId is required")
case in.ImageID != "" && in.BootVolumeID != "":
return fmt.Errorf("create instance: imageId and bootVolumeId are mutually exclusive")
case in.SSHPublicKey != "" && in.RootPassword != "":
return fmt.Errorf("create instance: sshPublicKey and rootPassword are mutually exclusive")
case in.GenerateRootPassword && (in.SSHPublicKey != "" || in.RootPassword != ""):
return fmt.Errorf("create instance: generateRootPassword conflicts with sshPublicKey/rootPassword")
}
return nil
}
// Instance 查询单个实例(含 IP 补全)。
func (s *OciConfigService) Instance(ctx context.Context, id uint, region, instanceID string) (oci.Instance, error) {
cred, err := s.credentialsByID(ctx, id)
if err != nil {
return oci.Instance{}, err
}
return s.client.GetInstance(ctx, cred, region, instanceID)
}
// UpdateInstance 更新实例名称、shape 或 Flex 规格。
func (s *OciConfigService) UpdateInstance(ctx context.Context, id uint, instanceID string, in oci.UpdateInstanceInput) (oci.Instance, error) {
if in.DisplayName == "" && in.Shape == "" && in.Ocpus == 0 {
return oci.Instance{}, fmt.Errorf("update instance: nothing to update")
}
cred, err := s.credentialsByID(ctx, id)
if err != nil {
return oci.Instance{}, err
}
return s.client.UpdateInstance(ctx, cred, instanceID, in)
}
// TerminateInstance 终止实例;preserveBootVolume 为 true 时保留引导卷。
func (s *OciConfigService) TerminateInstance(ctx context.Context, id uint, region, instanceID string, preserveBootVolume bool) error {
cred, err := s.credentialsByID(ctx, id)
if err != nil {
return err
}
return s.client.TerminateInstance(ctx, cred, region, instanceID, preserveBootVolume)
}
// InstanceAction 执行电源操作:START/STOP/RESET/SOFTSTOP/SOFTRESET。
func (s *OciConfigService) InstanceAction(ctx context.Context, id uint, region, instanceID, action string) (oci.Instance, error) {
cred, err := s.credentialsByID(ctx, id)
if err != nil {
return oci.Instance{}, err
}
return s.client.InstanceAction(ctx, cred, region, instanceID, action)
}
// BootVolumes 列出引导卷;compartmentID 为空表示租户根。
func (s *OciConfigService) BootVolumes(ctx context.Context, id uint, region, availabilityDomain, compartmentID string) ([]oci.BootVolume, error) {
cred, err := s.credentialsInCompartment(ctx, id, compartmentID)
if err != nil {
return nil, err
}
return s.client.ListBootVolumes(ctx, cred, region, availabilityDomain)
}
// BootVolume 查询引导卷详情(含挂载实例)。
func (s *OciConfigService) BootVolume(ctx context.Context, id uint, region, bootVolumeID string) (oci.BootVolume, error) {
cred, err := s.credentialsByID(ctx, id)
if err != nil {
return oci.BootVolume{}, err
}
return s.client.GetBootVolume(ctx, cred, region, bootVolumeID)
}
// UpdateBootVolume 更新引导卷名称、容量或性能。
func (s *OciConfigService) UpdateBootVolume(ctx context.Context, id uint, bootVolumeID string, in oci.UpdateBootVolumeInput) (oci.BootVolume, error) {
if in.DisplayName == "" && in.SizeInGBs == 0 && in.VpusPerGB == 0 {
return oci.BootVolume{}, fmt.Errorf("update boot volume: nothing to update")
}
cred, err := s.credentialsByID(ctx, id)
if err != nil {
return oci.BootVolume{}, err
}
return s.client.UpdateBootVolume(ctx, cred, bootVolumeID, in)
}
// DeleteBootVolume 删除引导卷。
func (s *OciConfigService) DeleteBootVolume(ctx context.Context, id uint, region, bootVolumeID string) error {
cred, err := s.credentialsByID(ctx, id)
if err != nil {
return err
}
return s.client.DeleteBootVolume(ctx, cred, region, bootVolumeID)
}