Files
oci-portal/internal/oci/real_instance_ip_integration_test.go
T

380 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package oci
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/oracle/oci-go-sdk/v65/core"
)
func createVolumeRequest(tenancyOCID, ad, name string, size int64) core.CreateVolumeRequest {
return core.CreateVolumeRequest{CreateVolumeDetails: core.CreateVolumeDetails{
CompartmentId: &tenancyOCID,
AvailabilityDomain: &ad,
DisplayName: &name,
SizeInGBs: &size,
}}
}
func getVolumeRequest(volID string) core.GetVolumeRequest {
return core.GetVolumeRequest{VolumeId: &volID}
}
func deleteVolumeRequest(volID string) core.DeleteVolumeRequest {
return core.DeleteVolumeRequest{VolumeId: &volID}
}
// e2eConsoleSSHKey 是控制台连接 e2e 用的一次性 RSA 公钥(私钥已丢弃,
// 仅验证连接串生成);OCI 控制台连接只接受 RSA 公钥。
const e2eConsoleSSHKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXrOBK+eL3JPB3t470k7x+au3IcGbCy3Za7HVcMqTD2XNjar+Te8IND7zjDlrU1DuGL0qa8MFGf8PX84SqK9fQ3kbIuz5KHS+Jk8RYyFSZ1iUs27n3Vkql0tbMj8raCqCiX9nWeu5GSxVu/7UTZG5Tk4mrJXcVe6s+65baQrd6D8V/WKSDBblI8Y0jQ7g2XGsHj5eBV49nCwEtwCWPRzkdNqI8Ac0heH6OcqreuH3GowNSsV9RIe/bHKWpu3Ap6DQHMexTHc5GMmcLu4F43Pij0OaoAA83QVCXLvTMZqyLNQOXxPUEsRWATRKKMKRsvitcWx7+v8WGh06iOHfeE59z oci-portal-e2e"
// TestRealOCIInstanceAutoNetworkAndPublicIP 验证默认可用域 + 自动建网(含
// IPv6 与安全列表全开放)+ 换公网 IP + 添加/取消分配 IPv6 + 控制台连接:
// 不传 availabilityDomain 与 subnetId 创建实例(默认 ad-1、自动建 VCN、分配
// IPv6)→ 等 RUNNING → 换公网 IP → 追加 IPv6 → 控制台连接创建/删除 →
// 删除全部 IPv6 → 清理。A1 容量不足时跳过。
func TestRealOCIInstanceAutoNetworkAndPublicIP(t *testing.T) {
if os.Getenv("OCI_INTEGRATION_TEST") != "1" {
t.Skip("set OCI_INTEGRATION_TEST=1 to run real OCI tests")
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
cred := loadTestCredentials(t, "试用期")
client := NewClient()
prefix := fmt.Sprintf("oci-portal-e2e-%d", time.Now().Unix())
images, err := client.ListImages(ctx, cred, ImagesQuery{OperatingSystem: "Canonical Ubuntu", Shape: "VM.Standard.A1.Flex"})
if err != nil || len(images) == 0 {
t.Fatalf("ListImages: %v", err)
}
// 不传 availabilityDomain 与 subnetId:应默认 ad-1 并自动建 oci-portal-auto-vcn 与子网
instance, err := client.LaunchInstance(ctx, cred, CreateInstanceInput{
DisplayName: prefix + "-vm",
Shape: "VM.Standard.A1.Flex",
Ocpus: 1,
MemoryInGBs: 6,
ImageID: images[0].ID,
BootVolumeSizeGBs: 50,
AssignPublicIP: true,
AssignIpv6: true,
RootPassword: "OciPortalE2E!" + fmt.Sprint(len(prefix)),
})
if err != nil {
if strings.Contains(err.Error(), "capacity") {
t.Skipf("A1 capacity unavailable: %v", err)
}
t.Fatalf("LaunchInstance (auto network): %v", err)
}
if !strings.HasSuffix(instance.AvailabilityDomain, "AD-1") {
t.Errorf("availability domain = %q, want default AD-1", instance.AvailabilityDomain)
}
t.Logf("launched %s in %s with auto network", instance.ID, instance.AvailabilityDomain)
// 确认自动建网产生了 oci-portal-auto-vcn,且 IPv6 与安全列表全开放生效
vcns, err := client.ListVCNs(ctx, cred, "")
if err != nil {
t.Fatalf("ListVCNs: %v", err)
}
autoVcnID := ""
for _, v := range vcns {
if v.DisplayName == autoVcnName {
autoVcnID = v.ID
checkAutoVCNDefaults(ctx, t, client, cred, v)
}
}
if autoVcnID == "" {
t.Error("auto vcn not created")
} else {
// 先注册 VCN 清理(LIFO 后执行),确保实例先终止再删网络
defer cleanupVCN(t, client, cred, autoVcnID)
}
defer terminateAndWait(t, client, cred, instance.ID)
if err := waitInstanceState(ctx, client, cred, instance.ID, "RUNNING"); err != nil {
t.Fatalf("wait RUNNING: %v", err)
}
got, err := client.GetInstance(ctx, cred, "", instance.ID)
if err != nil {
t.Fatalf("GetInstance: %v", err)
}
oldIP := got.PublicIP
if oldIP == "" {
t.Fatal("no public ip on primary vnic, want one")
}
if len(got.Ipv6Addresses) == 0 {
t.Error("no ipv6 address on instance, want auto network ipv6")
}
t.Logf("public=%s ipv6=%v", oldIP, got.Ipv6Addresses)
newIP, err := client.ChangeInstancePublicIP(ctx, cred, "", instance.ID)
if err != nil {
t.Fatalf("ChangeInstancePublicIP: %v", err)
}
t.Logf("public ip changed: %s -> %s", oldIP, newIP)
if newIP == "" || newIP == oldIP {
t.Errorf("new public ip = %q, want a different non-empty address", newIP)
}
// 追加一个自动分配的 IPv6,应与 launch 分配的共存
added, err := client.AddInstanceIpv6(ctx, cred, "", instance.ID, "")
if err != nil {
t.Fatalf("AddInstanceIpv6: %v", err)
}
t.Logf("ipv6 added: %s", added)
withAdded, err := client.GetInstance(ctx, cred, "", instance.ID)
if err != nil {
t.Fatalf("GetInstance after ipv6 add: %v", err)
}
if len(withAdded.Ipv6Addresses) != 2 {
t.Errorf("ipv6 addresses = %v, want 2 after add", withAdded.Ipv6Addresses)
}
verifyConsoleConnection(ctx, t, client, cred, instance.ID)
deleteInstanceIpv6AndVerify(ctx, t, client, cred, instance.ID, withAdded.Ipv6Addresses)
}
// verifyConsoleConnection 创建控制台连接、校验 VNC 连接串并删除。
func verifyConsoleConnection(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, instanceID string) {
t.Helper()
conn, err := client.CreateConsoleConnection(ctx, cred, "", instanceID, e2eConsoleSSHKey)
if err != nil {
t.Fatalf("CreateConsoleConnection: %v", err)
}
if conn.LifecycleState != "ACTIVE" || conn.VncConnectionString == "" {
t.Errorf("console connection state=%s vnc=%q, want ACTIVE with vnc string", conn.LifecycleState, conn.VncConnectionString)
}
t.Logf("console connection %s state=%s", conn.ID, conn.LifecycleState)
list, err := client.ListConsoleConnections(ctx, cred, "", instanceID)
if err != nil || len(list) == 0 {
t.Errorf("ListConsoleConnections: %v (got %d)", err, len(list))
}
if err := client.DeleteConsoleConnection(ctx, cred, "", conn.ID); err != nil {
t.Errorf("DeleteConsoleConnection: %v", err)
} else {
t.Log("console connection deleted")
}
}
// checkAutoVCNDefaults 校验自动 VCN 已分配 IPv6 /56 且默认安全列表全开放。
// OCI 在异步分配 IPv6 时可能追加缺省 ICMPv6 规则,因此按语义而非数量校验。
func checkAutoVCNDefaults(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, vcn VCN) {
t.Helper()
if len(vcn.Ipv6CidrBlocks) == 0 {
t.Error("auto vcn has no ipv6 cidr, want one")
}
sl, err := client.GetSecurityList(ctx, cred, "", vcn.DefaultSecurityListID)
if err != nil {
t.Errorf("GetSecurityList: %v", err)
return
}
if !hasAllOpenRules(sl) {
t.Errorf("default seclist not all-open: ingress=%+v egress=%+v", sl.IngressRules, sl.EgressRules)
}
}
// hasAllOpenRules 判断安全列表出入方向是否均放行 IPv4 与 IPv6 全部流量。
func hasAllOpenRules(sl SecurityList) bool {
in, out := map[string]bool{}, map[string]bool{}
for _, r := range sl.IngressRules {
if r.Protocol == "all" {
in[r.Source] = true
}
}
for _, r := range sl.EgressRules {
if r.Protocol == "all" {
out[r.Destination] = true
}
}
return in["0.0.0.0/0"] && in["::/0"] && out["0.0.0.0/0"] && out["::/0"]
}
// deleteInstanceIpv6AndVerify 取消分配实例的全部 IPv6 地址并确认清空。
func deleteInstanceIpv6AndVerify(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, instanceID string, addrs []string) {
t.Helper()
if len(addrs) == 0 {
return
}
for _, addr := range addrs {
if err := client.DeleteInstanceIpv6(ctx, cred, "", instanceID, addr); err != nil {
t.Fatalf("DeleteInstanceIpv6 %s: %v", addr, err)
}
}
after, err := client.GetInstance(ctx, cred, "", instanceID)
if err != nil {
t.Fatalf("GetInstance after ipv6 delete: %v", err)
}
if len(after.Ipv6Addresses) != 0 {
t.Errorf("ipv6 addresses after delete = %v, want none", after.Ipv6Addresses)
}
t.Logf("ipv6 deleted: %v", addrs)
// 删除 IPv6 后立即终止实例,偶发 OCI 侧 VNIC 级联清理失败回滚
// VNIC 残留 AVAILABLE 挡住删子网),等待片刻让状态收敛
time.Sleep(15 * time.Second)
}
// TestRealOCIVolumeAttachment 验证块卷附加/分离与引导卷挂载查询。
// 依赖已有实例:需先运行上面的测试留存实例,或有其他运行中实例;否则跳过。
// 这里独立创建一个实例做完整链路,A1 容量不足时跳过。
func TestRealOCIVolumeAttachment(t *testing.T) {
if os.Getenv("OCI_INTEGRATION_TEST") != "1" {
t.Skip("set OCI_INTEGRATION_TEST=1 to run real OCI tests")
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
cred := loadTestCredentials(t, "试用期")
client := NewClient()
prefix := fmt.Sprintf("oci-portal-e2e-%d", time.Now().Unix())
ads, err := client.ListAvailabilityDomains(ctx, cred, "")
if err != nil || len(ads) == 0 {
t.Fatalf("ListAvailabilityDomains: %v", err)
}
images, err := client.ListImages(ctx, cred, ImagesQuery{OperatingSystem: "Canonical Ubuntu", Shape: "VM.Standard.A1.Flex"})
if err != nil || len(images) == 0 {
t.Fatalf("ListImages: %v", err)
}
vcn, err := client.CreateVCN(ctx, cred, CreateVCNInput{DisplayName: prefix + "-vcn", CidrBlock: "10.97.0.0/16", DnsLabel: "e2evol"})
if err != nil {
t.Fatalf("CreateVCN: %v", err)
}
defer cleanupVCN(t, client, cred, vcn.ID)
subnet, err := client.CreateSubnet(ctx, cred, CreateSubnetInput{VcnID: vcn.ID, DisplayName: prefix + "-subnet", CidrBlock: "10.97.1.0/24", DnsLabel: "e2sub"})
if err != nil {
t.Fatalf("CreateSubnet: %v", err)
}
instance, err := client.LaunchInstance(ctx, cred, CreateInstanceInput{
AvailabilityDomain: ads[0], DisplayName: prefix + "-vm", Shape: "VM.Standard.A1.Flex",
Ocpus: 1, MemoryInGBs: 6, ImageID: images[0].ID, BootVolumeSizeGBs: 50, SubnetID: subnet.ID,
})
if err != nil {
if strings.Contains(err.Error(), "capacity") {
t.Skipf("A1 capacity unavailable: %v", err)
}
t.Fatalf("LaunchInstance: %v", err)
}
defer terminateAndWait(t, client, cred, instance.ID)
if err := waitInstanceState(ctx, client, cred, instance.ID, "RUNNING"); err != nil {
t.Fatalf("wait RUNNING: %v", err)
}
// 引导卷挂载查询
bvAtts, err := client.ListBootVolumeAttachments(ctx, cred, "", instance.ID)
if err != nil {
t.Fatalf("ListBootVolumeAttachments: %v", err)
}
if len(bvAtts) == 0 {
t.Error("no boot volume attachment, want one")
} else {
t.Logf("boot volume attachment: %s state=%s", bvAtts[0].ID, bvAtts[0].LifecycleState)
}
// 创建块卷并附加、分离
volID := createTestBlockVolume(ctx, t, client, cred, ads[0], prefix)
if volID == "" {
return
}
defer deleteTestBlockVolume(t, client, cred, volID)
att, err := client.AttachVolume(ctx, cred, "", instance.ID, volID, false)
if err != nil {
t.Fatalf("AttachVolume: %v", err)
}
t.Logf("attached volume: %s device=%s", att.ID, att.Device)
if err := waitVolumeAttachmentState(ctx, client, cred, instance.ID, att.ID, "ATTACHED"); err != nil {
t.Fatalf("wait volume ATTACHED: %v", err)
}
atts, err := client.ListVolumeAttachments(ctx, cred, "", instance.ID)
if err != nil || len(atts) == 0 {
t.Fatalf("ListVolumeAttachments: %v (got %d)", err, len(atts))
}
if err := client.DetachVolume(ctx, cred, "", att.ID); err != nil {
t.Fatalf("DetachVolume: %v", err)
}
if err := waitVolumeAttachmentState(ctx, client, cred, instance.ID, att.ID, "DETACHED"); err != nil {
t.Errorf("wait volume DETACHED: %v", err)
}
t.Log("volume detached")
}
func createTestBlockVolume(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, ad, prefix string) string {
t.Helper()
bc, err := client.blockstorageClient(cred, "")
if err != nil {
t.Errorf("blockstorage client: %v", err)
return ""
}
size := int64(50)
name := prefix + "-vol"
resp, err := bc.CreateVolume(ctx, createVolumeRequest(cred.TenancyOCID, ad, name, size))
if err != nil {
t.Fatalf("CreateVolume: %v", err)
}
volID := deref(resp.Id)
for i := 0; i < 60; i++ {
got, err := bc.GetVolume(ctx, getVolumeRequest(volID))
if err == nil && string(got.LifecycleState) == "AVAILABLE" {
return volID
}
select {
case <-ctx.Done():
return volID
case <-time.After(3 * time.Second):
}
}
return volID
}
func waitVolumeAttachmentState(ctx context.Context, client *RealClient, cred Credentials, instanceID, attachmentID, want string) error {
for i := 0; i < 60; i++ {
atts, err := client.ListVolumeAttachments(ctx, cred, "", instanceID)
if err != nil {
return fmt.Errorf("poll volume attachment: %w", err)
}
for _, a := range atts {
if a.ID == attachmentID && a.LifecycleState == want {
return nil
}
}
if want == "DETACHED" {
// 分离后可能不再出现在列表中,视为完成
found := false
for _, a := range atts {
if a.ID == attachmentID {
found = true
}
}
if !found {
return nil
}
}
select {
case <-ctx.Done():
return fmt.Errorf("poll volume attachment %s: %w", want, ctx.Err())
case <-time.After(3 * time.Second):
}
}
return fmt.Errorf("poll volume attachment %s: timed out", want)
}
func deleteTestBlockVolume(t *testing.T, client *RealClient, cred Credentials, volID string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
bc, err := client.blockstorageClient(cred, "")
if err != nil {
t.Errorf("cleanup: blockstorage client: %v", err)
return
}
if _, err := bc.DeleteVolume(ctx, deleteVolumeRequest(volID)); err != nil {
t.Errorf("cleanup: delete volume: %v", err)
return
}
t.Logf("cleanup: volume %s deleted", volID)
}