// Package oci 封装 Oracle Cloud Infrastructure 官方 Go SDK, // 对上层提供统一的客户端抽象;真实云请求只允许出现在本包。 package oci import ( "context" "errors" "fmt" "strings" "sync" "time" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/identity" "oci-portal/internal/aiwire" ) // cloudcmServiceName 是 OCI 云消费(Cloud Consumption Model)订阅的服务名, // 账户类别信息挂在该订阅之下。 const cloudcmServiceName = "CLOUDCM" // ErrNoCloudcmSubscription 表示租户下没有 serviceName=CLOUDCM 的订阅。 var ErrNoCloudcmSubscription = errors.New("no CLOUDCM subscription found") // TenancyInfo 是测活成功后取得的租户基本信息。 type TenancyInfo struct { Name string HomeRegionKey string } // Client 是真实 OCI 调用的统一入口,service 层只依赖此接口。 type Client interface { // ValidateKey 用 Identity GetTenancy 验证凭据有效性(测活)。 ValidateKey(ctx context.Context, cred Credentials) (TenancyInfo, error) // FetchAccountProfile 查询 CLOUDCM 订阅并判定账户类别。 FetchAccountProfile(ctx context.Context, cred Credentials) (AccountProfile, error) // ListCompartments 递归列出租户下全部 ACTIVE compartment(不含租户根)。 ListCompartments(ctx context.Context, cred Credentials) ([]Compartment, error) // ListRegionSubscriptions 列出租户已订阅的区域。 ListRegionSubscriptions(ctx context.Context, cred Credentials) ([]RegionSubscription, error) // SubscribeRegion 为租户订阅新区域;请求发往 homeRegion,操作不可撤销。 SubscribeRegion(ctx context.Context, cred Credentials, homeRegion, regionKey string) error // ListLimits 查询服务配额,name 为大小写不敏感的模糊过滤,按需附带用量。 ListLimits(ctx context.Context, cred Credentials, q LimitsQuery) ([]LimitValue, error) // ListLimitServices 列出配额体系支持的全部服务。 ListLimitServices(ctx context.Context, cred Credentials, region string) ([]LimitService, error) // ListSubscriptions 列出租户全部订阅摘要。 ListSubscriptions(ctx context.Context, cred Credentials) ([]SubscriptionInfo, error) // GetSubscription 查询单个订阅的完整信息。 GetSubscription(ctx context.Context, cred Credentials, subscriptionID string) (SubscriptionDetail, error) // ListShapes 列出租户在目标区域可用的实例规格(带缓存)。 ListShapes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]ComputeShape, error) // ListImages 列出可用的实例镜像。 ListImages(ctx context.Context, cred Credentials, q ImagesQuery) ([]Image, error) // GetImage 查询单个镜像。 GetImage(ctx context.Context, cred Credentials, region, imageID string) (Image, error) // VCN 增删改查与一键 IPv6。 ListVCNs(ctx context.Context, cred Credentials, region string) ([]VCN, error) CreateVCN(ctx context.Context, cred Credentials, in CreateVCNInput) (VCN, error) GetVCN(ctx context.Context, cred Credentials, region, vcnID string) (VCN, error) UpdateVCN(ctx context.Context, cred Credentials, region, vcnID, displayName string) (VCN, error) DeleteVCN(ctx context.Context, cred Credentials, region, vcnID string) error EnableVCNIPv6(ctx context.Context, cred Credentials, region, vcnID string) ([]IPv6Step, error) // 子网增删改查。 ListSubnets(ctx context.Context, cred Credentials, region, vcnID string) ([]Subnet, error) CreateSubnet(ctx context.Context, cred Credentials, in CreateSubnetInput) (Subnet, error) GetSubnet(ctx context.Context, cred Credentials, region, subnetID string) (Subnet, error) UpdateSubnet(ctx context.Context, cred Credentials, region, subnetID, displayName string) (Subnet, error) DeleteSubnet(ctx context.Context, cred Credentials, region, subnetID string) error // 安全列表增删改查。 ListSecurityLists(ctx context.Context, cred Credentials, region, vcnID string) ([]SecurityList, error) CreateSecurityList(ctx context.Context, cred Credentials, in CreateSecurityListInput) (SecurityList, error) GetSecurityList(ctx context.Context, cred Credentials, region, securityListID string) (SecurityList, error) UpdateSecurityList(ctx context.Context, cred Credentials, securityListID string, in UpdateSecurityListInput) (SecurityList, error) DeleteSecurityList(ctx context.Context, cred Credentials, region, securityListID string) error // ListAvailabilityDomains 列出区域内可用域,创建实例的前置查询。 ListAvailabilityDomains(ctx context.Context, cred Credentials, region string) ([]string, error) // 实例增删改查与电源操作。 ListInstances(ctx context.Context, cred Credentials, region string) ([]Instance, error) LaunchInstance(ctx context.Context, cred Credentials, in CreateInstanceInput) (Instance, error) GetInstance(ctx context.Context, cred Credentials, region, instanceID string) (Instance, error) UpdateInstance(ctx context.Context, cred Credentials, instanceID string, in UpdateInstanceInput) (Instance, error) TerminateInstance(ctx context.Context, cred Credentials, region, instanceID string, preserveBootVolume bool) error InstanceAction(ctx context.Context, cred Credentials, region, instanceID, action string) (Instance, error) // 引导卷查删改;引导卷由创建实例产生,不单独创建。 ListBootVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BootVolume, error) GetBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) (BootVolume, error) UpdateBootVolume(ctx context.Context, cred Credentials, bootVolumeID string, in UpdateBootVolumeInput) (BootVolume, error) DeleteBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) error // 实例 IP:更换主 VNIC 临时公网 IP、添加与取消分配 IPv6 地址。 ChangeInstancePublicIP(ctx context.Context, cred Credentials, region, instanceID string) (string, error) AddInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) (string, error) DeleteInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) error // VNIC 管理:列出实例网卡、附加次要网卡、分离(主网卡由 OCI 拒绝)、按网卡加 IPv6。 ListInstanceVnics(ctx context.Context, cred Credentials, region, instanceID string) ([]Vnic, error) AttachVnic(ctx context.Context, cred Credentials, region, instanceID string, in AttachVnicInput) (Vnic, error) DetachVnic(ctx context.Context, cred Credentials, region, attachmentID string) error AddVnicIpv6(ctx context.Context, cred Credentials, region, vnicID, address string) (string, error) // GenAI 网关:区域模型列表、聊天(非流式/流式)、配额探测、文本向量化。 ListGenAiModels(ctx context.Context, cred Credentials, region string) ([]GenAiModel, error) GenAiChat(ctx context.Context, cred Credentials, region, modelOcid string, ir aiwire.ChatRequest) (*aiwire.ChatResponse, error) GenAiChatStream(ctx context.Context, cred Credentials, region, modelOcid string, ir aiwire.ChatRequest) (GenAiStream, error) GenAiProbeChat(ctx context.Context, cred Credentials, region, modelOcid, modelName string) (int, error) GenAiEmbed(ctx context.Context, cred Credentials, region, modelOcid string, inputs []string, dimensions *int) ([][]float32, *aiwire.Usage, error) // 控制台连接:创建(VNC/串口连接串)、列出、删除。 CreateConsoleConnection(ctx context.Context, cred Credentials, region, instanceID, sshPublicKey string) (ConsoleConnection, error) ListConsoleConnections(ctx context.Context, cred Credentials, region, instanceID string) ([]ConsoleConnection, error) DeleteConsoleConnection(ctx context.Context, cred Credentials, region, connectionID string) error // 引导卷挂载:列出、挂载、分离、替换。 ListBootVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]BootVolumeAttachment, error) AttachBootVolume(ctx context.Context, cred Credentials, region, instanceID, bootVolumeID string) (BootVolumeAttachment, error) DetachBootVolume(ctx context.Context, cred Credentials, region, attachmentID string) error ReplaceBootVolume(ctx context.Context, cred Credentials, region, instanceID, newBootVolumeID string) (BootVolumeAttachment, error) // 块卷挂载:列出块卷、列出挂载、附加、分离。 ListBlockVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BlockVolume, error) ListVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]VolumeAttachment, error) AttachVolume(ctx context.Context, cred Credentials, region, instanceID, volumeID string, readOnly bool) (VolumeAttachment, error) DetachVolume(ctx context.Context, cred Credentials, region, attachmentID string) error // 租户观测:实例流量统计、成本分析。 SummarizeInstanceTraffic(ctx context.Context, cred Credentials, q TrafficQuery) (InstanceTraffic, error) SummarizeCosts(ctx context.Context, cred Credentials, q CostQuery) ([]CostItem, error) // ListAuditEvents 实时查询租户根 compartment 的审计事件(最多翻 maxAuditPages 页, // 到限截断并回传续查游标);page 传上次结果的 NextPage 可从断点续翻。 ListAuditEvents(ctx context.Context, cred Credentials, region string, start, end time.Time, page string) (AuditEventsResult, error) // 租户用户管理:经典 IAM 为主,新式租户自动回退 Identity Domains。 ListTenantUsers(ctx context.Context, cred Credentials) ([]TenantUser, error) GetTenantUserDetail(ctx context.Context, cred Credentials, homeRegion, userID string) (TenantUserDetail, error) CreateTenantUser(ctx context.Context, cred Credentials, homeRegion string, in CreateTenantUserInput) (TenantUser, error) UpdateTenantUser(ctx context.Context, cred Credentials, homeRegion, userID string, in UpdateTenantUserInput) (TenantUser, error) DeleteTenantUser(ctx context.Context, cred Credentials, homeRegion, userID string) error ResetTenantUserPassword(ctx context.Context, cred Credentials, homeRegion, userID string) (string, error) DeleteTenantUserMfaDevices(ctx context.Context, cred Credentials, homeRegion, userID string) (int, error) DeleteTenantUserApiKeys(ctx context.Context, cred Credentials, homeRegion, userID string, includeCurrent bool) (int, error) // 域设置:通知收件人、密码策略、身份设置。 GetNotificationRecipients(ctx context.Context, cred Credentials, region string) (NotificationRecipients, error) UpdateNotificationRecipients(ctx context.Context, cred Credentials, region string, emails []string) (NotificationRecipients, error) ListPasswordPolicies(ctx context.Context, cred Credentials, region string) ([]PasswordPolicyInfo, error) UpdatePasswordPolicy(ctx context.Context, cred Credentials, region, policyID string, in UpdatePasswordPolicyInput) (PasswordPolicyInfo, error) GetIdentitySetting(ctx context.Context, cred Credentials, region string) (IdentitySettingInfo, error) UpdateIdentitySetting(ctx context.Context, cred Credentials, region string, primaryEmailRequired bool) (IdentitySettingInfo, error) // Federation:SAML IdP 管理、域 SP 元数据、sign-on 免 MFA 规则。 ListIdentityProviders(ctx context.Context, cred Credentials, region string) ([]IdentityProviderInfo, error) CreateSamlIdentityProvider(ctx context.Context, cred Credentials, region string, in CreateIdpInput) (IdentityProviderInfo, error) SetIdentityProviderEnabled(ctx context.Context, cred Credentials, region, idpID string, enabled bool) (IdentityProviderInfo, error) DeleteIdentityProvider(ctx context.Context, cred Credentials, region, idpID string) error DownloadDomainSamlMetadata(ctx context.Context, cred Credentials, region string) ([]byte, error) ListConsoleSignOnRules(ctx context.Context, cred Credentials, region string) ([]SignOnRuleInfo, error) CreateMfaExemptionRule(ctx context.Context, cred Credentials, region, idpID, ruleName string) (SignOnRuleInfo, error) DeleteMfaExemptionRule(ctx context.Context, cred Credentials, region, ruleID string) error // 日志回传链路(方案A):Topic/CUSTOM_HTTPS 订阅/IAM Policy/Service Connector // 的幂等创建、状态聚合与销毁;IAM 写操作发往 homeRegion。 EnsureRelayTopic(ctx context.Context, cred Credentials) (RelayResource, error) EnsureRelaySubscription(ctx context.Context, cred Credentials, topicID, endpoint string) (RelayResource, error) GetRelaySubscription(ctx context.Context, cred Credentials, subscriptionID string) (RelayResource, error) EnsureRelayPolicy(ctx context.Context, cred Credentials, homeRegion string) (RelayResource, error) EnsureRelayConnector(ctx context.Context, cred Credentials, topicID, condition string) (RelayResource, error) RelayState(ctx context.Context, cred Credentials, endpoint string) (RelayState, error) DeleteRelayConnector(ctx context.Context, cred Credentials, connectorID string) error DeleteRelayPolicy(ctx context.Context, cred Credentials, homeRegion, policyID string) error DeleteRelaySubscription(ctx context.Context, cred Credentials, subscriptionID string) error DeleteRelayTopic(ctx context.Context, cred Credentials, topicID string) error } // RealClient 基于官方 SDK 实现 Client。 type RealClient struct { limitDefs sync.Map // tenancy|region|service → limitDefEntry,配额定义预筛缓存 shapes sync.Map // tenancy|region|ad → shapeCacheEntry,shape 清单缓存 } // NewClient 返回生产使用的真实客户端。 func NewClient() *RealClient { return &RealClient{} } func provider(cred Credentials) common.ConfigurationProvider { var pass *string if cred.Passphrase != "" { pass = &cred.Passphrase } return common.NewRawConfigurationProvider( cred.TenancyOCID, cred.UserOCID, cred.Region, cred.Fingerprint, cred.PrivateKey, pass, ) } // normalizeRegion 兼容区域短名:iad → us-ashburn-1;空值与全名原样返回。 // 实例等资源对象上的 region 字段是短名,直接透传时借此归一。 func normalizeRegion(region string) string { if region == "" { return "" } return string(common.StringToRegion(region)) } // ValidateKey 实现 Client。 func (c *RealClient) ValidateKey(ctx context.Context, cred Credentials) (TenancyInfo, error) { ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred)) if err != nil { return TenancyInfo{}, fmt.Errorf("new identity client: %w", err) } applyProxy(&ic.BaseClient, cred) resp, err := ic.GetTenancy(ctx, identity.GetTenancyRequest{TenancyId: &cred.TenancyOCID}) if err != nil { return TenancyInfo{}, fmt.Errorf("get tenancy: %w", err) } return TenancyInfo{ Name: deref(resp.Name), HomeRegionKey: deref(resp.HomeRegionKey), }, nil } // FetchAccountProfile 实现 Client:从订阅列表选中 CLOUDCM, // 再查询完整订阅并按付费模式、促销状态判定账户类别。 func (c *RealClient) FetchAccountProfile(ctx context.Context, cred Credentials) (AccountProfile, error) { subs, err := c.ListSubscriptions(ctx, cred) if err != nil { return AccountProfile{}, err } subID := "" for _, sub := range subs { if strings.EqualFold(sub.ServiceName, cloudcmServiceName) { subID = sub.ID break } } if subID == "" { return AccountProfile{}, ErrNoCloudcmSubscription } detail, err := c.GetSubscription(ctx, cred, subID) if err != nil { return AccountProfile{}, err } return buildAccountProfile(detail), nil } func buildAccountProfile(detail SubscriptionDetail) AccountProfile { p := AccountProfile{ SubscriptionID: detail.ID, ServiceName: detail.ServiceName, PaymentModel: detail.PaymentModel, SubscriptionTier: detail.SubscriptionTier, } p.PromotionStatus, p.PromotionAmount, p.PromotionExpires = pickPromotion(detail.Promotions) p.AccountType = ClassifyAccount(p.PaymentModel, p.SubscriptionTier, p.PromotionStatus) return p } // pickPromotion 从多条促销记录中选出最能代表当前状态的一条: // 优先 ACTIVE,其次 EXPIRED,否则回退第一条。 func pickPromotion(promos []PromotionInfo) (string, float32, *time.Time) { best, bestRank := -1, -1 for i, p := range promos { if rank := promotionRank(p.Status); rank > bestRank { best, bestRank = i, rank } } if best < 0 { return "", 0, nil } p := promos[best] return p.Status, p.Amount, p.TimeExpired } func promotionRank(status string) int { switch strings.ToUpper(status) { case "ACTIVE": return 2 case "EXPIRED": return 1 default: return 0 } } func deref(s *string) string { if s != nil { return *s } return "" } // orEmpty 把 nil 切片归一为空切片:JSON 序列化输出 [] 而非 null, // 避免前端对 null 调用 .length / .map / .join 崩溃。 func orEmpty[T any](s []T) []T { if s == nil { return []T{} } return s }