初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user