클러스터 및 SVM 피어링

이 페이지에서는 Google Cloud NetApp Volumes에서 ONTAP 스토리지 풀의 피어링을 설정하는 방법을 설명합니다.

클러스터 피어링 및 스토리지 가상 머신 (SVM) 피어링은 Flex Unified ONTAP 스토리지 풀을 다른 클러스터에 연결합니다. 이를 통해 SnapMirror 및 FlexCache와 같은 서비스를 실행할 수 있습니다.

peer_cluster.py 스크립트를 사용하여 제안을 수락하고 ONTAP 모드 풀의 암호를 입력합니다. 자세한 내용은 클러스터 피어링 스크립트를 참고하세요.

절차 선택

설정에 따라 다음 피어링 절차 중 하나를 선택합니다.

  • 하이브리드 피어링: ONTAP 모드 풀에서 기본 모드 NetApp Volumes 볼륨 (Standard, Premium, Extreme 또는 Flex Unified 기본 모드)으로 마이그레이션하거나 복제합니다. NetApp Volumes가 대상에서 피어링을 시작합니다. ONTAP 모드 소스 풀에서 피어링을 완료합니다.

  • 수동 피어링: 직접 피어링을 설정하고 하이브리드 마이그레이션 또는 외부 복제 워크플로를 사용하지 않습니다. 시나리오를 선택합니다.

    • ONTAP 모드 풀 및 온프레미스 또는 NetApp Volumes 외부에 있는 외부 ONTAP 클러스터

    • ONTAP 모드 풀 2개

외부 ONTAP 클러스터의 추가 피어링 옵션에 대한 자세한 내용은 NetApp ONTAP 문서를 참고하세요.

ONTAP 모드 풀에서 클러스터 피어링이 작동하는 방식

클러스터 피어링 프로세스는 제안 및 수락 워크플로를 따릅니다.

  • 제공: 한 클러스터가 피어 IC-LIF 주소에 대한 피어링을 시작하고 비밀번호를 수신합니다.

  • 수락: 피어 클러스터가 제공 클러스터의 IC-LIF 주소와 비밀번호를 사용하여 피어링을 완료합니다.

ONTAP CLI 프록시는 대화형이 아닙니다. cluster peer create -generate-passphrase 명령어는 입력을 묻지 않으므로 ontap_execute를 사용하여 ONTAP 모드 풀에서 제안을 실행할 수 있습니다. 모든 원격 IC-LIF 주소를 쉼표로 구분된 -peer-addrs 값으로 나열합니다. cluster peer accept 명령어는 비밀번호를 묻는 메시지를 표시하므로 프록시를 통해 이 단계가 실패합니다. ONTAP 모드 풀이 제안을 수락해야 하는 경우 peer_cluster.py을 사용합니다.

클러스터를 피어링하려면 다음 시나리오 중 하나를 사용하세요.

  • 시나리오 A (ONTAP 모드 및 외부 ONTAP): 모든 피어 IC-LIF 주소와 함께 ontap_execute를 사용하여 ONTAP 모드 풀에서 cluster peer create-generate-passphrase를 실행합니다. 외부 ONTAP 관리자는 모든 ONTAP 모드 IC-LIF 주소와 함께 클러스터 프롬프트에서 cluster peer accept를 실행하고 암호를 입력합니다.

  • 시나리오 B (ONTAP 모드 풀 2개): ontap_execute를 사용하여 풀 A에서 cluster peer create-generate-passphrase를 실행한 다음 풀 B에서 PEER_MODE=accept를 사용하여 peer_cluster.py를 실행합니다.

ONTAP 모드 풀에서 ontap_execute를 사용하여 vserver peer accept를 실행하여 SVM 피어링을 수행합니다. 시나리오 A에서 외부 ONTAP 클러스터는 vserver peer create를 실행합니다.

피어링이 필요한 복제 방법

다음 복제 방법에는 피어링이 필요합니다.

  • 모든 ONTAP 소스에서 Flex Unified ONTAP 모드로 (SnapMirror): ONTAP 모드 풀에서 피어링합니다.

  • ONTAP 대상에 Flex Unified ONTAP 모드 (SnapMirror): ONTAP 모드 풀에서 피어링합니다.

  • ONTAP 모드 소스에서 기본 모드 NetApp Volumes로 (볼륨 이전 또는 외부 복제): ONTAP 모드 소스 풀에서 하이브리드 피어링을 사용합니다.

NetApp Volumes 내의 기본 모드 볼륨 복제는 ONTAP 클러스터 피어링을 사용하지 않습니다.

하이브리드 피어링

소스가 ONTAP 모드 풀이고 대상이 NetApp Volumes의 기본 모드 볼륨인 경우 이 섹션을 사용합니다.

NetApp Volumes 작동 방식

하이브리드 피어링 워크플로에는 다음 단계가 포함됩니다.

  1. Google Cloud 콘솔에서 또는 Google Cloud CLI를 사용하여 대상 볼륨에 볼륨 마이그레이션 또는 외부 복제를 만듭니다. ONTAP 모드 소스 클러스터 이름, SVM, 볼륨, 소스 IC-LIF 주소를 지정합니다.

  2. 클러스터 피어링이 아직 설정되지 않은 경우 NetApp Volumes는 대상의 연결을 확인하고, 비밀번호를 생성하고, 피어링 안내를 표시합니다.

  3. NetApp Volumes는 소스 풀에서 자동화된 명령어를 실행하지 않으므로 ONTAP 모드 소스 풀에서 나머지 피어링 단계를 실행해야 합니다.

이 순서는 되돌릴 수 없습니다. 먼저 기본 모드 대상을 만들어야 합니다.

ONTAP 모드 소스에서 클러스터 피어링 완료

복제 또는 마이그레이션 탭의 Google Cloud 콘솔에서 또는 Google Cloud CLI를 사용하여 비밀번호와 대상 IC-LIF 주소를 가져옵니다.

 gcloud netapp volumes replications list --volume=DESTINATION_VOLUME --location=LOCATION \
  --format="table(hybridPeeringDetails.command,hybridPeeringDetails.passphrase,hybridPeeringDetails.subnetIp)"

ONTAP 모드 소스 풀에서 NetApp Volumes가 대상에 제공한 피어링을 수락합니다. 일반적인 ONTAP 모드 절차에 설명된 대로 ontap_execute를 설정합니다. 소스 풀의 POOL_NAMELOCATION을 설정합니다. PEER_IC_LIF_IPS에서 모든 대상 IC-LIF 주소를 나열한 다음 PEER_MODE=accept를 사용하여 peer_cluster.py를 실행합니다.

 export PROJECT_ID="myproject"
 export LOCATION="us-central1-a"
 export POOL_NAME="ontap-mode-source-pool"
 export PEER_MODE="accept"
 export PEER_IC_LIF_IPS="DESTINATION_IC_LIF_IP_1,DESTINATION_IC_LIF_IP_2"
 export PASSPHRASE="passphrase-from-gcnv-console"

 python3 peer_cluster.py

소스 풀에서 구성을 확인합니다.

 ontap_execute "cluster peer show"

ONTAP 모드 소스에서 SVM 피어링 완료

NetApp Volumes에서 SVM 피어링을 자동으로 완료하지 않으면 vserver peer 명령어가 표시됩니다. ontap_execute를 사용하여 명령어를 실행하거나 대기 중인 피어를 수락합니다.

 ontap_execute "vserver peer accept -vserver SOURCE_SVM_NAME -peer-vserver PEER_SVM_NAME"

구성 확인

 ontap_execute "vserver peer show"

Google Cloud 콘솔에서 하이브리드 워크플로로 돌아가거나 gcloud netapp volumes replications list을 실행하여 복제 또는 마이그레이션이 진행 중인지 확인합니다.

수동 피어링

하이브리드 워크플로 없이 피어링을 설정하는 경우(예: ONTAP 모드 풀과 외부 ONTAP 클러스터 간 또는 두 ONTAP 모드 풀 간 SnapMirror 전) 이 섹션을 사용합니다.

시작하기 전에

다음 기본 요건을 완료합니다.

  1. ONTAP 모드 풀의 IC-LIF가 피어 클러스터의 IC-LIF에 도달하는지 확인합니다. ONTAP 모드 풀의 IC-LIF는 IPspace Gcnv에 있고 외부 ONTAP 클러스터의 IC-LIF는 IPspace Default에 있습니다. ICMP 및 필수 클러스터 간 포트를 테스트합니다.

  2. 시작하기 전에 ontap_execute를 설정하세요.

  3. ONTAP 모드 풀에서 클러스터 이름, SVM 이름, 모든 피어 클러스터 IC-LIF IP 주소를 수집합니다. 노드당 하나의 주소를 쉼표로 구분하여 입력합니다. IC-LIF 수는 피어 클러스터 노드 수와 일치합니다.

  4. 풀의 클러스터 이름, SVM 이름, 모든 IC-LIF 주소를 피어 클러스터 관리자와 공유합니다.

개요

클러스터와 SVM을 수동으로 피어링하려면 다음 단계를 수행하세요.

  1. ontap_execute 설정

  2. 이름 및 IC-LIF 조회

  3. 연결 확인

  4. 동종업체 클러스터

  5. 동종 업계 SVM

  6. 피어링 확인

이름 및 IC-LIF 조회

각 ONTAP 모드 풀에서 일반적인 ONTAP 모드 절차에 설명된 조회 명령어를 실행합니다.

풀 크기에 따라 IC-LIF 주소를 나열하고 피어 클러스터 관리자에게 제공합니다.

연결 확인

-vserver Gcnv 플래그를 사용하여 풀에서 IC-LIF를 피어링하는 ping 명령어를 실행합니다.

 ontap_execute "network ping -vserver Gcnv -destination PEER_IC_LIF_IP -lif intercluster-1"

계속하기 전에 라우팅 또는 방화벽 문제를 해결하세요. 풀 간에 핑이 실패하면 VPC 및 파트너 네트워크 안내를 위해 NetApp Volumes가 Active Directory (및 VPC 뒤의 기타 네트워크)에 연결할 수 없는 이유를 참고하세요.

피어 클러스터

클러스터를 피어링하려면 다음 시나리오 중 하나를 사용하세요.

시나리오 A: ONTAP 모드 풀 및 외부 ONTAP

ONTAP 모드 풀을 외부 ONTAP 클러스터와 피어링하려면 다음 단계를 수행하세요.

  1. ONTAP 모드 풀에서 제공: 모든 피어 IC-LIF 주소와 함께 ontap_execute를 사용하여 cluster peer create -generate-passphrase 명령어를 실행합니다. 명령어 출력에서 비밀번호를 복사합니다.

  2. 외부 ONTAP에서 수락: 피어 클러스터 관리자가 ONTAP 프롬프트에서 모든 ONTAP 모드 IC-LIF 주소와 함께 cluster peer accept 명령어를 실행하고 비밀번호를 입력합니다.

ONTAP 모드 풀에서 피어링을 제공하려면 다음 단계를 따르세요.

 ontap_execute "cluster peer create -ipspace Gcnv -peer-addrs PEER_IC_LIF_1,PEER_IC_LIF_2 -generate-passphrase"

-peer-addrs 플래그에 피어 클러스터의 모든 IC-LIF 주소를 쉼표로 구분하여 나열합니다. 피어 클러스터에 노드가 3개 이상인 경우 주소를 추가합니다.

ONTAP는 명령어 출력의 Passphrase: 아래에 비밀번호를 표시합니다. 암호를 즉시 복사합니다. ONTAP에서 암호를 다시 표시할 수 없다고 경고합니다.

명령어가 실패하거나 출력에 비밀번호가 포함되지 않으면 PEER_MODE=offer 대신 peer_cluster.py를 사용하세요. 자세한 내용은 클러스터 피어링 스크립트를 참고하세요.

암호를 복사하여 외부 ONTAP 관리자에게 보냅니다. 기본적으로 1시간인 암호 유효 기간 내에 제안을 수락합니다.

피어 클러스터 프롬프트에서 모든 ONTAP 모드 IC-LIF 주소를 나열하여 외부 ONTAP 클러스터의 제안을 수락합니다.

 cluster peer accept -ipspace Default -peer-addrs ONTAP_MODE_IC_LIF_1,ONTAP_MODE_IC_LIF_2

ONTAP에서 메시지를 표시하면 암호를 입력합니다. ONTAP 모드 풀의 모든 IC-LIF 주소를 포함합니다.

ONTAP 모드 풀에서 구성을 확인합니다.

 ontap_execute "cluster peer show"

시나리오 B: ONTAP 모드 풀 2개

ONTAP 모드 풀 두 개를 피어링하려면 다음 단계를 수행하세요.

  1. 풀 A에서 제공: 풀 A에서 ontap_execute를 사용하여 cluster peer create -generate-passphrase 명령어를 실행합니다. -peer-addrs에서 풀 B의 모든 IC-LIF 주소를 나열합니다. 암호를 복사합니다.

  2. 풀 B에서 수락: 풀 B에서 PEER_MODE=acceptpeer_cluster.py를 실행하고 PEER_IC_LIF_IPS에 풀 A의 모든 IC-LIF 주소를 나열합니다.

풀 A의 혜택:

 ontap_execute "cluster peer create -ipspace Gcnv -peer-addrs POOL_B_IC_LIF_1,POOL_B_IC_LIF_2 -generate-passphrase"

풀 B의 모든 IC-LIF를 쉼표로 구분하여 나열합니다. 풀 B에 노드가 3개 이상 있는 경우 주소를 추가합니다. 명령어 출력에서 Passphrase: 값을 복사합니다.

명령어가 실패하면 풀 A에서 PEER_MODE=offer와 함께 peer_cluster.py를 사용합니다.

풀 B에서 수락:

 export PROJECT_ID="myproject"
 export LOCATION="us-west1-b"
 export POOL_NAME="pool-b"
 export PEER_MODE="accept"
 export PEER_IC_LIF_IPS="POOL_A_IC_LIF_1,POOL_A_IC_LIF_2"
 export PASSPHRASE="passphrase-from-pool-a"

 python3 peer_cluster.py

PEER_IC_LIF_IPS에 풀 A의 모든 IC-LIF를 나열합니다.

각 풀의 구성을 확인합니다.

 ontap_execute "cluster peer show"

사용 가능 여부가 '사용 가능'이고 인증이 'OK'인지 확인합니다.

피어 SVM

클러스터 피어링이 성공하면 SnapMirror의 SVM 피어를 만듭니다. FlexCache가 필요한 경우 -applications 플래그에 추가합니다.

시나리오 A: ONTAP 모드 풀 및 외부 ONTAP

ONTAP 모드 풀을 외부 ONTAP 클러스터와 피어링하려면 다음 단계를 수행하세요.

  1. 외부 ONTAP에서 만들기: 피어 클러스터 관리자가 ONTAP 프롬프트에서 vserver peer create 명령어를 실행합니다.

  2. ONTAP 모드 풀에서 수락: ontap_execute를 사용하여 vserver peer accept 명령어를 실행합니다.

피어 클러스터 프롬프트에서 외부 ONTAP 클러스터에 피어를 만듭니다.

 vserver peer create -vserver PEER_SVM_NAME -peer-vserver ONTAP_MODE_SVM_NAME \
  -peer-cluster ONTAP_MODE_CLUSTER_NAME -applications snapmirror

ONTAP 모드 풀에서 피어를 수락합니다.

 ontap_execute "vserver peer accept -vserver ONTAP_MODE_SVM_NAME -peer-vserver PEER_SVM_NAME"

시나리오 B: ONTAP 모드 풀 2개

ONTAP 모드 풀 두 개를 피어링하려면 다음 단계를 수행하세요.

  1. 풀 A에서 만들기: 풀 A에서 ontap_execute를 사용하여 vserver peer create 명령어를 실행합니다 (만들기 명령어를 사용할 수 없는 경우 peer_svm.py).

  2. 풀 B에서 수락: 풀 B에서 ontap_execute을 사용하여 vserver peer accept 명령어를 실행합니다.

풀 A에서 피어를 만듭니다.

 ontap_execute "vserver peer create -vserver POOL_A_SVM -peer-vserver POOL_B_SVM -peer-cluster POOL_B_CLUSTER_NAME -applications snapmirror"

풀 B에서 피어를 수락합니다.

 ontap_execute "vserver peer accept -vserver POOL_B_SVM -peer-vserver POOL_A_SVM"

두 풀에 동일한 SVM 피어를 만들지 마세요.

피어링 확인

ONTAP 모드 풀에서 피어링을 확인하려면 다음 단계를 따르세요.

 ontap_execute "cluster peer show"
 ontap_execute "vserver peer show"

클러스터 피어 가용성이 'Available'이고 SVM 피어 상태가 'peered'인지 확인합니다.

시나리오 B의 경우 두 번째 풀에서 절차를 반복합니다. 외부 ONTAP 관리자는 피어 클러스터에서 cluster peer showvserver peer show를 실행하여 구성을 확인할 수 있습니다.

클러스터 피어가 없으면 cluster peer show 명령어는 항목이 존재하지 않는다고 반환합니다.

피어링 삭제

클러스터 피어보다 먼저 SVM 피어를 삭제합니다.

한 풀에서 SVM 피어를 삭제하려면 풀에 연결할 수 있을 때 양쪽의 관계를 삭제합니다.

 ontap_execute "vserver peer delete -vserver LOCAL_SVM_NAME -peer-vserver PEER_SVM_NAME"

ONTAP CLI 프록시는 클러스터 피어 삭제를 지원하지 않습니다. 클러스터 피어를 삭제하려면 REST API를 통해 UUID로 삭제하세요.

먼저 피어를 나열하여 UUID를 찾습니다.

 TOKEN=$(gcloud auth print-access-token)
 curl -s -H "Authorization: Bearer $TOKEN" \

 "https://netapp.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/storagePools/POOL_NAME/ontap/api/cluster/peers"

반환된 각 UUID를 삭제합니다.

 curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \

 "https://netapp.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/storagePools/POOL_NAME/ontap/api/cluster/peers/PEER_UUID"

시나리오 B의 두 ONTAP 모드 풀에서 이 프로세스를 반복합니다. 시나리오 A의 경우 필요한 경우 외부 ONTAP 클러스터에서 cluster peer delete를 실행합니다.

클러스터 피어링 스크립트

다음을 peer_cluster.py로 저장합니다. 풀이 클러스터 피어링 제안 (PEER_MODE=accept)을 수락해야 하는 경우 ONTAP 모드 풀에서 사용합니다. 하이브리드 피어링시나리오 B에서는 accept 모드를 사용합니다. ontap_execute을 통해 cluster peer create -generate-passphrase이 실패하거나 비밀번호를 반환하지 않는 경우에만 offer 모드 (PEER_MODE=offer)를 사용하세요. 이 스크립트에는 Python 3.8 이상과 Python 표준 라이브러리가 필요합니다.

 #!/usr/bin/env python3
"""Cluster peer offer or accept on an ONTAP-mode pool through the NetApp Volumes REST proxy."""

import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request

REQUIRED = ("PROJECT_ID", "LOCATION", "POOL_NAME", "PEER_MODE")
missing = [name for name in REQUIRED if not os.environ.get(name)]
if missing:
    sys.exit(f"Set required environment variables: {', '.join(missing)}")

PROJECT_ID = os.environ["PROJECT_ID"]
LOCATION = os.environ["LOCATION"]
POOL_NAME = os.environ["POOL_NAME"]
PEER_MODE = os.environ["PEER_MODE"].lower()
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "10"))

API_ROOT = (
    f"https://netapp.googleapis.com/v1/projects/{PROJECT_ID}"
    f"/locations/{LOCATION}/storagePools/{POOL_NAME}/ontap/api"
)

def parse_ip_list(value: str) -> list:
    return [ip.strip() for ip in value.replace("#", ",").split(",") if ip.strip()]

def get_access_token() -> str:
    return subprocess.check_output(
        ["gcloud", "auth", "print-access-token"], text=True
    ).strip()

def api_request(method: str, path: str, body=None) -> dict:
    headers = {
        "Authorization": f"Bearer {get_access_token()}",
        "Content-Type": "application/json",
    }
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(f"{API_ROOT}{path}", data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode()
        sys.exit(f"API request failed ({exc.code}): {detail}")

def job_record(response: dict) -> dict:
    return response.get("rawResponse") or response.get("body") or response

def job_state(response: dict) -> str:
    return job_record(response).get("state", "")

def format_job_error(response: dict) -> str:
    record = job_record(response)
    message = record.get("message") or record.get("error", {}).get("message")
    if message:
        return message
    return json.dumps(response, indent=2)

def wait_for_job(job_uuid: str) -> None:
    print(f"ONTAP job UUID: {job_uuid}")
    while True:
        response = api_request("GET", f"/cluster/jobs/{job_uuid}")
        state = job_state(response)
        print(f"  Job state: {state}")
        if state == "success":
            return
        if state in ("failure", "error"):
            sys.exit(f"Job failed: {format_job_error(response)}")
        time.sleep(POLL_INTERVAL)

def extract_passphrase(response: dict) -> str:
    body = response.get("body") or response.get("rawResponse") or {}
    records = body.get("records") or []
    if records:
        auth = records[0].get("authentication") or {}
        if auth.get("passphrase"):
            return auth["passphrase"]
    auth = body.get("authentication") or {}
    return auth.get("passphrase", "")

def build_offer_body() -> dict:
    if not os.environ.get("PEER_IC_LIF_IPS"):
        sys.exit("Set PEER_IC_LIF_IPS for offer mode")

    payload = {
        "remote": {"ip_addresses": parse_ip_list(os.environ["PEER_IC_LIF_IPS"])},
        "authentication": {"generate_passphrase": True},
        "ipspace": {"name": os.environ.get("IPSPACE", "Gcnv")},
    }

    expiry = os.environ.get("EXPIRY_TIME")
    if expiry:
        payload["authentication"]["expiry_time"] = expiry

    local_svm = os.environ.get("LOCAL_SVM_NAME")
    if local_svm:
        payload["initial_allowed_svms"] = [{"name": local_svm}]

    apps = os.environ.get("PEER_APPLICATIONS", "snapmirror")
    payload["peer_applications"] = [a.strip() for a in apps.split(",") if a.strip()]

    encryption = os.environ.get("ENCRYPTION_PROPOSED")
    if encryption:
        payload["encryption"] = {"proposed": encryption}

    return {"body": payload}

def build_accept_body() -> dict:
    for name in ("PEER_IC_LIF_IPS", "PASSPHRASE"):
        if not os.environ.get(name):
            sys.exit(f"Set {name} for accept mode")

    return {
        "body": {
            "remote": {"ip_addresses": parse_ip_list(os.environ["PEER_IC_LIF_IPS"])},
            "authentication": {"passphrase": os.environ["PASSPHRASE"]},
            "ipspace": {"name": os.environ.get("IPSPACE", "Gcnv")},
        }
    }

def main() -> None:
    if PEER_MODE == "offer":
        body = build_offer_body()
        print("Creating cluster peer offer...")
    elif PEER_MODE == "accept":
        body = build_accept_body()
        print("Accepting cluster peer offer...")
    else:
        sys.exit("PEER_MODE must be 'offer' or 'accept'")

    response = api_request("POST", "/cluster/peers?return_records=true", body)

    job = (response.get("body") or {}).get("job") or response.get("job")
    if job and job.get("uuid"):
        wait_for_job(job["uuid"])

    if PEER_MODE == "offer":
        passphrase = extract_passphrase(response)
        if passphrase:
            print(f"\nPassphrase (provide to the peer pool): {passphrase}")
        else:
            print(json.dumps(response, indent=2))

if __name__ == "__main__":
    main()

선택적 환경 변수: EXPIRY_TIME, PEER_APPLICATIONS (기본값 snapmirror), ENCRYPTION_PROPOSED, IPSPACE (기본값 Gcnv)

SVM 피어링 스크립트 (대체)

ONTAP CLI 프록시를 통해 vserver peer create를 사용할 수 없는 경우에만 peer_svm.py를 사용하세요. 대신 피어 SVM에 설명된 대로 vserver peer createvserver peer accept를 사용하세요.

 #!/usr/bin/env python3
"""Create an SVM peer on an ONTAP-mode pool (fallback when vserver peer create is unavailable)."""

import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request

REQUIRED = (
    "PROJECT_ID", "LOCATION", "POOL_NAME",
    "LOCAL_SVM_NAME", "PEER_CLUSTER_NAME", "PEER_SVM_NAME",
)
missing = [name for name in REQUIRED if not os.environ.get(name)]
if missing:
    sys.exit(f"Set required environment variables: {', '.join(missing)}")

PROJECT_ID = os.environ["PROJECT_ID"]
LOCATION = os.environ["LOCATION"]
POOL_NAME = os.environ["POOL_NAME"]
LOCAL_SVM_NAME = os.environ["LOCAL_SVM_NAME"]
PEER_CLUSTER_NAME = os.environ["PEER_CLUSTER_NAME"]
PEER_SVM_NAME = os.environ["PEER_SVM_NAME"]
APPLICATIONS = [
    app.strip()
    for app in os.environ.get("APPLICATIONS", "snapmirror").split(",")
    if app.strip()
]
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "10"))

API_ROOT = (
    f"https://netapp.googleapis.com/v1/projects/{PROJECT_ID}"
    f"/locations/{LOCATION}/storagePools/{POOL_NAME}/ontap/api"
)

def get_access_token() -> str:
    return subprocess.check_output(
        ["gcloud", "auth", "print-access-token"], text=True
    ).strip()

def api_request(method: str, path: str, body=None) -> dict:
    headers = {
        "Authorization": f"Bearer {get_access_token()}",
        "Content-Type": "application/json",
    }
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(f"{API_ROOT}{path}", data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as exc:
        sys.exit(f"API request failed ({exc.code}): {exc.read().decode()}")

def format_job_error(response: dict) -> str:
    record = response.get("rawResponse") or response.get("body") or response
    message = record.get("message") or record.get("error", {}).get("message")
    return message or json.dumps(response, indent=2)

def wait_for_job(job_uuid: str) -> None:
    while True:
        response = api_request("GET", f"/cluster/jobs/{job_uuid}")
        record = response.get("rawResponse") or response.get("body") or response
        state = record.get("state", "")
        print(f"  Job state: {state}")
        if state == "success":
            print("SVM peer relationship created.")
            return
        if state in ("failure", "error"):
            sys.exit(f"Job failed: {format_job_error(response)}")
        time.sleep(POLL_INTERVAL)

def main() -> None:
    body = {
        "body": {
            "svm": {"name": LOCAL_SVM_NAME},
            "peer": {
                "cluster": {"name": PEER_CLUSTER_NAME},
                "svm": {"name": PEER_SVM_NAME},
            },
            "applications": APPLICATIONS,
        }
    }
    response = api_request("POST", "/svm/peers?return_records=true", body)
    job = (response.get("body") or {}).get("job") or response.get("job")
    if not job or not job.get("uuid"):
        print(json.dumps(response, indent=2))
        return
    wait_for_job(job["uuid"])

if __name__ == "__main__":
    main()

다음 단계