diff --git a/api-runtime/common-runtime/VMPublicIPManager.go b/api-runtime/common-runtime/VMPublicIPManager.go index 502696928..c5f2fdf4b 100644 --- a/api-runtime/common-runtime/VMPublicIPManager.go +++ b/api-runtime/common-runtime/VMPublicIPManager.go @@ -110,9 +110,32 @@ func AssignVMDefaultPublicIP(connectionName string, vmName string) (*cres.Public return nil, err } + waitUntilVMPublicIPVisible(connectionName, vmName) + return info, nil } +// waitUntilVMPublicIPVisible polls GetVM until the VM's PublicIP is visible +// or a short timeout elapses. Some CSPs (observed on NHN Cloud) report the +// Associate call itself as successful before the association is visible +// through a VM lookup - callers that immediately GetVM right after +// AssignVMDefaultPublicIP returns would otherwise see an empty PublicIP +// despite the assign having actually succeeded. Best-effort: a timeout is +// silently ignored, since AssociatePublicIP already succeeded and the +// caller has the authoritative PublicIPInfo regardless. +func waitUntilVMPublicIPVisible(connectionName string, vmName string) { + waiter := NewWaiter(2, 30) + for { + vmInfo, err := GetVM(connectionName, VM, vmName) + if err == nil && vmInfo.PublicIP != "" { + return + } + if !waiter.Wait() { + return + } + } +} + // UnassignVMDefaultPublicIP disassociates and DELETES the PublicIP currently // attached to the VM's default NIC. Fails if the VM has no default PublicIP. // diff --git a/api/docs.go b/api/docs.go index d29934997..71cf4ebcc 100644 --- a/api/docs.go +++ b/api/docs.go @@ -17629,6 +17629,11 @@ const docTemplate = `{ "spider.ClusterTokenStatus": { "type": "object", "properties": { + "expirationTimestamp": { + "description": "ExpirationTimestamp is when the token stops being accepted, in RFC3339.\nIt is omitted when the CSP provides no expiry information, which the spec allows:\nclients then keep the credential until a 401 forces a refresh.", + "type": "string", + "example": "2026-09-02T05:15:00Z" + }, "token": { "type": "string", "example": "k8s-aws-v1.aHR0cHM6Ly9zdHMuYXA..." diff --git a/api/swagger.json b/api/swagger.json index 43ed3eeab..495f59277 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -17626,6 +17626,11 @@ "spider.ClusterTokenStatus": { "type": "object", "properties": { + "expirationTimestamp": { + "description": "ExpirationTimestamp is when the token stops being accepted, in RFC3339.\nIt is omitted when the CSP provides no expiry information, which the spec allows:\nclients then keep the credential until a 401 forces a refresh.", + "type": "string", + "example": "2026-09-02T05:15:00Z" + }, "token": { "type": "string", "example": "k8s-aws-v1.aHR0cHM6Ly9zdHMuYXA..." diff --git a/api/swagger.yaml b/api/swagger.yaml index 9653bb237..040a5cc05 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2755,6 +2755,13 @@ definitions: type: object spider.ClusterTokenStatus: properties: + expirationTimestamp: + description: |- + ExpirationTimestamp is when the token stops being accepted, in RFC3339. + It is omitted when the CSP provides no expiry information, which the spec allows: + clients then keep the credential until a 401 forces a refresh. + example: "2026-09-02T05:15:00Z" + type: string token: example: k8s-aws-v1.aHR0cHM6Ly9zdHMuYXA... type: string diff --git a/cloud-control-manager/cloud-driver/drivers/aws/resources/PublicIPHandler.go b/cloud-control-manager/cloud-driver/drivers/aws/resources/PublicIPHandler.go index 51d8b3954..9afdbb18c 100644 --- a/cloud-control-manager/cloud-driver/drivers/aws/resources/PublicIPHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/aws/resources/PublicIPHandler.go @@ -373,10 +373,20 @@ func (h *AwsPublicIPHandler) RemoveDefaultPublicIP(vmIID irs.IID) (bool, error) return false, err } + // CB-Spider fills an unset StsToken with the literal string "Not set" + // (see KeyValueListGetValue in CloudDriverHandler_common.go), not "" - the + // same normalization AwsDriver.go already applies before building the v1 + // session. Skipping it here would sign the request with the literal + // string "Not set" as the STS session token, which AWS rejects with + // AuthFailure even though ClientId/ClientSecret are valid. + stsToken := h.CredentialInfo.StsToken + if stsToken == "Not set" { + stsToken = "" + } v2Client := ec2v2.New(ec2v2.Options{ Region: h.Region.Region, Credentials: credentialsv2.NewStaticCredentialsProvider( - h.CredentialInfo.ClientId, h.CredentialInfo.ClientSecret, h.CredentialInfo.StsToken), + h.CredentialInfo.ClientId, h.CredentialInfo.ClientSecret, stsToken), }) _, err = v2Client.ModifyNetworkInterfaceAttribute(context.TODO(), &ec2v2.ModifyNetworkInterfaceAttributeInput{ NetworkInterfaceId: awsv2.String(primaryENIId), diff --git a/cloud-control-manager/cloud-driver/drivers/kt/resources/PublicIPHandler.go b/cloud-control-manager/cloud-driver/drivers/kt/resources/PublicIPHandler.go index 3ddb86f2f..12a8fd856 100644 --- a/cloud-control-manager/cloud-driver/drivers/kt/resources/PublicIPHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/kt/resources/PublicIPHandler.go @@ -19,7 +19,6 @@ import ( irs "github.com/cloud-barista/cb-spider/cloud-control-manager/cloud-driver/interfaces/resources" ktvpcsdk "github.com/cloud-barista/ktcloudvpc-sdk-go" ips "github.com/cloud-barista/ktcloudvpc-sdk-go/openstack/compute/v2/extensions/floatingips" - "github.com/cloud-barista/ktcloudvpc-sdk-go/openstack/networking/v2/ports" portforward "github.com/cloud-barista/ktcloudvpc-sdk-go/openstack/networking/v2/extensions/layer3/portforwarding" "github.com/cloud-barista/ktcloudvpc-sdk-go/openstack/networking/v2/extensions/layer3/staticnat" "github.com/cloud-barista/ktcloudvpc-sdk-go/pagination" @@ -337,9 +336,23 @@ func (h *KTVpcPublicIPHandler) AssociatePublicIP(publicIPIID irs.IID, vmIID irs. // with a timeout - the goroutine is abandoned (not cancelled) if it fires, // trading a leaked goroutine on the rare hang for a request that still // returns an error instead of hanging. -func (h *KTVpcPublicIPHandler) resolveSecurityContext(vmIID irs.IID, nicIID irs.IID) (tierNetworkId string, sgSystemIDs []string, err error) { +// resolveVMInfo fetches the VM's full VMInfo via GetVM() - the shared +// lookup behind resolveSecurityContext (Tier/SecurityGroup info) and +// resolveVMPrivateIP (PrivateIP). KT Cloud VPC does not really run on +// Neutron - the openstack-shaped `ports` API (ports.Get/ports.List) is a +// thin compatibility shim that has been observed to return persistent +// Internal Server Errors, so it must not be used for any of this; GetVM() +// is the only reliable source. +// +// GetVM() itself pulls in some unrelated lookups (image/disk info) and one +// of those has been observed to hang indefinitely on a slow/misbehaving KT +// API response. To avoid blocking this request forever, the call is bounded +// with a timeout - the goroutine is abandoned (not cancelled) if it fires, +// trading a leaked goroutine on the rare hang for a request that still +// returns an error instead of hanging. +func (h *KTVpcPublicIPHandler) resolveVMInfo(vmIID irs.IID) (irs.VMInfo, error) { if vmIID.SystemId == "" && vmIID.NameId == "" { - return "", nil, fmt.Errorf("resolveSecurityContext: vmIID is required to resolve Tier/SecurityGroup info via GetVM()") + return irs.VMInfo{}, fmt.Errorf("vmIID is required to resolve VM info via GetVM()") } // GetVM() (via mappingVMInfo) uses ImageClient/VolumeClient too (e.g. for @@ -375,15 +388,21 @@ func (h *KTVpcPublicIPHandler) resolveSecurityContext(vmIID irs.IID, nicIID irs. resultCh <- getVMResult{vmInfo, getErr} }() - var vmInfo irs.VMInfo select { case r := <-resultCh: if r.err != nil { - return "", nil, fmt.Errorf("resolveSecurityContext: GetVM failed for VM [%s]: %w", vmIID.NameId, r.err) + return irs.VMInfo{}, fmt.Errorf("GetVM failed for VM [%s]: %w", vmIID.NameId, r.err) } - vmInfo = r.vmInfo + return r.vmInfo, nil case <-time.After(90 * time.Second): - return "", nil, fmt.Errorf("resolveSecurityContext: GetVM timed out after 90s for VM [%s]", vmIID.NameId) + return irs.VMInfo{}, fmt.Errorf("GetVM timed out after 90s for VM [%s]", vmIID.NameId) + } +} + +func (h *KTVpcPublicIPHandler) resolveSecurityContext(vmIID irs.IID, nicIID irs.IID) (tierNetworkId string, sgSystemIDs []string, err error) { + vmInfo, vmErr := h.resolveVMInfo(vmIID) + if vmErr != nil { + return "", nil, fmt.Errorf("resolveSecurityContext: %w", vmErr) } if vmInfo.SubnetIID.SystemId == "" { @@ -492,26 +511,18 @@ func (h *KTVpcPublicIPHandler) removeStaticNATIfAny(publicIPSystemId string) { } } -// resolveVMPrivateIP finds the first private IP of a VM via its ports. +// resolveVMPrivateIP finds a VM's private IP via GetVM() (see resolveVMInfo - +// the openstack-shaped `ports` API is deliberately not used here, it has +// been observed to return persistent Internal Server Errors on KT Cloud VPC). func (h *KTVpcPublicIPHandler) resolveVMPrivateIP(vmIID irs.IID) (string, error) { - deviceID := vmIID.SystemId - if deviceID == "" { - deviceID = vmIID.NameId - } - allPages, err := ports.List(h.NetworkClient, ports.ListOpts{DeviceID: deviceID}).AllPages() - if err != nil { - return "", fmt.Errorf("failed to list ports for VM [%s]: %w", deviceID, err) - } - portList, err := ports.ExtractPorts(allPages) + vmInfo, err := h.resolveVMInfo(vmIID) if err != nil { - return "", fmt.Errorf("failed to extract ports for VM [%s]: %w", deviceID, err) + return "", fmt.Errorf("resolveVMPrivateIP: %w", err) } - for _, p := range portList { - if len(p.FixedIPs) > 0 && p.FixedIPs[0].IPAddress != "" { - return p.FixedIPs[0].IPAddress, nil - } + if vmInfo.PrivateIP == "" { + return "", fmt.Errorf("no private IP found for VM [%s]", vmIID.NameId) } - return "", fmt.Errorf("no private IP found for VM [%s]", deviceID) + return vmInfo.PrivateIP, nil } // RemoveDefaultPublicIP removes whatever PublicIP is currently bound to the diff --git a/cloud-control-manager/cloud-driver/drivers/ncp/resources/PublicIPHandler.go b/cloud-control-manager/cloud-driver/drivers/ncp/resources/PublicIPHandler.go index aad722c66..2b64a9212 100644 --- a/cloud-control-manager/cloud-driver/drivers/ncp/resources/PublicIPHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/ncp/resources/PublicIPHandler.go @@ -76,28 +76,45 @@ func (h *NcpVpcPublicIPHandler) CreatePublicIP(reqInfo irs.PublicIPInfo) (irs.Pu created := resp.PublicIpInstanceList[0] systemId := ncloud.StringValue(created.PublicIpInstanceNo) - // NCP is async: poll until the IP reaches a stable operation state before returning. - // Attempting DeletePublicIpInstance while the IP is still initializing returns error 1080101. + h.waitForPublicIPStable(systemId) + + info := h.extractPublicIPInfo(created) + info.IId.NameId = reqInfo.IId.NameId + return info, nil +} + +// waitForPublicIPStable polls until the given Public IP (a) leaves the async +// INIT/CREAT state NCP puts it in right after creation or after being +// auto-assigned at VM-creation time, AND (b) has no in-flight +// PublicIpInstanceOperation ("Public IP instance operation", per the NCP SDK +// field doc) - e.g. a just-completed +// Associate/Disassociate is still being applied. Any operation against the +// IP while either condition holds (Disassociate, Delete, ...) fails with NCP +// error 1080101 ("This is not an authorized IP in operation") - the +// "operation" in that message is this very field, not a caller-IP ACL. +// Best-effort - a poll error or timeout is silently ignored and the caller +// proceeds anyway, same as CreatePublicIP already did before this was +// extracted into a shared helper. +func (h *NcpVpcPublicIPHandler) waitForPublicIPStable(systemId string) { for i := 0; i < 30; i++ { pollResp, pollErr := h.VMClient.V2Api.GetPublicIpInstanceList(&vserver.GetPublicIpInstanceListRequest{ RegionCode: ncloud.String(h.RegionInfo.Region), PublicIpInstanceNoList: []*string{ncloud.String(systemId)}, }) if pollErr == nil && len(pollResp.PublicIpInstanceList) > 0 { + pip := pollResp.PublicIpInstanceList[0] statusCode := "" - if pollResp.PublicIpInstanceList[0].PublicIpInstanceStatus != nil { - statusCode = ncloud.StringValue(pollResp.PublicIpInstanceList[0].PublicIpInstanceStatus.Code) + if pip.PublicIpInstanceStatus != nil { + statusCode = ncloud.StringValue(pip.PublicIpInstanceStatus.Code) } - if statusCode != "" && statusCode != "INIT" && statusCode != "CREAT" { - break + statusStable := statusCode != "" && statusCode != "INIT" && statusCode != "CREAT" + opClear := pip.PublicIpInstanceOperation == nil || ncloud.StringValue(pip.PublicIpInstanceOperation.Code) == "" + if statusStable && opClear { + return } } time.Sleep(2 * time.Second) } - - info := h.extractPublicIPInfo(created) - info.IId.NameId = reqInfo.IId.NameId - return info, nil } func (h *NcpVpcPublicIPHandler) ListPublicIP() ([]*irs.PublicIPInfo, error) { @@ -307,17 +324,34 @@ func (h *NcpVpcPublicIPHandler) DisassociatePublicIP(publicIPIID irs.IID) (bool, // RemoveDefaultPublicIP removes whatever Public IP Instance is currently // associated with the VM, discovered live via GetPublicIpInstanceList -// filtered by ServerName (regardless of whether it was ever tracked as a -// separate CB-Spider PublicIP resource) - then disassociates and deletes it -// via the existing DisassociatePublicIP/DeletePublicIP methods. Works on a -// running VM - no stop/restart required. +// (regardless of whether it was ever tracked as a separate CB-Spider +// PublicIP resource) - then disassociates and deletes it via the existing +// DisassociatePublicIP/DeletePublicIP methods. Works on a running VM - no +// stop/restart required. +// +// vmIID here is a driver-level IID rebuilt by getDriverIID() from the VM's +// stored SystemId, so vmIID.NameId is NOT the VM's real server name (it is +// derived from SystemId, see api-runtime/common-runtime/CommonManager.go). +// Matching by ServerName would therefore filter on a bogus value, so the +// association is resolved by ServerInstanceNo instead, same as +// AssociatePublicIP/DisassociatePublicIP above. func (h *NcpVpcPublicIPHandler) RemoveDefaultPublicIP(vmIID irs.IID) (bool, error) { hiscallInfo := GetCallLogScheme(h.RegionInfo.Zone, call.PUBLICIP, vmIID.NameId, "RemoveDefaultPublicIP()") start := call.Start() + serverInstanceNo := vmIID.SystemId + if serverInstanceNo == "" { + serverInstanceNo = vmIID.NameId + } + if serverInstanceNo == "" { + err := fmt.Errorf("RemoveDefaultPublicIP: vmIID (SystemId or NameId) is required for NCP") + cblogger.Error(err) + return false, err + } + req := &vserver.GetPublicIpInstanceListRequest{ - RegionCode: ncloud.String(h.RegionInfo.Region), - ServerName: ncloud.String(vmIID.NameId), + RegionCode: ncloud.String(h.RegionInfo.Region), + IsAssociated: ncloud.Bool(true), } resp, err := h.VMClient.V2Api.GetPublicIpInstanceList(req) if err != nil { @@ -325,19 +359,41 @@ func (h *NcpVpcPublicIPHandler) RemoveDefaultPublicIP(vmIID irs.IID) (bool, erro LoggingError(hiscallInfo, err) return false, err } - if len(resp.PublicIpInstanceList) == 0 { + + var attached []*vserver.PublicIpInstance + for _, pip := range resp.PublicIpInstanceList { + if ncloud.StringValue(pip.ServerInstanceNo) == serverInstanceNo { + attached = append(attached, pip) + } + } + if len(attached) == 0 { err := fmt.Errorf("no PublicIP found attached to VM %s", vmIID.NameId) cblogger.Error(err) return false, err } - for _, pip := range resp.PublicIpInstanceList { - pipIID := irs.IID{SystemId: ncloud.StringValue(pip.PublicIpInstanceNo)} + for _, pip := range attached { + systemId := ncloud.StringValue(pip.PublicIpInstanceNo) + // A PublicIP auto-assigned at VM-creation time (AssociateWithPublicIp) + // is commonly still mid-async-setup (INIT/CREAT) by the time a caller + // can react to the VM going Running - see waitForPublicIPStable. + h.waitForPublicIPStable(systemId) + + pipIID := irs.IID{SystemId: systemId} if _, err := h.DisassociatePublicIP(pipIID); err != nil { cblogger.Error(err) LoggingError(hiscallInfo, err) return false, err } + + // Disassociating triggers its own async state transition, so the IP + // can be back in a non-stable operation state by the time Delete + // runs - wait again rather than assuming the earlier wait still + // covers it (this is the exact case CreatePublicIP's comment above + // documents: "Attempting DeletePublicIpInstance while the IP is + // still initializing returns error 1080101"). + h.waitForPublicIPStable(systemId) + if _, err := h.DeletePublicIP(pipIID); err != nil { cblogger.Error(err) LoggingError(hiscallInfo, err) diff --git a/cloud-control-manager/cloud-driver/drivers/nhn/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/nhn/resources/VMHandler.go index 29a3ec455..3c8ac3de0 100644 --- a/cloud-control-manager/cloud-driver/drivers/nhn/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/nhn/resources/VMHandler.go @@ -1137,8 +1137,16 @@ func (vmHandler *NhnCloudVMHandler) mappingVMInfo(server servers.Server) (irs.VM } nicInfo.PublicIPs = publicIPs allPublicIPs = append(allPublicIPs, publicIPs...) - if idx == 0 && len(publicIPs) > 0 { - vmInfo.PublicIP = publicIPs[0] + if idx == 0 { + // Overwrite (including clearing to "") with this live, per-NIC + // floating IP lookup - it supersedes the possibly-stale value + // read from Nova's server.Addresses above (e.g. right after + // UnassignVMDefaultPublicIP, Nova may still report the old IP). + if len(publicIPs) > 0 { + vmInfo.PublicIP = publicIPs[0] + } else { + vmInfo.PublicIP = "" + } } } } diff --git a/cloud-control-manager/cloud-driver/drivers/openstack/resources/VMHandler.go b/cloud-control-manager/cloud-driver/drivers/openstack/resources/VMHandler.go index 9d904dc13..f0ee92494 100644 --- a/cloud-control-manager/cloud-driver/drivers/openstack/resources/VMHandler.go +++ b/cloud-control-manager/cloud-driver/drivers/openstack/resources/VMHandler.go @@ -909,8 +909,16 @@ func (vmHandler *OpenStackVMHandler) mappingServerInfo(server servers.Server) ir } nicInfo.PublicIPs = publicIPs allPublicIPs = append(allPublicIPs, publicIPs...) - if idx == 0 && len(publicIPs) > 0 { - vmInfo.PublicIP = publicIPs[0] + if idx == 0 { + // Overwrite (including clearing to "") with this live, per-NIC + // floating IP lookup - it supersedes the possibly-stale value + // read from Nova's server.Addresses above (e.g. right after + // UnassignVMDefaultPublicIP, Nova may still report the old IP). + if len(publicIPs) > 0 { + vmInfo.PublicIP = publicIPs[0] + } else { + vmInfo.PublicIP = "" + } } } } diff --git a/test/vm-default-publicip-test/README.md b/test/vm-default-publicip-test/README.md index de97ac368..b3340ce53 100644 --- a/test/vm-default-publicip-test/README.md +++ b/test/vm-default-publicip-test/README.md @@ -254,5 +254,3 @@ Failed : 0 - **AWS, GCP만 Resume 후 PublicIP가 바뀜(`CHANGED`)** — 나머지 8개 CSP(Azure/Alibaba/Tencent/IBM/OpenStack/NCP/NHN/KT)는 전부 `SAME`(동일 IP 유지). - Alibaba는 Suspend 중 조회된 PublicIP가 `(none)`이었으나 Resume 후 Initial과 동일 IP로 복귀 — 최종 비교는 `SAME`. - VM Delete, 기본 리소스(KeyPair/SG/VPC) Cleanup도 10개 CSP 전부 정상 완료. - -전체 CSP 상세 표 및 문서 조사와의 비교는 [`vm-default-publicip-analysis.md`](../../vm-default-publicip-analysis.md)의 "실제 시험 결과 (2026-08-20)" 섹션 참고. diff --git a/test/vm-default-publicip-test/assign-publicip-true/README.md b/test/vm-default-publicip-test/assign-publicip-true/README.md new file mode 100644 index 000000000..00f3f936b --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/README.md @@ -0,0 +1,239 @@ +# CB-Spider VM AssignPublicIP=true Test + +`ReqInfo.AssignPublicIP: true` 옵션으로 VM을 생성했을 때, 생성 직후부터 Public +IP가 정상적으로 할당되어 SSH 접속이 가능하며, 이렇게 만든 VM에서 VM Manager의 +default-PublicIP API(`UnassignVMDefaultPublicIP`/`AssignVMDefaultPublicIP`)로 +Public IP를 뗐다 다시 붙일 수 있는지를 10개 CSP 전체에서 검증하는 테스트 +스위트입니다. `../assign-publicip-false/`와 상위 디렉터리 +`test/vm-default-publicip-test/`를 참고하여 작성했습니다. + +## Purpose + +VM 생성 REST API의 `AssignPublicIP`(옵션, `*bool`) 필드가 `true`로 설정되었을 +때, 그리고 그렇게 만든 VM에서 나중에 VM Manager의 default-PublicIP API로 +Public IP를 뗐다 다시 붙일 수 있는지를 검증합니다 (`true` → `false` → `true`): + +1. VM이 `Running` 상태에 정상적으로 도달하는가 (타임아웃/Failed면 실패) +2. VM에 `PrivateIP`가 정상적으로 할당되는가 +3. VM에 `PublicIP`가 생성 직후부터 **할당되는가** (값이 비어있으면 실패) +4. 할당된 초기 Public IP로 SSH 접속 확인 +5. `UnassignVMDefaultPublicIP` 요청 (`DELETE /vm/{Name}/publicip`) +6. Public IP가 해제되었는가 확인 (값이 존재하면 실패) +7. `AssignVMDefaultPublicIP` 요청 (`POST /vm/{Name}/publicip`) +8. Public IP가 다시 할당되었는가 확인 (값이 비어있으면 실패) +9. 재할당된 Public IP로 SSH 접속 재확인 + +## Prerequisites + +### CB-Spider Running + +```bash +cd ./bin; ./start.sh +``` + +### Pre-created Network Resources + +상위 디렉터리의 네트워크 준비 스크립트로 VPC/Subnet/SecurityGroup/KeyPair가 +미리 생성되어 있어야 합니다 (Connection 이름, 이미지/스펙, VPC/Subnet CIDR은 +`../README.md`를 참고). + +```bash +cd .. +./run-all-csp-network-prepare.sh +cd assign-publicip-true +``` + +### Required Tools + +- `bash` 3.2+ +- `curl` +- `jq` +- `ssh` client (OpenSSH) — for the SSH-login checks (initial + after re-assign) + +## Test Flow + +각 CSP에 대해 다음 순서로 검증합니다: + +1. **CreateVM** — `POST /spider/vm` (`ReqInfo.AssignPublicIP: true` 포함) — + `cb-spider-truepublicip-test` 인스턴스 생성 +2. **Poll Running** — `GET /spider/vmstatus/{Name}` — `Running` 상태가 될 + 때까지 폴링 (기본 15초 간격, 최대 1800초). `Failed` 상태이거나 타임아웃이면 + 즉시 FAIL +3. **GetVM** — `GET /spider/vm/{Name}` — `PrivateIP`/`PublicIP` 조회 +4. **Verify (has PublicIP)** — `PrivateIP`가 비어있으면 FAIL, `PublicIP`가 + 비어있으면 FAIL +5. **SSH check (initial)** — 초기 Public IP + 상위 디렉터리에서 준비한 + KeyPair의 PrivateKey로 SSH 접속 시도 (기본 10회 재시도, 30초 간격). 접속 + 실패(`FAIL`)는 물론, PrivateKey 파일이 없어서 시도 자체를 못한 경우 + (`NO_KEY`)와 Public IP가 비어 있어 시도할 수 없는 경우(`NO_IP`)도 접속 + 가능 여부를 확인할 수 없으므로 모두 전체 FAIL 처리 +6. **UnassignVMDefaultPublicIP** — `DELETE /vm/{Name}/publicip` — Public IP + 해제(+ 삭제) 요청 +7. **Verify (unassigned)** — `GetVM`으로 `PublicIP`가 비었는지 확인, 남아있으면 + FAIL +8. **AssignVMDefaultPublicIP** — `POST /vm/{Name}/publicip` — Public IP 자동 + 생성+재할당 요청 +9. **Verify (reassigned)** — `GetVM`으로 `PublicIP`가 다시 채워졌는지 확인, + 비어있으면 FAIL +10. **SSH check (reassigned)** — 재할당된 Public IP로 SSH 접속 재확인. 규칙은 + 4번과 동일 +11. 인스턴스는 자동 삭제되지 않습니다 — + `delete-all-csp-assign-publicip-true-vm.sh`로 별도 정리 + +## How to Run Tests + +### All CSPs in Parallel + +```bash +./run-all-csp-assign-publicip-true-tests.sh +``` + +**Example output:** +``` +====================================================================================================================================================================================================== + VM AssignPublicIP=true TEST SUMMARY - ALL CSPs +====================================================================================================================================================================================================== + +CSP | Result | VMStatus | PrivateIP | PubIP(init) | SSH | PubIP(unassign) | PubIP(reassign) | SSH2 | Elapsed | Reason +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +AWS | PASS | Running | 192.168.1.10 | 3.34.12.55 | OK | (none) | 15.164.10.20 | OK | 4m30s | - +AZURE | PASS | Running | 192.168.0.5 | 20.196.55.10 | OK | (none) | 20.196.60.30 | OK | 6m12s | - +... +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +Total: 10 PASS: 10 FAIL: 0 +``` + +### Individual CSP + +```bash +./aws-assign-publicip-true-test.sh +./azure-assign-publicip-true-test.sh +./gcp-assign-publicip-true-test.sh +./alibaba-assign-publicip-true-test.sh +./tencent-assign-publicip-true-test.sh +./ibm-assign-publicip-true-test.sh +./openstack-assign-publicip-true-test.sh +./ncp-assign-publicip-true-test.sh +./nhn-assign-publicip-true-test.sh +./kt-assign-publicip-true-test.sh +``` + +단독 실행 시 결과 디렉터리는 `RESULT_DIR` 환경변수로 지정하거나 기본값 +(`/tmp/vm_truepublicip_results`)이 사용됩니다. + +### Delete All Test VMs + +```bash +./delete-all-csp-assign-publicip-true-vm.sh +``` + +## Configuration + +```bash +export SPIDER_URL=http://localhost:1024 # CB-Spider REST API URL +export SPIDER_AUTH=admin:***** # Basic auth (admin:) +``` + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|--------------| +| `SPIDER_URL` | `http://localhost:1024` | CB-Spider REST API URL | +| `SPIDER_AUTH` | `admin:****` | Basic auth credentials | +| `MAX_WAIT_SEC` | `1800` (create) / `900` (delete) | Timeout per instance (seconds) | +| `POLL_INTERVAL` | `15` | Polling interval (seconds) | +| `KEY_DIR` | `/tmp/vm_publicip_keys` | Directory holding the PrivateKey saved by `../common-network-prepare.sh` (file: `${KEY_DIR}/${CONNECTION_NAME}.pem`) | +| `SSH_USER` | `cb-user` | OS login user for the SSH-login checks | +| `SSH_MAX_ATTEMPTS` | `10` | SSH connection retry count | +| `SSH_RETRY_INTERVAL` | `30` | Seconds between SSH retries | +| `VERBOSE` | `0` | `1`로 설정 시 CSP별 전체 로그 덤프 출력 | + +## Result Format + +결과 파일(`result_.txt`)은 파이프(|) 구분 11개 필드: + +``` +CSP|Result|VMStatus|PrivateIP|PublicIP(Initial)|SSH(Initial)|PublicIP(Unassigned)|PublicIP(Reassigned)|SSH(Reassigned)|Elapsed|Reason +``` + +| 필드 | 설명 | +|------|------| +| `CSP` | CSP 이름 (예: AWS) | +| `Result` | `PASS` / `FAIL` | +| `VMStatus` | 최종 확인된 상태 (예: `Running`), 실패 시 실패 당시 상태 | +| `PrivateIP` | 생성된 VM의 PrivateIP (`-`는 생성 실패로 조회 불가) | +| `PublicIP(Initial)` | 생성 직후(AssignPublicIP=true)의 PublicIP — 비어있지 않아야 함 | +| `SSH(Initial)` | 초기 PublicIP로의 SSH 접속 결과 — `OK`/`FAIL`/`NO_IP`/`NO_KEY` | +| `PublicIP(Unassigned)` | `UnassignVMDefaultPublicIP` 이후의 PublicIP — `(none)`이어야 함 | +| `PublicIP(Reassigned)` | `AssignVMDefaultPublicIP` 이후의 PublicIP — 비어있지 않아야 함 | +| `SSH(Reassigned)` | 재할당된 PublicIP로의 SSH 접속 결과 — `OK`/`FAIL`/`NO_IP`/`NO_KEY` | +| `Elapsed` | 경과 시간 | +| `Reason` | FAIL 사유 (예: PublicIP가 예상치 못하게 존재/부재, 타임아웃, Failed 상태, Assign/Unassign API 에러, SSH 실패 등) | + +## Script Structure + +``` +assign-publicip-true/ +├── run-all-csp-assign-publicip-true-tests.sh # Orchestrator: 전체 CSP 병렬 실행 +├── delete-all-csp-assign-publicip-true-vm.sh # Orchestrator: 전체 CSP 병렬 삭제 (../common-vm-delete.sh 재사용) +├── common-assign-publicip-true-test.sh # Common: Create -> Poll Running -> Verify -> SSH -> Unassign -> Verify -> Assign -> Verify -> SSH +├── aws-assign-publicip-true-test.sh +├── azure-assign-publicip-true-test.sh +├── gcp-assign-publicip-true-test.sh +├── alibaba-assign-publicip-true-test.sh +├── tencent-assign-publicip-true-test.sh +├── ibm-assign-publicip-true-test.sh +├── openstack-assign-publicip-true-test.sh +├── ncp-assign-publicip-true-test.sh +├── nhn-assign-publicip-true-test.sh +└── kt-assign-publicip-true-test.sh +``` + +## Logs & Results + +``` +/tmp/vm_truepublicip_test_/results/result_.txt +/tmp/vm_truepublicip_test_/logs/log_.txt +``` + +전체 삭제 실행 시: +``` +/tmp/vm_truepublicip_delete_results_/result_.txt +/tmp/vm_truepublicip_delete_logs_/log_.txt +``` + +## API Reference + +| Operation | Method | Path | +|-----------|--------|------| +| CreateVM | `POST` | `/spider/vm` | +| GetVMStatus | `GET` | `/spider/vmstatus/{Name}?ConnectionName=` | +| GetVM | `GET` | `/spider/vm/{Name}?ConnectionName=` | +| UnassignVMDefaultPublicIP | `DELETE` | `/spider/vm/{Name}/publicip` | +| AssignVMDefaultPublicIP | `POST` | `/spider/vm/{Name}/publicip` | +| DeleteVM | `DELETE` | `/spider/vm/{Name}` | + +## 시험 결과 + +### 2026-09-04 + +`./run-all-csp-assign-publicip-true-tests.sh` 전체 실행 (10개 CSP 병렬) — **10/10 PASS**. + +``` +CSP | Result | VMStatus | PrivateIP | PubIP(init) | SSH | PubIP(unassign) | PubIP(reassign) | SSH2 | Elapsed | Reason +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +AWS | PASS | Running | 192.168.1.154 | 3.25.115.56 | OK | (none) | 3.24.171.164 | OK | 1m55s | - +AZURE | PASS | Running | 192.168.0.4 | 40.82.134.16 | OK | (none) | 52.141.56.178 | OK | 1m36s | - +GCP | PASS | Running | 192.168.1.9 | 136.113.32.98 | OK | (none) | 34.136.155.127 | OK | 2m17s | - +ALIBABA | PASS | Running | 192.168.1.127 | 123.56.91.87 | OK | (none) | 39.96.59.58 | OK | 1m4s | - +TENCENT | PASS | Running | 192.168.1.9 | 101.43.140.61 | OK | (none) | 82.157.148.206 | OK | 1m37s | - +IBM | PASS | Running | 192.168.1.6 | 150.239.80.67 | OK | (none) | 169.63.102.8 | OK | 2m10s | - +OPENSTACK | PASS | Running | 192.168.1.39 | 183.111.177.156 | OK | (none) | 183.111.177.137 | OK | 3m26s | - +NCP | PASS | Running | 192.168.1.6 | 49.50.143.235 | OK | (none) | 49.50.143.236 | OK | 7m29s | - +NHN | PASS | Running | 192.168.1.101 | 103.218.159.89 | OK | (none) | 133.186.228.111 | OK | 3m22s | - +KT | PASS | Running | 10.29.102.141 | 210.104.76.98 | OK | (none) | 210.104.76.98 | OK | 4m53s | - +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +Total: 10 PASS: 10 FAIL: 0 +``` + +- 10개 CSP 전부 `AssignPublicIP=true`로 생성 → 생성 직후 PublicIP 할당 확인 → SSH 접속 확인 → `UnassignVMDefaultPublicIP` → PublicIP 해제 확인 → `AssignVMDefaultPublicIP` → PublicIP 재할당 확인 → SSH 재접속 확인까지 전 단계 통과. diff --git a/test/vm-default-publicip-test/assign-publicip-true/alibaba-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/alibaba-assign-publicip-true-test.sh new file mode 100755 index 000000000..0399ac8f4 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/alibaba-assign-publicip-true-test.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Alibaba VM AssignPublicIP=true Test Script +# Alibaba rotates its public Ubuntu image monthly (e.g. alibase_20260413.vhd -> +# alibase_20260501.vhd), so instead of a fixed ImageName this script resolves +# the lexicographically latest public image whose name starts with +# IMAGE_NAME_PREFIX via GET /spider/vmimage (same approach as +# ../alibaba-vm-publicip-test.sh). +# Author: CB-Spider Team + +export CSP_NAME="ALIBABA" +export CONNECTION_NAME="alibaba-beijing-config" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_alibaba.txt" + +SPIDER_URL="${SPIDER_URL:-http://localhost:1024}" +SPIDER_AUTH="${SPIDER_AUTH:-admin:****}" +IMAGE_NAME_PREFIX="ubuntu_24_04_x64_20G_alibase_" + +mkdir -p "$(dirname "${RESULT_FILE}")" + +echo "[${CSP_NAME}] Resolving latest public image with prefix '${IMAGE_NAME_PREFIX}'..." +image_list=$(curl -u "${SPIDER_AUTH}" -s \ + "${SPIDER_URL}/spider/vmimage?ConnectionName=${CONNECTION_NAME}" 2>&1) + +resolved_image=$(echo "${image_list}" | jq -r --arg p "${IMAGE_NAME_PREFIX}" ' + .image[]? | (.Name // .IId.NameId // "") | select(startswith($p)) +' 2>/dev/null | sort | tail -n1) + +if [[ -z "${resolved_image}" ]]; then + echo "[${CSP_NAME}] ERROR: no public image found with prefix '${IMAGE_NAME_PREFIX}'" + echo "${CSP_NAME}|FAIL|IMAGE_RESOLVE_ERROR|-|-|-|-|-|-|-|no public image found with prefix '${IMAGE_NAME_PREFIX}'" > "${RESULT_FILE}" + exit 1 +fi +echo "[${CSP_NAME}] Resolved image: ${resolved_image}" + +export CREATE_JSON="{ + \"ConnectionName\": \"alibaba-beijing-config\", + \"ReqInfo\": { + \"Name\": \"cb-spider-truepublicip-test\", + \"ImageName\": \"${resolved_image}\", + \"VPCName\": \"vpc-01\", + \"SubnetName\": \"subnet-01\", + \"SecurityGroupNames\": [\"sg-01\"], + \"VMSpecName\": \"ecs.c9i.large\", + \"KeyPairName\": \"keypair-01\", + \"AssignPublicIP\": true + } +}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/aws-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/aws-assign-publicip-true-test.sh new file mode 100755 index 000000000..f865be102 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/aws-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# AWS VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="AWS" +export CONNECTION_NAME="aws-config01" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_aws.txt" + +export CREATE_JSON='{ + "ConnectionName": "aws-config01", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "ami-0131a0fdbb6fda7e6", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "t2.micro", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/azure-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/azure-assign-publicip-true-test.sh new file mode 100755 index 000000000..3d9716c0d --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/azure-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Azure VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="AZURE" +export CONNECTION_NAME="azure-koreacentral-config" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_azure.txt" + +export CREATE_JSON='{ + "ConnectionName": "azure-koreacentral-config", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "Canonical:ubuntu-25_04-daily:minimal:25.04.202601140", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "Standard_B1ls", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/common-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/common-assign-publicip-true-test.sh new file mode 100755 index 000000000..43e9da8c6 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/common-assign-publicip-true-test.sh @@ -0,0 +1,268 @@ +#!/bin/bash + +# CB-Spider VM AssignPublicIP=true Test - Common Test Script +# Flow: Create VM with ReqInfo.AssignPublicIP=true -> Poll until Running -> +# Get VM Info -> Verify PrivateIP AND PublicIP are both set -> SSH check +# (initial) -> UnassignVMDefaultPublicIP -> Verify PublicIP is now empty +# -> AssignVMDefaultPublicIP -> Verify PublicIP is set again -> SSH check +# (re-assigned) -> Write result +# Author: CB-Spider Team +# +# Required env vars (set by per-CSP scripts): +# CSP_NAME - Display name (e.g., AWS) +# CONNECTION_NAME - Spider connection config name +# VM_NAME - VM instance name +# CREATE_JSON - JSON body for POST /spider/vm (must include "AssignPublicIP": true) +# RESULT_FILE - Path to write pipe-separated result line +# +# Optional env vars: +# SPIDER_URL - Spider REST API URL (default: http://localhost:1024) +# SPIDER_AUTH - Basic auth credentials (default: admin:****) +# MAX_WAIT_SEC - Max seconds to wait for Running after create (default: 1800) +# POLL_INTERVAL - Polling interval in seconds (default: 15) +# KEY_DIR - Directory holding the PrivateKey saved by +# ../common-network-prepare.sh (default: /tmp/vm_publicip_keys). +# The actual file is "${KEY_DIR}/${CONNECTION_NAME}.pem". +# SSH_USER - OS login user for the SSH-login check (default: cb-user) +# SSH_MAX_ATTEMPTS - SSH connection retry count (default: 10) +# SSH_RETRY_INTERVAL - Seconds between SSH retries (default: 30) +# +# Result file format (11 fields): +# CSP|Result|VMStatus|PrivateIP|PublicIP(Initial)|SSH(Initial)|PublicIP(Unassigned)|PublicIP(Reassigned)|SSH(Reassigned)|Elapsed|Reason + +format_elapsed() { + local sec=$1 + if [[ ${sec} -lt 60 ]]; then + echo "${sec}s" + else + echo "$((sec / 60))m$((sec % 60))s" + fi +} + +SPIDER_URL="${SPIDER_URL:-http://localhost:1024}" +SPIDER_AUTH="${SPIDER_AUTH:-admin:****}" +MAX_WAIT_SEC="${MAX_WAIT_SEC:-1800}" +POLL_INTERVAL="${POLL_INTERVAL:-15}" + +KEY_DIR="${KEY_DIR:-/tmp/vm_publicip_keys}" +KEY_FILE="${KEY_DIR}/${CONNECTION_NAME}.pem" +SSH_USER="${SSH_USER:-cb-user}" +SSH_MAX_ATTEMPTS="${SSH_MAX_ATTEMPTS:-10}" +SSH_RETRY_INTERVAL="${SSH_RETRY_INTERVAL:-30}" + +mkdir -p "$(dirname "${RESULT_FILE}")" + +# write_fail_result STATUS REASON +# Writes a FAIL result line and exits 1. Keeps the pipe-field count consistent +# (11 fields) so the summary table parses correctly. Fields already captured +# before the failing step (e.g. PrivateIP/initial PublicIP/SSH when a later +# step like Unassign fails) are preserved instead of being blanked to "-", +# so a FAIL row doesn't misleadingly look like VM creation itself failed. +write_fail_result() { + echo "${CSP_NAME}|FAIL|$1|${private_ip:--}|${initial_public_ip_display:--}|${ssh_initial:--}|${unassigned_public_ip_display:--}|${reassigned_public_ip_display:--}|${ssh_reassigned:--}|${elapsed_fmt:--}|$2" > "${RESULT_FILE}" + exit 1 +} + +# get_status -> prints current VMStatus (e.g. Running, Creating, ...) or "unknown" +get_status() { + local resp + resp=$(curl -u "${SPIDER_AUTH}" -s \ + "${SPIDER_URL}/spider/vmstatus/${VM_NAME}?ConnectionName=${CONNECTION_NAME}" 2>&1) + echo "${resp}" | jq -r '.Status // "unknown"' 2>/dev/null +} + +# get_public_ip -> prints current PublicIP field of GET /spider/vm/{Name} (may be empty) +get_public_ip() { + local resp + resp=$(curl -u "${SPIDER_AUTH}" -s \ + "${SPIDER_URL}/spider/vm/${VM_NAME}?ConnectionName=${CONNECTION_NAME}" 2>&1) + echo "${resp}" | jq -r '.PublicIP // empty' 2>/dev/null +} + +# check_ssh IP PHASE_LABEL -> prints one of: OK | FAIL | NO_IP | NO_KEY +# Retries the SSH login up to SSH_MAX_ATTEMPTS times (cloud-init needs time to +# provision the SSH_USER account after the PublicIP becomes reachable). +check_ssh() { + local ip="$1" phase="$2" + + if [[ -z "${ip}" || "${ip}" == "(none)" ]]; then + echo "[${CSP_NAME}] [${phase}] No PublicIP available - skipping SSH check." >&2 + echo "NO_IP" + return + fi + if [[ ! -f "${KEY_FILE}" ]]; then + echo "[${CSP_NAME}] [${phase}] No PrivateKey file at ${KEY_FILE} - skipping SSH check." >&2 + echo "NO_KEY" + return + fi + + local attempt + for ((attempt = 1; attempt <= SSH_MAX_ATTEMPTS; attempt++)); do + echo "[${CSP_NAME}] [${phase}] SSH attempt ${attempt}/${SSH_MAX_ATTEMPTS} -> ${SSH_USER}@${ip}" >&2 + if ssh -i "${KEY_FILE}" \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=10 \ + -o BatchMode=yes \ + -o LogLevel=ERROR \ + "${SSH_USER}@${ip}" "echo cb-spider-ssh-ok" 2>/dev/null | grep -q "cb-spider-ssh-ok"; then + echo "[${CSP_NAME}] [${phase}] SSH login OK (attempt ${attempt})" >&2 + echo "OK" + return + fi + [[ ${attempt} -lt SSH_MAX_ATTEMPTS ]] && sleep "${SSH_RETRY_INTERVAL}" + done + + echo "[${CSP_NAME}] [${phase}] SSH login FAILED after ${SSH_MAX_ATTEMPTS} attempts." >&2 + echo "FAIL" +} + +start_time=$(date +%s) +timestamp=$(date '+%Y-%m-%d %H:%M:%S') + +echo "[${CSP_NAME}] [${timestamp}] Creating VM '${VM_NAME}' with AssignPublicIP=true..." + +# ── 1) Create VM ────────────────────────────────────────────────────────────── +create_resp=$(curl -u "${SPIDER_AUTH}" -sX POST "${SPIDER_URL}/spider/vm" \ + -H 'Content-Type: application/json' \ + -d "${CREATE_JSON}" 2>&1) + +err_msg=$(echo "${create_resp}" | jq -r '.message // empty' 2>/dev/null) +if [[ -n "${err_msg}" ]]; then + echo "[${CSP_NAME}] ERROR on create: ${err_msg}" + write_fail_result "CREATE_ERROR" "${err_msg}" +fi +echo "[${CSP_NAME}] Create request accepted." + +# ── 2) Wait for Running ─────────────────────────────────────────────────────── +echo "[${CSP_NAME}] Waiting for VM to become Running (poll every ${POLL_INTERVAL}s, max ${MAX_WAIT_SEC}s)..." + +elapsed=0 +final_status="unknown" +while true; do + sleep "${POLL_INTERVAL}" + elapsed=$((elapsed + POLL_INTERVAL)) + + cur_status=$(get_status) + status_lower=$(echo "${cur_status}" | tr '[:upper:]' '[:lower:]') + echo "[${CSP_NAME}] Status: ${cur_status} (elapsed: ${elapsed}s)" + + if [[ "${status_lower}" == "running" ]]; then + final_status="${cur_status}" + break + fi + if [[ "${status_lower}" == "failed" ]]; then + elapsed_fmt=$(format_elapsed "${elapsed}") + echo "[${CSP_NAME}] VM entered Failed state." + write_fail_result "${cur_status}" "VM entered Failed state instead of Running" + fi + if [[ ${elapsed} -ge ${MAX_WAIT_SEC} ]]; then + elapsed_fmt=$(format_elapsed "${elapsed}") + echo "[${CSP_NAME}] TIMEOUT: VM did not reach Running within ${MAX_WAIT_SEC}s (last status '${cur_status}')" + write_fail_result "${cur_status}" "Timed out waiting for Running" + fi +done +echo "[${CSP_NAME}] VM is Running." + +# ── 3) Get VM Info, check PrivateIP / PublicIP ─────────────────────────────── +info=$(curl -u "${SPIDER_AUTH}" -s \ + "${SPIDER_URL}/spider/vm/${VM_NAME}?ConnectionName=${CONNECTION_NAME}" 2>&1) + +private_ip=$(echo "${info}" | jq -r '.PrivateIP // empty' 2>/dev/null) +initial_public_ip=$(echo "${info}" | jq -r '.PublicIP // empty' 2>/dev/null) + +private_ip_display="${private_ip:-(none)}" +initial_public_ip_display="${initial_public_ip:-(none)}" + +echo "[${CSP_NAME}] PrivateIP=${private_ip_display} PublicIP=${initial_public_ip_display}" + +# ── 4) Verify: must have a PrivateIP AND must have a PublicIP ─────────────── +if [[ -z "${private_ip}" ]]; then + echo "[${CSP_NAME}] FAIL: PrivateIP is empty." + write_fail_result "${final_status}" "PrivateIP is empty" +fi + +if [[ -z "${initial_public_ip}" ]]; then + echo "[${CSP_NAME}] FAIL: PublicIP was not assigned even though AssignPublicIP=true was requested." + write_fail_result "${final_status}" "PublicIP unexpectedly absent" +fi + +echo "[${CSP_NAME}] OK: VM Running with PrivateIP=${private_ip} and PublicIP=${initial_public_ip}." + +# ── 5) SSH check via the initial PublicIP ──────────────────────────────────── +ssh_initial=$(check_ssh "${initial_public_ip}" "initial") +echo "[${CSP_NAME}] SSH check (initial) result: ${ssh_initial}" + +if [[ "${ssh_initial}" != "OK" ]]; then + write_fail_result "${final_status}" "SSH check after create did not succeed: ${ssh_initial}" +fi + +# ── 6) UnassignVMDefaultPublicIP ───────────────────────────────────────────── +echo "[${CSP_NAME}] Requesting UnassignVMDefaultPublicIP..." +unassign_resp=$(curl -u "${SPIDER_AUTH}" -sX DELETE "${SPIDER_URL}/spider/vm/${VM_NAME}/publicip" \ + -H 'Content-Type: application/json' \ + -d "{\"ConnectionName\": \"${CONNECTION_NAME}\"}" 2>&1) + +unassign_err=$(echo "${unassign_resp}" | jq -r '.message // empty' 2>/dev/null) +if [[ -n "${unassign_err}" ]]; then + echo "[${CSP_NAME}] ERROR on UnassignVMDefaultPublicIP: ${unassign_err}" + write_fail_result "${final_status}" "UnassignVMDefaultPublicIP error: ${unassign_err}" +fi +unassign_result=$(echo "${unassign_resp}" | jq -r '.Result // empty' 2>/dev/null) +if [[ "${unassign_result}" != "true" ]]; then + echo "[${CSP_NAME}] FAIL: UnassignVMDefaultPublicIP did not return Result=true (got '${unassign_result}')." + write_fail_result "${final_status}" "UnassignVMDefaultPublicIP returned Result=${unassign_result}" +fi + +# ── 7) Confirm PublicIP is no longer assigned ──────────────────────────────── +unassigned_public_ip=$(get_public_ip) +unassigned_public_ip_display="${unassigned_public_ip:-(none)}" +echo "[${CSP_NAME}] PublicIP after UnassignVMDefaultPublicIP: ${unassigned_public_ip_display}" + +if [[ -n "${unassigned_public_ip}" ]]; then + echo "[${CSP_NAME}] FAIL: PublicIP still present after UnassignVMDefaultPublicIP." + write_fail_result "${final_status}" "PublicIP unexpectedly present after Unassign: ${unassigned_public_ip}" +fi + +# ── 8) AssignVMDefaultPublicIP ──────────────────────────────────────────────── +echo "[${CSP_NAME}] Requesting AssignVMDefaultPublicIP..." +assign_resp=$(curl -u "${SPIDER_AUTH}" -sX POST "${SPIDER_URL}/spider/vm/${VM_NAME}/publicip" \ + -H 'Content-Type: application/json' \ + -d "{\"ConnectionName\": \"${CONNECTION_NAME}\"}" 2>&1) + +assign_err=$(echo "${assign_resp}" | jq -r '.message // empty' 2>/dev/null) +if [[ -n "${assign_err}" ]]; then + echo "[${CSP_NAME}] ERROR on AssignVMDefaultPublicIP: ${assign_err}" + write_fail_result "${final_status}" "AssignVMDefaultPublicIP error: ${assign_err}" +fi + +# ── 9) Confirm PublicIP was re-assigned ────────────────────────────────────── +reassigned_public_ip=$(get_public_ip) +reassigned_public_ip_display="${reassigned_public_ip:-(none)}" +echo "[${CSP_NAME}] PublicIP after AssignVMDefaultPublicIP: ${reassigned_public_ip_display}" + +if [[ -z "${reassigned_public_ip}" ]]; then + echo "[${CSP_NAME}] FAIL: PublicIP was not assigned after AssignVMDefaultPublicIP." + write_fail_result "${final_status}" "AssignVMDefaultPublicIP did not result in a PublicIP" +fi + +# ── 10) SSH check via the re-assigned PublicIP ─────────────────────────────── +ssh_reassigned=$(check_ssh "${reassigned_public_ip}" "reassigned") +echo "[${CSP_NAME}] SSH check (reassigned) result: ${ssh_reassigned}" + +# Anything other than a confirmed OK means SSH reachability couldn't be +# verified (including NO_KEY - a missing PrivateKey file leaves the result +# unknown, not passing), so it fails the test. +if [[ "${ssh_reassigned}" != "OK" ]]; then + write_fail_result "${final_status}" "SSH check after AssignVMDefaultPublicIP did not succeed: ${ssh_reassigned}" +fi + +end_time=$(date +%s) +elapsed_total=$((end_time - start_time)) +elapsed_fmt=$(format_elapsed "${elapsed_total}") + +echo "[${CSP_NAME}] PASS: full AssignPublicIP=true + Unassign/Assign lifecycle succeeded (${elapsed_fmt})" + +# Format: CSP|Result|VMStatus|PrivateIP|PublicIP(Initial)|SSH(Initial)|PublicIP(Unassigned)|PublicIP(Reassigned)|SSH(Reassigned)|Elapsed|Reason +echo "${CSP_NAME}|PASS|${final_status}|${private_ip}|${initial_public_ip_display}|${ssh_initial}|${unassigned_public_ip_display}|${reassigned_public_ip_display}|${ssh_reassigned}|${elapsed_fmt}|-" \ + > "${RESULT_FILE}" diff --git a/test/vm-default-publicip-test/assign-publicip-true/delete-all-csp-assign-publicip-true-vm.sh b/test/vm-default-publicip-test/assign-publicip-true/delete-all-csp-assign-publicip-true-vm.sh new file mode 100755 index 000000000..234376a6e --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/delete-all-csp-assign-publicip-true-vm.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +# CB-Spider VM AssignPublicIP=true Test - VM Delete Runner for All CSPs +# Terminates the 'cb-spider-truepublicip-test' VM on all 10 CSPs in parallel +# and waits for removal. Reuses ../common-vm-delete.sh (the same delete logic +# as the parent vm-default-publicip-test suite). +# Author: CB-Spider Team +# Note: Written for bash 3.2+ compatibility (macOS default shell) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PARENT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +export SPIDER_URL="${SPIDER_URL:-http://localhost:1024}" +export SPIDER_AUTH="${SPIDER_AUTH:-admin:****}" +export MAX_WAIT_SEC="${MAX_WAIT_SEC:-900}" +export POLL_INTERVAL="${POLL_INTERVAL:-15}" + +export RESULT_DIR="/tmp/vm_truepublicip_delete_results_$$" +LOG_DIR="/tmp/vm_truepublicip_delete_logs_$$" +mkdir -p "${RESULT_DIR}" "${LOG_DIR}" + +to_lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; } + +csp_connection() { + case "$1" in + AWS) echo "aws-config01" ;; + AZURE) echo "azure-koreacentral-config" ;; + GCP) echo "gcp-iowa-config" ;; + ALIBABA) echo "alibaba-beijing-config" ;; + TENCENT) echo "tencent-beijing3-config" ;; + IBM) echo "ibm-us-east-1-config" ;; + OPENSTACK) echo "openstack-config01" ;; + NCP) echo "ncp-korea1-config" ;; + NHN) echo "nhn-korea-pangyo1-config" ;; + KT) echo "kt-mokdong1-config" ;; + esac +} + +print_separator() { echo "------------------------------------------------------------"; } + +echo "" +echo "################################################################################" +echo "# CB-Spider VM AssignPublicIP=true Test - VM Delete - All CSPs #" +echo "################################################################################" +echo "" +echo "Spider URL : ${SPIDER_URL}" +echo "Result dir : ${RESULT_DIR}" +echo "Log dir : ${LOG_DIR}" +echo "" +echo "Launching VM deletion on all CSPs in parallel..." +echo "" + +CSP_ORDER="AWS AZURE GCP ALIBABA TENCENT IBM OPENSTACK NCP NHN KT" + +for csp in ${CSP_ORDER}; do + conn=$(csp_connection "${csp}") + log_file="${LOG_DIR}/log_$(to_lower "${csp}").txt" + echo "[MAIN] Starting ${csp} VM delete (log: ${log_file})" + ( + export CSP_NAME="${csp}" + export CONNECTION_NAME="${conn}" + export VM_NAME="cb-spider-truepublicip-test" + export RESULT_FILE="${RESULT_DIR}/result_$(to_lower "${csp}").txt" + "${PARENT_DIR}/common-vm-delete.sh" + ) > "${log_file}" 2>&1 & + echo $! > "${LOG_DIR}/pid_${csp}.txt" +done + +echo "" +echo "[MAIN] All CSP VM deletions launched. Waiting for completion..." +echo "" + +for csp in ${CSP_ORDER}; do + pid=$(cat "${LOG_DIR}/pid_${csp}.txt" 2>/dev/null) + if [[ -n "${pid}" ]]; then + wait "${pid}" + exit_code=$? + if [[ ${exit_code} -eq 0 ]]; then + echo "[MAIN] ${csp} completed successfully" + else + echo "[MAIN] ${csp} finished with exit code ${exit_code} (check ${LOG_DIR}/log_$(to_lower "${csp}").txt)" + fi + fi +done + +echo "" +echo "[MAIN] All CSP VM deletions finished. Collecting results..." +echo "" + +echo "============================================================" +echo " VM DELETE SUMMARY - ALL CSPs (AssignPublicIP=true)" +echo "============================================================" +echo "" +printf "%-12s | %-15s | %-20s | %-10s\n" "CSP" "Result" "Detail" "Elapsed" +print_separator + +for csp in ${CSP_ORDER}; do + result_file="${RESULT_DIR}/result_$(to_lower "${csp}").txt" + + if [[ -f "${result_file}" ]]; then + IFS='|' read -r r_csp r_result r_detail r_elapsed < "${result_file}" + else + r_csp="${csp}" + r_result="NO_RESULT" + r_detail="-" + r_elapsed="-" + fi + + printf "%-12s | %-15s | %-20s | %-10s\n" "${r_csp}" "${r_result}" "${r_detail}" "${r_elapsed}" +done + +print_separator +echo "" +echo "Logs : ${LOG_DIR}/" +echo "Results: ${RESULT_DIR}/" +echo "" +echo "============================================================" +echo "" + +if [[ "${VERBOSE:-0}" == "1" ]]; then + echo "" + for csp in ${CSP_ORDER}; do + log_file="${LOG_DIR}/log_$(to_lower "${csp}").txt" + echo "────────────── ${csp} ──────────────" + [[ -f "${log_file}" ]] && cat "${log_file}" || echo "(no log)" + echo "" + done +fi diff --git a/test/vm-default-publicip-test/assign-publicip-true/gcp-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/gcp-assign-publicip-true-test.sh new file mode 100755 index 000000000..2153f448a --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/gcp-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# GCP VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="GCP" +export CONNECTION_NAME="gcp-iowa-config" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_gcp.txt" + +export CREATE_JSON='{ + "ConnectionName": "gcp-iowa-config", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-2404-noble-amd64-v20240423", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "e2-standard-2", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/ibm-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/ibm-assign-publicip-true-test.sh new file mode 100755 index 000000000..75eed7622 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/ibm-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# IBM VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="IBM" +export CONNECTION_NAME="ibm-us-east-1-config" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_ibm.txt" + +export CREATE_JSON='{ + "ConnectionName": "ibm-us-east-1-config", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "r014-1696a049-e959-493d-9a97-1655ef4c942e", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "bx2-2x8", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/kt-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/kt-assign-publicip-true-test.sh new file mode 100755 index 000000000..04208b050 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/kt-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# KT Cloud VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="KT" +export CONNECTION_NAME="kt-mokdong1-config" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_kt.txt" + +export CREATE_JSON='{ + "ConnectionName": "kt-mokdong1-config", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "1a772df6-262e-43a7-896f-98fa23d715c7", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "4x8.itl", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/ncp-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/ncp-assign-publicip-true-test.sh new file mode 100755 index 000000000..68a69c6b1 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/ncp-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# NCP VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="NCP" +export CONNECTION_NAME="ncp-korea1-config" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_ncp.txt" + +export CREATE_JSON='{ + "ConnectionName": "ncp-korea1-config", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "104630229", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "s2-g3", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/nhn-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/nhn-assign-publicip-true-test.sh new file mode 100755 index 000000000..93f0b3fdc --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/nhn-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# NHN Cloud VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="NHN" +export CONNECTION_NAME="nhn-korea-pangyo1-config" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_nhn.txt" + +export CREATE_JSON='{ + "ConnectionName": "nhn-korea-pangyo1-config", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "5396655e-166a-4875-80d2-ed8613aa054f", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "m2.c4m8", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/openstack-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/openstack-assign-publicip-true-test.sh new file mode 100755 index 000000000..49f182e58 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/openstack-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# OpenStack VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="OPENSTACK" +export CONNECTION_NAME="openstack-config01" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_openstack.txt" + +export CREATE_JSON='{ + "ConnectionName": "openstack-config01", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "78d90dae-d21d-4606-a9dd-c1268e321864", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "m1.small", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh" diff --git a/test/vm-default-publicip-test/assign-publicip-true/run-all-csp-assign-publicip-true-tests.sh b/test/vm-default-publicip-test/assign-publicip-true/run-all-csp-assign-publicip-true-tests.sh new file mode 100755 index 000000000..d97d9af27 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/run-all-csp-assign-publicip-true-tests.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +# CB-Spider VM AssignPublicIP=true Test Runner for All CSPs +# For each CSP: creates a VM with ReqInfo.AssignPublicIP=true, waits for +# Running, verifies PrivateIP AND PublicIP are both set, SSH-checks the +# initial PublicIP, then exercises the default-PublicIP lifecycle in reverse: +# UnassignVMDefaultPublicIP -> verify PublicIP is now empty -> +# AssignVMDefaultPublicIP -> verify PublicIP is set again -> SSH check via +# the re-assigned PublicIP. +# FAIL on any step failing (VM never reaches Running, a PublicIP present/absent +# when it shouldn't be, Assign/Unassign API errors, or an SSH login failure). +# All CSPs run concurrently. A unified result table is shown at the end. +# +# Author: CB-Spider Team +# Note: Written for bash 3.2+ compatibility (macOS default shell) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── Configuration ───────────────────────────────────────────────────────────── +export SPIDER_URL="${SPIDER_URL:-http://localhost:1024}" +export SPIDER_AUTH="${SPIDER_AUTH:-admin:****}" +export MAX_WAIT_SEC="${MAX_WAIT_SEC:-1800}" +export POLL_INTERVAL="${POLL_INTERVAL:-15}" + +BASE_DIR="/tmp/vm_truepublicip_test_$$" +export RESULT_DIR="${BASE_DIR}/results" +LOG_DIR="${BASE_DIR}/logs" +mkdir -p "${RESULT_DIR}" "${LOG_DIR}" + +# ── Helpers ─────────────────────────────────────────────────────────────────── +to_lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; } + +csp_script() { + case "$1" in + AWS) echo "aws-assign-publicip-true-test.sh" ;; + AZURE) echo "azure-assign-publicip-true-test.sh" ;; + GCP) echo "gcp-assign-publicip-true-test.sh" ;; + ALIBABA) echo "alibaba-assign-publicip-true-test.sh" ;; + TENCENT) echo "tencent-assign-publicip-true-test.sh" ;; + IBM) echo "ibm-assign-publicip-true-test.sh" ;; + OPENSTACK) echo "openstack-assign-publicip-true-test.sh" ;; + NCP) echo "ncp-assign-publicip-true-test.sh" ;; + NHN) echo "nhn-assign-publicip-true-test.sh" ;; + KT) echo "kt-assign-publicip-true-test.sh" ;; + esac +} + +print_separator() { + printf '%194s\n' '' | tr ' ' '-' +} + +print_header() { + echo "" + printf '%194s\n' '' | tr ' ' '=' + echo " VM AssignPublicIP=true TEST SUMMARY - ALL CSPs" + printf '%194s\n' '' | tr ' ' '=' + echo "" + printf "%-11s | %-6s | %-10s | %-15s | %-15s | %-6s | %-15s | %-15s | %-6s | %-8s | %-s\n" \ + "CSP" "Result" "VMStatus" "PrivateIP" "PubIP(init)" "SSH" "PubIP(unassign)" "PubIP(reassign)" "SSH2" "Elapsed" "Reason" + print_separator +} + +# ── Banner ───────────────────────────────────────────────────────────────────── +echo "" +echo "################################################################################" +echo "# CB-Spider VM AssignPublicIP=true Test - Starting All CSPs #" +echo "################################################################################" +echo "" +echo "Spider URL : ${SPIDER_URL}" +echo "Max wait : ${MAX_WAIT_SEC}s per CSP" +echo "Poll interval: ${POLL_INTERVAL}s" +echo "Base dir : ${BASE_DIR}" +echo "" +echo "Launching all CSP tests in parallel..." +echo "" + +CSP_ORDER="AWS AZURE GCP ALIBABA TENCENT IBM OPENSTACK NCP NHN KT" + +# ── Launch all CSP scripts in background ───────────────────────────────────── +for csp in ${CSP_ORDER}; do + script=$(csp_script "${csp}") + log_file="${LOG_DIR}/log_$(to_lower "${csp}").txt" + echo "[MAIN] Starting ${csp} (log: ${log_file})" + "${SCRIPT_DIR}/${script}" > "${log_file}" 2>&1 & + echo $! > "${LOG_DIR}/pid_${csp}.txt" +done + +echo "" +echo "[MAIN] All CSP tests launched. Waiting for completion..." +echo "[MAIN] Monitor: tail -f ${LOG_DIR}/log_.txt" +echo "" + +# ── Wait for all CSP background jobs ───────────────────────────────────────── +for csp in ${CSP_ORDER}; do + pid=$(cat "${LOG_DIR}/pid_${csp}.txt" 2>/dev/null) + if [[ -n "${pid}" ]]; then + wait "${pid}" + exit_code=$? + if [[ ${exit_code} -eq 0 ]]; then + echo "[MAIN] ${csp} completed successfully" + else + echo "[MAIN] ${csp} finished with exit code ${exit_code} (check ${LOG_DIR}/log_$(to_lower "${csp}").txt)" + fi + fi +done + +echo "" +echo "[MAIN] All CSP tests finished. Collecting results..." +echo "" + +# ── Print result table ──────────────────────────────────────────────────────── +print_header + +pass_count=0 +fail_count=0 + +for csp in ${CSP_ORDER}; do + result_file="${RESULT_DIR}/result_$(to_lower "${csp}").txt" + + if [[ -f "${result_file}" ]]; then + IFS='|' read -r r_csp r_result r_status r_priv r_pub_init r_ssh_init r_pub_unassign r_pub_reassign r_ssh_reassign r_elapsed r_reason \ + < "${result_file}" + else + r_csp="${csp}" + r_result="NO_RESULT" + r_status="-" + r_priv="-" + r_pub_init="-" + r_ssh_init="-" + r_pub_unassign="-" + r_pub_reassign="-" + r_ssh_reassign="-" + r_elapsed="-" + r_reason="script crashed before writing a result" + fi + + printf "%-11s | %-6s | %-10s | %-15s | %-15s | %-6s | %-15s | %-15s | %-6s | %-8s | %-s\n" \ + "${r_csp}" "${r_result}" "${r_status}" "${r_priv}" "${r_pub_init}" "${r_ssh_init}" "${r_pub_unassign}" "${r_pub_reassign}" "${r_ssh_reassign}" "${r_elapsed}" "${r_reason}" + + if [[ "${r_result}" == "PASS" ]]; then + pass_count=$((pass_count + 1)) + else + fail_count=$((fail_count + 1)) + fi +done + +print_separator +echo "" +echo "Total: $((pass_count + fail_count)) PASS: ${pass_count} FAIL: ${fail_count}" +echo "" +echo "Logs : ${LOG_DIR}/" +echo "Results: ${RESULT_DIR}/" +echo "" +echo "Legend : PASS requires, in order: VMStatus=Running with a non-empty" +echo " PrivateIP and a non-empty PublicIP right after create (since" +echo " AssignPublicIP=true), SSH-reachable via that initial PublicIP;" +echo " UnassignVMDefaultPublicIP succeeds and PublicIP goes back to" +echo " empty; AssignVMDefaultPublicIP results in a non-empty PublicIP" +echo " again that is SSH-reachable. Any step failing (create error," +echo " timeout, VM entered Failed, PublicIP present/absent when it" +echo " shouldn't be, Assign/Unassign API errors, or SSH not confirmed" +echo " OK) is FAIL." +echo " SSH columns: OK (login succeeded) is the only passing value - FAIL" +echo " (login failed after retries), NO_IP, and NO_KEY (PrivateKey file" +echo " missing, see KEY_DIR) all fail the test since reachability could" +echo " not be confirmed." +echo "" +printf '%194s\n' '' | tr ' ' '=' +echo "" + +# ── Per-CSP full log dump (optional, controlled by VERBOSE=1) ──────────────── +if [[ "${VERBOSE:-0}" == "1" ]]; then + echo "" + echo "################################################################################" + echo "# Per-CSP Detailed Logs #" + echo "################################################################################" + for csp in ${CSP_ORDER}; do + log_file="${LOG_DIR}/log_$(to_lower "${csp}").txt" + echo "" + echo "────────────────────────────── ${csp} ──────────────────────────────" + if [[ -f "${log_file}" ]]; then + cat "${log_file}" + else + echo "(no log)" + fi + done +fi + +# Propagate failure to caller so a non-zero FAIL count fails this step +[[ ${fail_count} -eq 0 ]] diff --git a/test/vm-default-publicip-test/assign-publicip-true/tencent-assign-publicip-true-test.sh b/test/vm-default-publicip-test/assign-publicip-true/tencent-assign-publicip-true-test.sh new file mode 100755 index 000000000..e645d58f5 --- /dev/null +++ b/test/vm-default-publicip-test/assign-publicip-true/tencent-assign-publicip-true-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Tencent VM AssignPublicIP=true Test Script +# Author: CB-Spider Team + +export CSP_NAME="TENCENT" +export CONNECTION_NAME="tencent-beijing3-config" +export VM_NAME="cb-spider-truepublicip-test" +export RESULT_FILE="${RESULT_DIR:-/tmp/vm_truepublicip_results}/result_tencent.txt" + +export CREATE_JSON='{ + "ConnectionName": "tencent-beijing3-config", + "ReqInfo": { + "Name": "cb-spider-truepublicip-test", + "ImageName": "img-pi0ii46r", + "VPCName": "vpc-01", + "SubnetName": "subnet-01", + "SecurityGroupNames": ["sg-01"], + "VMSpecName": "S5.MEDIUM8", + "KeyPairName": "keypair-01", + "AssignPublicIP": true + } +}' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"${SCRIPT_DIR}/common-assign-publicip-true-test.sh"