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