package oci import ( "context" "encoding/base64" "fmt" "strings" "sync" "time" "github.com/oracle/oci-go-sdk/v65/core" "github.com/oracle/oci-go-sdk/v65/identity" ) // Instance 是一个计算实例;IP 字段来自 VNIC 补全查询。 type Instance struct { ID string `json:"id"` DisplayName string `json:"displayName"` LifecycleState string `json:"lifecycleState"` Shape string `json:"shape"` Ocpus float32 `json:"ocpus"` MemoryInGBs float32 `json:"memoryInGBs"` AvailabilityDomain string `json:"availabilityDomain"` Region string `json:"region"` CompartmentID string `json:"compartmentId"` ImageID string `json:"imageId"` SubnetID string `json:"subnetId,omitempty"` PrivateIP string `json:"privateIp,omitempty"` PublicIP string `json:"publicIp,omitempty"` Ipv6Addresses []string `json:"ipv6Addresses,omitempty"` FreeformTags map[string]string `json:"freeformTags,omitempty"` DefinedTags map[string]map[string]any `json:"definedTags,omitempty"` TimeCreated *time.Time `json:"timeCreated"` } // CreateInstanceInput 是创建实例的输入:镜像与引导卷二选一作为启动源, // Flex shape 必须提供 ocpus 与 memoryInGBs。 type CreateInstanceInput struct { Region string CompartmentID string // 目标 compartment,空为租户根 AvailabilityDomain string DisplayName string Shape string Ocpus float32 MemoryInGBs float32 ImageID string BootVolumeID string BootVolumeSizeGBs int64 BootVolumeVpusPerGB int64 // 引导卷性能,10 均衡 / 20 高性能,仅镜像启动源生效 SubnetID string // 为空时自动创建 VCN 与子网 AssignPublicIP bool ReservedPublicIPID string // 非空时不分配临时公网 IP,实例就绪后由 service 层绑定该保留 IP AssignIpv6 bool SSHPublicKey string // 与 RootPassword 互斥 RootPassword string // 与 SSHPublicKey 互斥,非空时生成开启 root 密码登录的 cloud-init GenerateRootPassword bool // 由服务端生成随机 root 密码并写入 FreeformTags["RootPassword"] UserData string // 显式 cloud-init,base64 编码;RootPassword 非空时被其生成脚本覆盖 FreeformTags map[string]string // 实例自由标签 } // UpdateInstanceInput 是更新实例的输入;零值字段不更新。 type UpdateInstanceInput struct { Region string DisplayName string Shape string // 更换 shape;运行中实例 OCI 会自动重启,换 Flex 时须同时给 ocpus Ocpus float32 MemoryInGBs float32 } // instanceActions 是允许的电源操作集合。 var instanceActions = map[string]core.InstanceActionActionEnum{ "START": core.InstanceActionActionStart, "STOP": core.InstanceActionActionStop, "RESET": core.InstanceActionActionReset, "SOFTSTOP": core.InstanceActionActionSoftstop, "SOFTRESET": core.InstanceActionActionSoftreset, } func (c *RealClient) computeClient(cred Credentials, region string) (core.ComputeClient, error) { cc, err := core.NewComputeClientWithConfigurationProvider(provider(cred)) if err != nil { return cc, fmt.Errorf("new compute client: %w", err) } applyProxy(&cc.BaseClient, cred) if region != "" { cc.SetRegion(normalizeRegion(region)) } return cc, nil } // ListAvailabilityDomains 实现 Client:列出区域内的可用域名称。 func (c *RealClient) ListAvailabilityDomains(ctx context.Context, cred Credentials, region string) ([]string, error) { ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred)) if err != nil { return nil, fmt.Errorf("new identity client: %w", err) } applyProxy(&ic.BaseClient, cred) if region != "" { ic.SetRegion(normalizeRegion(region)) } resp, err := ic.ListAvailabilityDomains(ctx, identity.ListAvailabilityDomainsRequest{ CompartmentId: &cred.TenancyOCID, }) if err != nil { return nil, fmt.Errorf("list availability domains: %w", err) } names := make([]string, 0, len(resp.Items)) for _, ad := range resp.Items { names = append(names, deref(ad.Name)) } return names, nil } // defaultAvailabilityDomain 返回区域内第一个可用域(即 AD-1)。 func (c *RealClient) defaultAvailabilityDomain(ctx context.Context, cred Credentials, region string) (string, error) { ads, err := c.ListAvailabilityDomains(ctx, cred, region) if err != nil { return "", fmt.Errorf("resolve default availability domain: %w", err) } if len(ads) == 0 { return "", fmt.Errorf("resolve default availability domain: region has no availability domain") } return ads[0], nil } // ListInstances 实现 Client:列出实例并补全网络 IP 信息。 func (c *RealClient) ListInstances(ctx context.Context, cred Credentials, region string) ([]Instance, error) { cc, err := c.computeClient(cred, region) if err != nil { return nil, err } var instances []Instance var page *string for { resp, err := cc.ListInstances(ctx, core.ListInstancesRequest{ CompartmentId: cred.EffectiveCompartment(), Page: page, }) if err != nil { return nil, fmt.Errorf("list instances: %w", err) } for _, item := range resp.Items { // 已终止实例的 VNIC 已不存在,保留在列表中仅供展示 instances = append(instances, toInstance(item)) } if resp.OpcNextPage == nil { break } page = resp.OpcNextPage } c.attachInstanceIPs(ctx, cred, region, instances) // 列表瘦身:标签体积大且列表页不消费(详情接口仍完整返回) for i := range instances { instances[i].FreeformTags = nil instances[i].DefinedTags = nil } return orEmpty(instances), nil } // LaunchInstance 实现 Client;availabilityDomain 为空时默认使用第一个 // 可用域(ad-1),subnetId 为空时自动准备默认网络。 func (c *RealClient) LaunchInstance(ctx context.Context, cred Credentials, in CreateInstanceInput) (Instance, error) { cc, err := c.computeClient(cred, in.Region) if err != nil { return Instance{}, err } if in.AvailabilityDomain == "" { ad, err := c.defaultAvailabilityDomain(ctx, cred, in.Region) if err != nil { return Instance{}, err } in.AvailabilityDomain = ad } if in.SubnetID == "" { subnetID, err := c.ensureDefaultSubnet(ctx, cred, in.Region) if err != nil { return Instance{}, fmt.Errorf("auto provision subnet: %w", err) } in.SubnetID = subnetID } details, err := buildLaunchDetails(*cred.EffectiveCompartment(), in) if err != nil { return Instance{}, err } resp, err := cc.LaunchInstance(ctx, core.LaunchInstanceRequest{LaunchInstanceDetails: details}) if err != nil { return Instance{}, fmt.Errorf("launch instance: %w", err) } return toInstance(resp.Instance), nil } func buildLaunchDetails(compartmentID string, in CreateInstanceInput) (core.LaunchInstanceDetails, error) { // 保留 IP 不能在 launch 时直接绑定:先不分配临时公网 IP,就绪后由 service 层换绑 if in.ReservedPublicIPID != "" { in.AssignPublicIP = false } details := core.LaunchInstanceDetails{ CompartmentId: &compartmentID, AvailabilityDomain: &in.AvailabilityDomain, Shape: &in.Shape, DisplayName: &in.DisplayName, SourceDetails: buildInstanceSource(in), CreateVnicDetails: &core.CreateVnicDetails{ SubnetId: &in.SubnetID, AssignPublicIp: &in.AssignPublicIP, }, } if details.SourceDetails == nil { return details, fmt.Errorf("launch instance: imageId or bootVolumeId is required") } if in.AssignIpv6 { details.CreateVnicDetails.AssignIpv6Ip = &in.AssignIpv6 } if in.Ocpus > 0 { details.ShapeConfig = &core.LaunchInstanceShapeConfigDetails{Ocpus: &in.Ocpus} if in.MemoryInGBs > 0 { details.ShapeConfig.MemoryInGBs = &in.MemoryInGBs } } if len(in.FreeformTags) > 0 { details.FreeformTags = in.FreeformTags } details.Metadata = buildInstanceMetadata(in) return details, nil } func buildInstanceSource(in CreateInstanceInput) core.InstanceSourceDetails { if in.BootVolumeID != "" { return core.InstanceSourceViaBootVolumeDetails{BootVolumeId: &in.BootVolumeID} } if in.ImageID != "" { src := core.InstanceSourceViaImageDetails{ImageId: &in.ImageID} if in.BootVolumeSizeGBs > 0 { src.BootVolumeSizeInGBs = &in.BootVolumeSizeGBs } if in.BootVolumeVpusPerGB > 0 { src.BootVolumeVpusPerGB = &in.BootVolumeVpusPerGB } return src } return nil } func buildInstanceMetadata(in CreateInstanceInput) map[string]string { metadata := make(map[string]string, 2) if in.SSHPublicKey != "" { metadata["ssh_authorized_keys"] = in.SSHPublicKey } // RootPassword 生成的脚本优先作为 user_data,其次才是显式 UserData if in.RootPassword != "" { metadata["user_data"] = rootPasswordUserData(in.RootPassword) } else if in.UserData != "" { metadata["user_data"] = in.UserData } if len(metadata) == 0 { return nil } return metadata } // rootPasswordUserData 生成开启 root 密码登录的 cloud-init 脚本并 base64 编码。 // 密码用单引号安全转义,避免 shell 注入。 func rootPasswordUserData(password string) string { script := "#!/bin/bash\n" + "echo root:" + shellSingleQuote(password) + " | chpasswd\n" + "sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin yes/g' /etc/ssh/sshd_config\n" + "sed -i 's/^#\\?PasswordAuthentication.*/PasswordAuthentication yes/g' /etc/ssh/sshd_config\n" + "rm -f /etc/ssh/sshd_config.d/* /etc/ssh/ssh_config.d/*\n" + "systemctl restart sshd\n" return base64.StdEncoding.EncodeToString([]byte(script)) } // shellSingleQuote 用单引号包裹字符串,内部单引号转义为 '\”。 func shellSingleQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } // GetInstance 实现 Client:查询实例并补全网络 IP 信息。 func (c *RealClient) GetInstance(ctx context.Context, cred Credentials, region, instanceID string) (Instance, error) { cc, err := c.computeClient(cred, region) if err != nil { return Instance{}, err } resp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID}) if err != nil { return Instance{}, fmt.Errorf("get instance %s: %w", instanceID, err) } instances := []Instance{toInstance(resp.Instance)} c.attachInstanceIPs(ctx, cred, region, instances) return instances[0], nil } // UpdateInstance 实现 Client:改名或调整 Flex 规格。 func (c *RealClient) UpdateInstance(ctx context.Context, cred Credentials, instanceID string, in UpdateInstanceInput) (Instance, error) { cc, err := c.computeClient(cred, in.Region) if err != nil { return Instance{}, err } details := core.UpdateInstanceDetails{} if in.DisplayName != "" { details.DisplayName = &in.DisplayName } if in.Shape != "" { details.Shape = &in.Shape } if in.Ocpus > 0 { details.ShapeConfig = &core.UpdateInstanceShapeConfigDetails{Ocpus: &in.Ocpus} if in.MemoryInGBs > 0 { details.ShapeConfig.MemoryInGBs = &in.MemoryInGBs } } resp, err := cc.UpdateInstance(ctx, core.UpdateInstanceRequest{ InstanceId: &instanceID, UpdateInstanceDetails: details, }) if err != nil { return Instance{}, fmt.Errorf("update instance %s: %w", instanceID, err) } return toInstance(resp.Instance), nil } // TerminateInstance 实现 Client;preserveBootVolume 为 true 时保留引导卷。 func (c *RealClient) TerminateInstance(ctx context.Context, cred Credentials, region, instanceID string, preserveBootVolume bool) error { cc, err := c.computeClient(cred, region) if err != nil { return err } _, err = cc.TerminateInstance(ctx, core.TerminateInstanceRequest{ InstanceId: &instanceID, PreserveBootVolume: &preserveBootVolume, }) if err != nil { return fmt.Errorf("terminate instance %s: %w", instanceID, err) } return nil } // InstanceAction 实现 Client:执行电源操作。 func (c *RealClient) InstanceAction(ctx context.Context, cred Credentials, region, instanceID, action string) (Instance, error) { actionEnum, ok := instanceActions[strings.ToUpper(strings.TrimSpace(action))] if !ok { return Instance{}, fmt.Errorf("instance action: unsupported action %q", action) } cc, err := c.computeClient(cred, region) if err != nil { return Instance{}, err } resp, err := cc.InstanceAction(ctx, core.InstanceActionRequest{ InstanceId: &instanceID, Action: actionEnum, }) if err != nil { return Instance{}, fmt.Errorf("instance action %s on %s: %w", action, instanceID, err) } return toInstance(resp.Instance), nil } // hostCompartment 返回宿主资源自身所在 compartment 的请求指针:OCI 的 // 关联查询要求 compartmentId 与资源实际所在 compartment 一致,传租户根 // 会漏掉子 compartment 里的资源;id 缺失时保底用生效 compartment。 func hostCompartment(cred Credentials, id string) *string { if id != "" { return &id } return cred.EffectiveCompartment() } // attachInstanceIPs 批量补全实例的子网与 IP:按实例所在 compartment 分组 // 列出 VNIC 挂载,再逐 VNIC 查询地址;直接用实例自带的 compartment, // 避免为定位挂载再逐台调用 GetInstance。 func (c *RealClient) attachInstanceIPs(ctx context.Context, cred Credentials, region string, instances []Instance) { active := make(map[string]*Instance, len(instances)) compartments := make(map[string]struct{}) for i := range instances { if instances[i].LifecycleState == string(core.InstanceLifecycleStateTerminated) { continue } active[instances[i].ID] = &instances[i] compartments[deref(hostCompartment(cred, instances[i].CompartmentID))] = struct{}{} } if len(active) == 0 { return } cc, err := c.computeClient(cred, region) if err != nil { return } vn, err := c.vcnClient(cred, region) if err != nil { return } for compartmentID := range compartments { fillInstanceIPs(ctx, cc, vn, compartmentID, active) } } // fillInstanceIPs 列出单个 compartment 内的 VNIC 挂载,为命中的实例补全地址。 // 逐 VNIC 的 GetVnic 是列表接口的主要耗时点,限并发 8 路并行拉取。 func fillInstanceIPs(ctx context.Context, cc core.ComputeClient, vn core.VirtualNetworkClient, compartmentID string, active map[string]*Instance) { attResp, err := cc.ListVnicAttachments(ctx, core.ListVnicAttachmentsRequest{CompartmentId: &compartmentID}) if err != nil { return } var ( wg sync.WaitGroup mu sync.Mutex sem = make(chan struct{}, 8) primarySeen = make(map[*Instance]bool) ) for _, att := range attResp.Items { inst, ok := active[deref(att.InstanceId)] if !ok || att.VnicId == nil { continue } wg.Add(1) sem <- struct{}{} go func(vnicID *string, inst *Instance) { defer wg.Done() defer func() { <-sem }() vnicResp, err := vn.GetVnic(ctx, core.GetVnicRequest{VnicId: vnicID}) if err != nil { return } mu.Lock() applyVnicAddrs(inst, vnicResp.Vnic, primarySeen) mu.Unlock() }(att.VnicId, inst) } wg.Wait() } // applyVnicAddrs 将 VNIC 地址写入实例。多网卡实例以主网卡为准: // 主网卡返回前先用先到的网卡兜底,主网卡到达后覆盖并锁定,避免并发 // 完成顺序决定展示结果。调用方需持有保护 inst 与 primarySeen 的锁。 func applyVnicAddrs(inst *Instance, v core.Vnic, primarySeen map[*Instance]bool) { isPrimary := v.IsPrimary != nil && *v.IsPrimary if !isPrimary && (primarySeen[inst] || inst.SubnetID != "") { return } inst.SubnetID = deref(v.SubnetId) inst.PrivateIP = deref(v.PrivateIp) inst.PublicIP = deref(v.PublicIp) inst.Ipv6Addresses = v.Ipv6Addresses if isPrimary { primarySeen[inst] = true } } func toInstance(inst core.Instance) Instance { out := Instance{ ID: deref(inst.Id), DisplayName: deref(inst.DisplayName), LifecycleState: string(inst.LifecycleState), Shape: deref(inst.Shape), AvailabilityDomain: deref(inst.AvailabilityDomain), Region: deref(inst.Region), CompartmentID: deref(inst.CompartmentId), ImageID: deref(inst.ImageId), FreeformTags: inst.FreeformTags, DefinedTags: inst.DefinedTags, TimeCreated: sdkTime(inst.TimeCreated), } if inst.ShapeConfig != nil { if inst.ShapeConfig.Ocpus != nil { out.Ocpus = *inst.ShapeConfig.Ocpus } if inst.ShapeConfig.MemoryInGBs != nil { out.MemoryInGBs = *inst.ShapeConfig.MemoryInGBs } } return out }