初始提交:OCI 面板后端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:04 +08:00
commit b9a3e97e84
168 changed files with 31794 additions and 0 deletions
+314
View File
@@ -0,0 +1,314 @@
package oci
import (
"context"
"fmt"
"net/netip"
"time"
"github.com/oracle/oci-go-sdk/v65/core"
)
// ipv6AnyCIDR 是 IPv6 默认路由与放行目标。
const ipv6AnyCIDR = "::/0"
// IPv6Step 是一键启用 IPv6 过程中的一个步骤结果。
type IPv6Step struct {
Step string `json:"step"`
Status string `json:"status"` // done / skipped / failed
Detail string `json:"detail,omitempty"`
}
// EnableVCNIPv6 实现 Client:为 VCN 一键启用 IPv6。依次执行:
// 1. VCN 分配 Oracle GUA /56(已有则跳过);
// 2. 每个无 IPv6 的子网从 /56 中分配一个 /64;
// 3. 默认路由表补 ::/0 → Internet Gateway(无 IGW 时自动创建);
// 4. 默认安全列表补 egress ::/0 放行。
//
// 步骤级失败记录在返回的步骤列表中,不中断后续无依赖的步骤。
func (c *RealClient) EnableVCNIPv6(ctx context.Context, cred Credentials, region, vcnID string) ([]IPv6Step, error) {
vn, err := c.vcnClient(cred, region)
if err != nil {
return nil, err
}
vcnResp, err := vn.GetVcn(ctx, core.GetVcnRequest{VcnId: &vcnID})
if err != nil {
return nil, fmt.Errorf("get vcn %s: %w", vcnID, err)
}
// 子网 / IGW 的关联查询须用 VCN 实际所在 compartment,传租户根会漏查
vcnCompartment := *hostCompartment(cred, deref(vcnResp.CompartmentId))
var steps []IPv6Step
vcnCidr, step := ensureVCNIPv6Cidr(ctx, vn, vcnResp.Vcn)
steps = append(steps, step)
steps = append(steps, ensureSubnetsIPv6(ctx, vn, vcnCompartment, vcnID, vcnCidr)...)
steps = append(steps, ensureIPv6Route(ctx, vn, vcnCompartment, vcnResp.Vcn))
steps = append(steps, ensureIPv6Egress(ctx, vn, vcnResp.Vcn))
return orEmpty(steps), nil
}
// ensureVCNIPv6Cidr 确保 VCN 拥有 IPv6 GUA,返回其 /56 前缀。
func ensureVCNIPv6Cidr(ctx context.Context, vn core.VirtualNetworkClient, vcn core.Vcn) (string, IPv6Step) {
if len(vcn.Ipv6CidrBlocks) > 0 {
return vcn.Ipv6CidrBlocks[0], IPv6Step{Step: "vcn-ipv6-cidr", Status: "skipped", Detail: vcn.Ipv6CidrBlocks[0]}
}
enabled := true
_, err := vn.AddIpv6VcnCidr(ctx, core.AddIpv6VcnCidrRequest{
VcnId: vcn.Id,
AddVcnIpv6CidrDetails: core.AddVcnIpv6CidrDetails{IsOracleGuaAllocationEnabled: &enabled},
})
if err != nil {
return "", IPv6Step{Step: "vcn-ipv6-cidr", Status: "failed", Detail: err.Error()}
}
cidr, err := waitVCNIPv6Cidr(ctx, vn, *vcn.Id)
if err != nil {
return "", IPv6Step{Step: "vcn-ipv6-cidr", Status: "failed", Detail: err.Error()}
}
return cidr, IPv6Step{Step: "vcn-ipv6-cidr", Status: "done", Detail: cidr}
}
// waitVCNIPv6Cidr 轮询等待 Oracle GUA 分配完成(异步操作)。
func waitVCNIPv6Cidr(ctx context.Context, vn core.VirtualNetworkClient, vcnID string) (string, error) {
for i := 0; i < 30; i++ {
resp, err := vn.GetVcn(ctx, core.GetVcnRequest{VcnId: &vcnID})
if err != nil {
return "", fmt.Errorf("poll vcn ipv6 cidr: %w", err)
}
if len(resp.Ipv6CidrBlocks) > 0 {
return resp.Ipv6CidrBlocks[0], nil
}
select {
case <-ctx.Done():
return "", fmt.Errorf("poll vcn ipv6 cidr: %w", ctx.Err())
case <-time.After(2 * time.Second):
}
}
return "", fmt.Errorf("poll vcn ipv6 cidr: timed out")
}
// ensureSubnetsIPv6 为每个还没有 IPv6 的子网从 VCN /56 中分配 /64
// compartmentID 须是 VCN 实际所在 compartment,否则列不到子网。
func ensureSubnetsIPv6(ctx context.Context, vn core.VirtualNetworkClient, compartmentID, vcnID, vcnCidr string) []IPv6Step {
if vcnCidr == "" {
return []IPv6Step{{Step: "subnet-ipv6-cidr", Status: "failed", Detail: "vcn has no ipv6 cidr"}}
}
subnets, err := listSubnets(ctx, vn, compartmentID, vcnID)
if err != nil {
return []IPv6Step{{Step: "subnet-ipv6-cidr", Status: "failed", Detail: err.Error()}}
}
used := make([]string, 0, len(subnets))
for _, s := range subnets {
if s.Ipv6CidrBlock != "" {
used = append(used, s.Ipv6CidrBlock)
}
}
var steps []IPv6Step
for _, s := range subnets {
if s.Ipv6CidrBlock != "" {
steps = append(steps, IPv6Step{Step: "subnet-ipv6-cidr", Status: "skipped", Detail: s.DisplayName + " " + s.Ipv6CidrBlock})
continue
}
steps = append(steps, addSubnetIPv6(ctx, vn, s, vcnCidr, &used))
}
return orEmpty(steps)
}
func addSubnetIPv6(ctx context.Context, vn core.VirtualNetworkClient, s Subnet, vcnCidr string, used *[]string) IPv6Step {
cidr, err := nextSubnetIPv6CIDR(vcnCidr, *used)
if err != nil {
return IPv6Step{Step: "subnet-ipv6-cidr", Status: "failed", Detail: s.DisplayName + ": " + err.Error()}
}
_, err = vn.AddIpv6SubnetCidr(ctx, core.AddIpv6SubnetCidrRequest{
SubnetId: &s.ID,
AddSubnetIpv6CidrDetails: core.AddSubnetIpv6CidrDetails{Ipv6CidrBlock: &cidr},
})
if err != nil {
return IPv6Step{Step: "subnet-ipv6-cidr", Status: "failed", Detail: s.DisplayName + ": " + err.Error()}
}
*used = append(*used, cidr)
return IPv6Step{Step: "subnet-ipv6-cidr", Status: "done", Detail: s.DisplayName + " " + cidr}
}
// nextSubnetIPv6CIDR 从 VCN 的 /56 前缀中选出未被占用的 /64:
// /56 的前 7 字节固定,第 8 字节 0~255 即 256 个可用 /64。
func nextSubnetIPv6CIDR(vcnCidr string, used []string) (string, error) {
prefix, err := netip.ParsePrefix(vcnCidr)
if err != nil {
return "", fmt.Errorf("parse vcn ipv6 cidr %s: %w", vcnCidr, err)
}
base := prefix.Addr().As16()
taken := make(map[byte]bool, len(used))
for _, u := range used {
p, err := netip.ParsePrefix(u)
if err != nil {
continue
}
b := p.Addr().As16()
if [7]byte(b[:7]) == [7]byte(base[:7]) {
taken[b[7]] = true
}
}
for i := 0; i <= 255; i++ {
if taken[byte(i)] {
continue
}
candidate := base
candidate[7] = base[7] | byte(i)
return netip.PrefixFrom(netip.AddrFrom16(candidate), 64).String(), nil
}
return "", fmt.Errorf("no free /64 left in %s", vcnCidr)
}
// ensureIPv6Route 在默认路由表补 ::/0 → IGW;没有 IGW 时自动创建。
// compartmentID 须是 VCN 实际所在 compartment,IGW 查找与创建都在其中进行。
func ensureIPv6Route(ctx context.Context, vn core.VirtualNetworkClient, compartmentID string, vcn core.Vcn) IPv6Step {
rtResp, err := vn.GetRouteTable(ctx, core.GetRouteTableRequest{RtId: vcn.DefaultRouteTableId})
if err != nil {
return IPv6Step{Step: "route-ipv6-default", Status: "failed", Detail: err.Error()}
}
for _, rule := range rtResp.RouteRules {
if deref(rule.Destination) == ipv6AnyCIDR {
return IPv6Step{Step: "route-ipv6-default", Status: "skipped", Detail: "::/0 route exists"}
}
}
igwID, err := ensureInternetGateway(ctx, vn, compartmentID, *vcn.Id)
if err != nil {
return IPv6Step{Step: "route-ipv6-default", Status: "failed", Detail: err.Error()}
}
dest := ipv6AnyCIDR
rules := append(rtResp.RouteRules, core.RouteRule{
Destination: &dest,
DestinationType: core.RouteRuleDestinationTypeCidrBlock,
NetworkEntityId: &igwID,
})
_, err = vn.UpdateRouteTable(ctx, core.UpdateRouteTableRequest{
RtId: vcn.DefaultRouteTableId,
UpdateRouteTableDetails: core.UpdateRouteTableDetails{RouteRules: rules},
})
if err != nil {
return IPv6Step{Step: "route-ipv6-default", Status: "failed", Detail: err.Error()}
}
return IPv6Step{Step: "route-ipv6-default", Status: "done", Detail: "::/0 -> " + igwID}
}
// ensureInternetGateway 返回 VCN 已有的 IGW,没有则创建一个;
// compartmentID 须是 VCN 实际所在 compartment,否则查不到已有 IGW。
func ensureInternetGateway(ctx context.Context, vn core.VirtualNetworkClient, compartmentID, vcnID string) (string, error) {
listResp, err := vn.ListInternetGateways(ctx, core.ListInternetGatewaysRequest{
CompartmentId: &compartmentID,
VcnId: &vcnID,
})
if err != nil {
return "", fmt.Errorf("list internet gateways: %w", err)
}
if len(listResp.Items) > 0 {
return deref(listResp.Items[0].Id), nil
}
enabled := true
name := "oci-portal-igw"
createResp, err := vn.CreateInternetGateway(ctx, core.CreateInternetGatewayRequest{
CreateInternetGatewayDetails: core.CreateInternetGatewayDetails{
CompartmentId: &compartmentID,
VcnId: &vcnID,
IsEnabled: &enabled,
DisplayName: &name,
},
})
if err != nil {
return "", fmt.Errorf("create internet gateway: %w", err)
}
return deref(createResp.InternetGateway.Id), nil
}
// AddInstanceIpv6 实现 Client:为实例主 VNIC 分配一个 IPv6 地址并返回。
// address 为空时由子网 /64 自动分配;子网未启用 IPv6 时 OCI 返回错误。
func (c *RealClient) AddInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) (string, error) {
vnic, err := c.primaryVnic(ctx, cred, region, instanceID)
if err != nil {
return "", err
}
return c.AddVnicIpv6(ctx, cred, region, deref(vnic.Id), address)
}
// AddVnicIpv6 实现 Client:为指定 VNIC 分配一个 IPv6 地址并返回(多网卡场景)。
func (c *RealClient) AddVnicIpv6(ctx context.Context, cred Credentials, region, vnicID, address string) (string, error) {
vn, err := c.vcnClient(cred, region)
if err != nil {
return "", err
}
details := core.CreateIpv6Details{VnicId: &vnicID}
if address != "" {
details.IpAddress = &address
}
resp, err := vn.CreateIpv6(ctx, core.CreateIpv6Request{CreateIpv6Details: details})
if err != nil {
return "", fmt.Errorf("create ipv6 on vnic %s: %w", vnicID, err)
}
return deref(resp.IpAddress), nil
}
// DeleteInstanceIpv6 实现 Client:取消分配实例上的指定 IPv6 地址。
// 遍历实例 VNIC 找到地址对应的 Ipv6 对象后删除;地址不存在时报错。
func (c *RealClient) DeleteInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) error {
vn, err := c.vcnClient(cred, region)
if err != nil {
return err
}
vnics, _, err := c.instanceVnics(ctx, cred, region, instanceID)
if err != nil {
return err
}
for _, vnic := range vnics {
resp, err := vn.ListIpv6s(ctx, core.ListIpv6sRequest{VnicId: vnic.Id})
if err != nil {
continue
}
for _, ip := range resp.Items {
if !sameIPv6(deref(ip.IpAddress), address) {
continue
}
if _, err := vn.DeleteIpv6(ctx, core.DeleteIpv6Request{Ipv6Id: ip.Id}); err != nil {
return fmt.Errorf("delete ipv6 %s: %w", address, err)
}
return nil
}
}
return fmt.Errorf("delete ipv6: address %s not found on instance %s", address, instanceID)
}
// sameIPv6 规范化比较两个 IPv6 地址(兼容大小写与缩写形式差异)。
func sameIPv6(a, b string) bool {
pa, errA := netip.ParseAddr(a)
pb, errB := netip.ParseAddr(b)
if errA != nil || errB != nil {
return a == b
}
return pa == pb
}
// ensureIPv6Egress 在默认安全列表补 egress ::/0 全协议放行。
func ensureIPv6Egress(ctx context.Context, vn core.VirtualNetworkClient, vcn core.Vcn) IPv6Step {
slResp, err := vn.GetSecurityList(ctx, core.GetSecurityListRequest{SecurityListId: vcn.DefaultSecurityListId})
if err != nil {
return IPv6Step{Step: "seclist-ipv6-egress", Status: "failed", Detail: err.Error()}
}
for _, rule := range slResp.EgressSecurityRules {
if deref(rule.Destination) == ipv6AnyCIDR {
return IPv6Step{Step: "seclist-ipv6-egress", Status: "skipped", Detail: "::/0 egress exists"}
}
}
dest := ipv6AnyCIDR
protocol := "all"
rules := append(slResp.EgressSecurityRules, core.EgressSecurityRule{
Protocol: &protocol,
Destination: &dest,
DestinationType: core.EgressSecurityRuleDestinationTypeCidrBlock,
})
_, err = vn.UpdateSecurityList(ctx, core.UpdateSecurityListRequest{
SecurityListId: vcn.DefaultSecurityListId,
UpdateSecurityListDetails: core.UpdateSecurityListDetails{EgressSecurityRules: rules},
})
if err != nil {
return IPv6Step{Step: "seclist-ipv6-egress", Status: "failed", Detail: err.Error()}
}
return IPv6Step{Step: "seclist-ipv6-egress", Status: "done", Detail: "egress ::/0 allowed"}
}