197 lines
7.1 KiB
Go
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)
|
|
}
|