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 in.ReservedPublicIPID != "" && count > 1 { return nil, nil, fmt.Errorf("create instances: reserved public ip only supports single instance") } 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 } if one.ReservedPublicIPID != "" { region, instanceID, ipID := one.Region, instance.ID, one.ReservedPublicIPID s.goBind(func() { s.bindReservedIPWhenReady(cred, region, instanceID, ipID) }) } 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) }