ピア クラスタと 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 クラスタ。

    • 2 つの ONTAP モードのプール。

外部 ONTAP クラスタの追加のピアリング オプションの詳細については、NetApp ONTAP のドキュメントをご覧ください。

ONTAP モードのプールでのクラスタ ピアリングの仕組み

クラスタ ピアリング プロセスは、オファーと承諾のワークフローに従います。

  • Offer: 1 つのクラスタがピア IC-LIF アドレスへのピアリングを開始し、パスフレーズを受け取ります。

  • Accept: ピア クラスタは、オファリング クラスタの 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(2 つの ONTAP モードのプール): ontap_execute を使用してプール A で cluster peer create-generate-passphrase を実行し、PEER_MODE=accept を使用してプール B で peer_cluster.py を実行します。

ONTAP モードのプールで ontap_execute を使用して vserver peer accept を実行し、SVM ピアリングを行います。シナリオ A では、外部 ONTAP クラスタが vserver peer create を実行します。

ピアリングが必要なレプリケーション方法

次のレプリケーション方法ではピアリングが必要です。

  • ONTAP ソースから Flex Unified ONTAP モード(SnapMirror): ONTAP モード プールでピアリングします。

  • Flex Unified ONTAP モードから任意の 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 モードのソースプールで実行する必要があります。

この注文を元に戻すことはできません。最初に Default-mode の宛先を作成する必要があります。

ONTAP モードの移行元でクラスタ ピアリングを完了する

Google Cloud コンソールの [Replication] タブまたは [Migration] タブで、または 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 クラスタ間、または 2 つの ONTAP モードのプール間で SnapMirror を実行する前などです。

始める前に

次の前提条件を満たします。

  1. ONTAP モード プールの IC-LIF がピア クラスタの IC-LIF に到達することを確認します。ONTAP モードのプール上の IC-LIF は IPspace Gcnv にあり、外部 ONTAP クラスタでは IPspace Default にあります。ICMP と必要なクラスタ間ポートをテストします。

  2. 開始する前に、ontap_execute を設定します。

  3. ONTAP モード プールで、クラスタ名、SVM 名、すべてのピア クラスタ IC-LIF IP アドレスを収集します。ノードごとに 1 つのアドレスを指定し、カンマで区切ります。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"

続行する前に、ルーティングまたはファイアウォールの問題を修正してください。プール間で ping が失敗した場合は、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 モードの IC-LIF アドレスを指定して ONTAP プロンプトで cluster peer accept コマンドを実行し、パスフレーズを入力します。

ONTAP モード プールでピアリングを提供するには:

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

ピア クラスタのすべての IC-LIF アドレスを -peer-addrs フラグにカンマ区切りで指定します。ピア クラスタに 3 つ以上のノードがある場合は、アドレスを追加します。

ONTAP は、コマンド出力の Passphrase: にパスフレーズを表示します。パスフレーズをすぐにコピーします。ONTAP は、パスフレーズを再度表示できないことを警告します。

コマンドが失敗した場合や、出力にパスフレーズが含まれていない場合は、代わりに PEER_MODE=offerpeer_cluster.py を使用します。詳細については、クラスタ ピアリング スクリプトをご覧ください。

パスフレーズをコピーして、外部 ONTAP 管理者に送信します。パスフレーズの有効期間(デフォルトでは 1 時間)内にオファーを承認します。

ピア クラスタのプロンプトで、外部 ONTAP クラスタのオファーを承認します。すべての ONTAP モードの IC-LIF アドレスを一覧表示します。

 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: 2 つの ONTAP モードのプール

2 つの ONTAP モード プールをピアリングするには、次の操作を行います。

  1. プール A でオファー: プール A で ontap_execute を使用して cluster peer create -generate-passphrase コマンドを実行します。-peer-addrs でプール B のすべての IC-LIF アドレスを一覧表示します。パスフレーズをコピーします。

  2. プール B で受け入れる: プール B で PEER_MODE=accept を使用して peer_cluster.py を実行し、プール A のすべての IC-LIF アドレスを PEER_IC_LIF_IPS に一覧表示します。

プール 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

プール A のすべての IC-LIF を PEER_IC_LIF_IPS にリストします。

各プールの構成を確認します。

 ontap_execute "cluster peer show"

可用性が [Available] で、認証が [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: 2 つの ONTAP モードのプール

2 つの ONTAP モード プールをピアリングするには、次の操作を行います。

  1. プール A で作成: プール A で ontap_execute(作成コマンドが使用できない場合は peer_svm.py)を使用して vserver peer create コマンドを実行します。

  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 ピアの State が peered であることを確認します。

シナリオ B の場合は、2 番目のプールで手順を繰り返します。外部 ONTAP 管理者は、ピア クラスタで cluster peer showvserver peer show を実行して構成を確認できます。

クラスタ ピアが存在しない場合、cluster peer show コマンドはエントリが存在しないことを返します。

ピアリングを削除する

クラスタ ピアの前に SVM ピアを削除します。

1 つのプールで 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 モードを使用します。offer モード(PEER_MODE=offer)は、ontap_execute を介して cluster peer create -generate-passphrase が失敗した場合、またはパスフレーズが返されなかった場合にのみ使用します。このスクリプトには、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_TIMEPEER_APPLICATIONS(デフォルト snapmirror)、ENCRYPTION_PROPOSEDIPSPACE(デフォルト Gcnv)。

SVM ピアリング スクリプト(フォールバック)

peer_svm.py は、ONTAP CLI プロキシで vserver peer create を使用できない場合にのみ使用します。代わりに、ピア 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()

次のステップ