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