175 lines
6.0 KiB
Go
175 lines
6.0 KiB
Go
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)
|
|
}
|