package oci import ( "context" "fmt" "os" "strings" "testing" "time" ) // TestRealOCIInstanceLifecycle 走真实实例完整生命周期: // 建网络 → 查 AD/镜像 → 创建 A1 实例 → 等待 RUNNING → 校验 IP 补全 → // 改名 → 引导卷查改 → STOP → 终止(不保留卷)→ 清理网络。 // A1 容量不足时跳过(试用区域常见)。 func TestRealOCIInstanceLifecycle(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()) vcn, err := client.CreateVCN(ctx, cred, CreateVCNInput{ DisplayName: prefix + "-vcn", CidrBlock: "10.98.0.0/16", DnsLabel: "e2einst", }) 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.98.1.0/24", DnsLabel: "e2esub", }) if err != nil { t.Fatalf("CreateSubnet: %v", err) } ads, err := client.ListAvailabilityDomains(ctx, cred, "") if err != nil || len(ads) == 0 { t.Fatalf("ListAvailabilityDomains: %v (got %d)", err, len(ads)) } 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 (got %d)", err, len(images)) } t.Logf("using ad=%s image=%s", ads[0], images[0].DisplayName) 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, AssignPublicIP: true, SSHPublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEfake+e2e+key oci-portal-e2e", }) if err != nil { if strings.Contains(err.Error(), "Out of host capacity") || strings.Contains(err.Error(), "OutOfCapacity") { t.Skipf("A1 capacity unavailable in this region: %v", err) } t.Fatalf("LaunchInstance: %v", err) } t.Logf("launched instance %s (%s)", instance.DisplayName, instance.ID) 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) } t.Logf("instance running: shape=%s %gocpu/%gGB privateIp=%s publicIp=%s", got.Shape, got.Ocpus, got.MemoryInGBs, got.PrivateIP, got.PublicIP) if got.PrivateIP == "" { t.Error("PrivateIP empty, want enriched from VNIC") } if got.PublicIP == "" { t.Error("PublicIP empty, want assigned") } if got.SubnetID != subnet.ID { t.Errorf("SubnetID = %s, want %s", got.SubnetID, subnet.ID) } if _, err := client.UpdateInstance(ctx, cred, instance.ID, UpdateInstanceInput{DisplayName: prefix + "-vm-renamed"}); err != nil { t.Errorf("UpdateInstance: %v", err) } verifyBootVolume(ctx, t, client, cred, ads[0], instance.ID, prefix) if _, err := client.InstanceAction(ctx, cred, "", instance.ID, "STOP"); err != nil { t.Fatalf("InstanceAction STOP: %v", err) } if err := waitInstanceState(ctx, client, cred, instance.ID, "STOPPED"); err != nil { t.Fatalf("wait STOPPED: %v", err) } t.Log("instance stopped, terminating") } func verifyBootVolume(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, ad, instanceID, prefix string) { t.Helper() volumes, err := client.ListBootVolumes(ctx, cred, "", ad) if err != nil { t.Errorf("ListBootVolumes: %v", err) return } for _, v := range volumes { detail, err := client.GetBootVolume(ctx, cred, "", v.ID) if err != nil { t.Errorf("GetBootVolume: %v", err) continue } if detail.AttachedInstanceID != instanceID { continue } t.Logf("boot volume %s size=%dGB vpus=%d attached=%s", detail.DisplayName, detail.SizeInGBs, detail.VpusPerGB, detail.AttachedInstanceID) if detail.SizeInGBs != 50 { t.Errorf("boot volume size = %d, want 50", detail.SizeInGBs) } if _, err := client.UpdateBootVolume(ctx, cred, v.ID, UpdateBootVolumeInput{DisplayName: prefix + "-bv"}); err != nil { t.Errorf("UpdateBootVolume: %v", err) } return } t.Errorf("no boot volume attached to instance %s found in %s", instanceID, ad) } // waitInstanceState 轮询实例直到到达目标状态。 func waitInstanceState(ctx context.Context, client *RealClient, cred Credentials, instanceID, want string) error { for i := 0; i < 60; i++ { inst, err := client.GetInstance(ctx, cred, "", instanceID) if err != nil { return fmt.Errorf("poll instance: %w", err) } if inst.LifecycleState == want { return nil } select { case <-ctx.Done(): return fmt.Errorf("poll instance state %s: %w", want, ctx.Err()) case <-time.After(5 * time.Second): } } return fmt.Errorf("poll instance state %s: timed out", want) } // terminateAndWait 终止实例(不保留引导卷)并等待 TERMINATED, // 保证后续网络清理不被占用的 VNIC 阻塞。 func terminateAndWait(t *testing.T, client *RealClient, cred Credentials, instanceID string) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute) defer cancel() inst, err := client.GetInstance(ctx, cred, "", instanceID) if err == nil && inst.LifecycleState == "TERMINATED" { return } if err := client.TerminateInstance(ctx, cred, "", instanceID, false); err != nil { t.Errorf("cleanup: terminate instance: %v", err) return } if err := waitInstanceState(ctx, client, cred, instanceID, "TERMINATED"); err != nil { t.Errorf("cleanup: wait TERMINATED: %v (manual cleanup may be required)", err) return } t.Logf("cleanup: instance %s terminated", instanceID) }