אשכולות עמיתים ומכונות וירטואליות של אחסון (SVM)

בדף הזה מוסבר איך להגדיר peering למאגר האחסון ONTAP ב-Google Cloud NetApp Volumes.

שיוך אשכולות ושיוך מכונות וירטואליות לאחסון (SVM) מחברים את מאגר האחסון של Flex Unified ONTAP לאשכול אחר. כך שירותים כמו SnapMirror ו-FlexCache יכולים לפעול.

משתמשים בסקריפט peer_cluster.py כדי לאשר הצעה ומזינים סיסמה למאגר במצב ONTAP. מידע נוסף זמין במאמר בנושא סקריפט של שיתוף פעולה בין אשכולות.

בחירת הליך

בוחרים אחת מהפעולות הבאות ליצירת קישור בין רשתות, בהתאם להגדרה שלכם:

  • Hybrid peering: אתם מעבירים או משכפלים ממאגר במצב ONTAP לנפח NetApp Volumes במצב ברירת מחדל (Standard,‏ Premium,‏ Extreme או Flex Unified במצב ברירת מחדל). NetApp Volumes מתחיל ליצור קשר מהיעד. משלימים את ה-peering במאגר המקור במצב ONTAP.

  • Peering ידני: אתם יוצרים את ה-Peering בעצמכם ואתם לא משתמשים בהעברה היברידית או בתהליך עבודה של שכפול חיצוני. בוחרים תרחיש:

    • מאגר ONTAP-mode ואשכול ONTAP חיצוני שנמצא בפריסה מקומית או מחוץ ל-NetApp Volumes.

    • שתי מאגרי אחסון במצב ONTAP.

לקבלת מידע נוסף על אפשרויות נוספות של שיתוף פעולה באשכול ONTAP חיצוני, אפשר לעיין במסמכי התיעוד של NetApp ONTAP.

איך עובד שיתוף פעולה בין אשכולות במאגר במצב ONTAP

תהליך ה-peering בין אשכולות מתבצע לפי תהליך עבודה של הצעה ואישור:

  • הצעה: אשכול אחד יוזם קישור בין רשתות שכנות (peering) לעבר כתובות IC-LIF של עמיתים ומקבל ביטוי סיסמה.

  • Accept (אישור): קבוצת ה-Peering משלימה את ה-Peering באמצעות כתובות IC-LIF וסיסמת הגישה של קבוצת ה-Offering.

הפרוקסי של ONTAP CLI לא אינטראקטיבי. הפקודה cluster peer create -generate-passphrase לא מבקשת קלט, כך שאפשר להריץ את המבצע במאגר במצב ONTAP באמצעות ontap_execute. מפרטים את כל כתובות ה-IC-LIF המרוחקות כערך -peer-addrs מופרד בפסיקים. הפקודה cluster peer accept מבקשת את סיסמת הגישה, ולכן השלב הזה נכשל דרך ה-proxy. משתמשים ב-peer_cluster.py כשמאגר במצב ONTAP צריך לקבל הצעה.

כדי לבצע פירינג בין האשכולות, אפשר להשתמש באחד מהתרחישים הבאים:

  • תרחיש א' (מצב ONTAP ו-ONTAP חיצוני): מריצים את הפקודה cluster peer create-generate-passphrase במאגר במצב ONTAP באמצעות ontap_execute עם כל כתובות ה-IC-LIF של העמיתים. האדמין החיצוני של ONTAP מריץ את הפקודה cluster peer accept בהנחיה של האשכול עם כל הכתובות של IC-LIF במצב ONTAP, ומזין את ביטוי הסיסמה.

  • תרחיש ב' (שני מאגרי ONTAP): מריצים את הפקודה cluster peer create-generate-passphrase במאגר א' באמצעות ontap_execute, ואז מריצים את הפקודה peer_cluster.py עם PEER_MODE=accept במאגר ב'.

מבצעים שיוך של SVM על ידי הפעלת vserver peer accept באמצעות ontap_execute במאגר במצב ONTAP. בתרחיש א', אשכול ONTAP חיצוני מבצע את הפעולות הבאות: vserver peer create.

שיטות שכפול שדורשות שיתוף פעולה

שיטות השכפול הבאות דורשות שימוש ב-Peering:

  • כל מקור ONTAP למצב Flex Unified ONTAP ‏ (SnapMirror): יצירת קשר עמיתים במאגר ONTAP-mode.

  • העברה גמישה של נתונים ממצב Unified ONTAP לכל יעד ONTAP‏ (SnapMirror): יצירת קשר בין מאגרים במצב ONTAP.

  • מקור במצב ONTAP ליעדים במצב ברירת מחדל של NetApp Volumes (העברת נפח או שכפול חיצוני): צריך להשתמש בחיבור היברידי בין רשתות עמיתות במאגר המקור במצב ONTAP.

שכפול נפחים במצב ברירת מחדל ב-NetApp Volumes לא משתמש ב-ONTAP cluster peering.

Hybrid peering

משתמשים בקטע הזה כשהמקור הוא מאגר במצב ONTAP והיעד הוא נפח במצב ברירת מחדל ב-NetApp Volumes.

איך NetApp Volumes עובד

תהליך העבודה של שילוב בין רשתות כולל את השלבים הבאים:

  1. יוצרים העברת נפח או שכפול חיצוני בנפח היעד במסוף Google Cloud או באמצעות Google Cloud CLI. מציינים את שם אשכול המקור במצב ONTAP, את ה-SVM, את עוצמת הקול ואת כתובות ה-IC-LIF של המקור.

  2. אם עדיין לא הוגדר שיוך בין אשכולות, NetApp Volumes בודק את הקישוריות ביעד, יוצר סיסמה ומציג הוראות לשיוך.

  3. צריך לבצע את שאר השלבים של יצירת הקישור במאגר המקור במצב ONTAP, כי NetApp Volumes לא מריץ פקודות אוטומטיות במאגר המקור.

אי אפשר לבטל את ההזמנה הזו. קודם צריך ליצור את היעד במצב ברירת מחדל.

השלמת שיוך אשכולות במקור במצב ONTAP

מאחזרים את סיסמת הגישה ואת כתובות ה-IC-LIF של היעד במסוף Google Cloud בכרטיסייה Replication (שכפול) או Migration (העברה), או באמצעות Google Cloud CLI:

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

במאגר המקור במצב ONTAP, מאשרים את ה-peering ש-NetApp Volumes הציע ביעד. מגדירים את ontap_execute כפי שמתואר במאמר תהליכים נפוצים במצב ONTAP. מגדירים את POOL_NAME ואת LOCATION עבור מאגר המקורות. מציגים רשימה של כל כתובות ה-IC-LIF של היעד ב-PEER_IC_LIF_IPS, ואז מריצים את peer_cluster.py עם PEER_MODE=accept:

 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"

השלמת הפירינג של SVM במקור במצב ONTAP

אם NetApp Volumes לא מסיים את ה-SVM peering באופן אוטומטי, הוא מציג פקודת 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 כדי לוודא שהשכפול או ההעברה מתבצעים.

חיבור ידני בין רשתות

משתמשים בקטע הזה כשמקימים בעצמכם קישור בין רשתות ללא תהליך עבודה היברידי, למשל לפני SnapMirror בין מאגר במצב ONTAP לבין אשכול ONTAP חיצוני, או בין שני מאגרים במצב ONTAP.

לפני שמתחילים

צריך לוודא שמתקיימות הדרישות המוקדמות הבאות:

  1. מוודאים ש-IC-LIFs במאגר במצב ONTAP מגיעים ל-IC-LIFs של אשכול העמיתים. ממשקי IC-LIF במאגרי ONTAP-mode נמצאים ב-IPspace Gcnv; באשכולות ONTAP חיצוניים הם נמצאים ב-IPspace Default. בודקים את פרוטוקול ICMP ואת היציאות הנדרשות בין אשכולות.

  2. לפני שמתחילים, צריך להגדיר את ontap_execute.

  3. אוספים את שם האשכול, שם ה-SVM וכל כתובות ה-IP של IC-LIF באשכולות עמיתים במאגר ONTAP-mode. צריך להזין כתובת אחת לכל צומת, ולהפריד בין הכתובות באמצעות פסיקים. מספר ממשקי ה-IC-LIF תואם למספר הצמתים באשכול העמית.

  4. משתפים עם האדמין של אשכול ה-peer את שם האשכול של המאגר, שם ה-SVM וכל כתובות ה-IC-LIF.

סקירה כללית

כדי לבצע פירינג בין האשכולות וה-SVM באופן ידני, פועלים לפי השלבים הבאים:

  1. הגדרה של ontap_execute

  2. חיפוש שמות ומספרי IC-LIF

  3. בדיקת הקישוריות

  4. אשכולות להשוואה

  5. Peer SVMs

  6. אימות של ה-Peering

חיפוש שמות ו-IC-LIF

בכל מאגר במצב ONTAP, מריצים את פקודות החיפוש שמתוארות במאמר הליכים נפוצים במצב ONTAP.

מפרטים את כתובות ה-IC-LIF, שמשתנות בהתאם לגודל המאגר, ומעבירים אותן לאדמין של אשכול העמיתים.

בדיקת הקישוריות

מריצים פקודת פינג כדי לבצע פירינג של IC-LIFs מהמאגר באמצעות הדגל -vserver Gcnv:

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

לפני שממשיכים, צריך לפתור בעיות שקשורות לניתוב או לחומת אש. אם הפינג בין המאגרים נכשל, אפשר לעיין במאמר למה אין אפשרות להגיע מ-NetApp Volumes אל Active Directory (ואל רשתות אחרות מאחורי ה-VPC) לקבלת הנחיות לגבי VPC ורשתות שותפים.

אשכולות של חברות להשוואה

כדי לבצע פירינג בין האשכולות, אפשר להשתמש באחד מהתרחישים הבאים:

תרחיש א': מאגר ONTAP ומערכת ONTAP חיצונית

כדי ליצור קשר בין מאגר במצב ONTAP לבין אשכול ONTAP חיצוני, מבצעים את השלבים הבאים:

  1. הצעה במאגר במצב ONTAP: מריצים את הפקודה cluster peer create -generate-passphrase באמצעות ontap_execute עם כל כתובות ה-IC-LIF של העמיתים. מעתיקים את סיסמת הגישה מהפלט של הפקודה.

  2. אישור ב-ONTAP החיצוני: מנהל האשכול העמית מריץ את הפקודה cluster peer accept בשורת הפקודה של ONTAP עם כל הכתובות של IC-LIF במצב ONTAP ומזין את ביטוי הסיסמה.

כדי להציע שירותי peering במאגר במצב ONTAP:

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

מציינים את כל כתובות ה-IC-LIF של אשכול העמיתים בדגל -peer-addrs, ומפרידים ביניהן באמצעות פסיקים. אם באשכול העמיתים יש יותר משני צמתים, מוסיפים עוד כתובות.

ב-ONTAP, סיסמת הגישה מוצגת בפלט של הפקודה בקטע Passphrase:. מעתיקים את סיסמת הגישה מיד. מערכת ONTAP מזהירה שאי אפשר להציג את סיסמת הגישה שוב.

אם הפקודה נכשלת או שהפלט לא כולל סיסמה, צריך להשתמש בפקודה peer_cluster.py עם PEER_MODE=offer. מידע נוסף מופיע במאמר סקריפט ליצירת חיבור בין אשכולות.

מעתיקים את ביטוי הגישה ושולחים אותו לאדמין החיצוני של ONTAP. מאשרים את המבצע תוך תקופת התוקף של ביטוי הסיסמה, שהיא שעה אחת כברירת מחדל.

מאשרים את ההצעה באשכול ONTAP החיצוני בהנחיה של אשכול העמיתים על ידי הצגת כל הכתובות של IC-LIF במצב ONTAP:‏

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

מזינים את ביטוי הסיסמה כשמוצגת בקשה לכך ב-ONTAP. צריך לכלול את כל כתובות ה-IC-LIF ממאגר ONTAP-mode.

מאמתים את ההגדרה במאגר במצב ONTAP:

 ontap_execute "cluster peer show"

תרחיש ב': שתי מאגרי אחסון במצב ONTAP

כדי ליצור שותפות בין שני מאגרי ONTAP-mode:

  1. הצעה על מאגר A: מריצים את הפקודה cluster peer create -generate-passphrase באמצעות ontap_execute במאגר A. צריך לרשום את כל כתובות ה-IC-LIF ממאגר B ב--peer-addrs. מעתיקים את ביטוי-הסיסמה.

  2. Accept on pool B: מריצים את peer_cluster.py עם PEER_MODE=accept ב-pool B, ומפרטים את כל כתובות ה-IC-LIF מ-pool A ב-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"

מפרטים את כל ה-IC-LIF ממאגר B, ומפרידים ביניהם באמצעות פסיק. מוסיפים כתובות נוספות אם במאגר B יש יותר משני צמתים. מעתיקים את הערך Passphrase: מפלט הפקודה.

אם הפקודה נכשלת, משתמשים בפקודה peer_cluster.py עם PEER_MODE=offer במאגר A.

אישור במאגר ב':

 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

תציג רשימה של כל ה-IC-LIF ממאגר A ב-PEER_IC_LIF_IPS.

מאמתים את ההגדרה בכל מאגר:

 ontap_execute "cluster peer show"

מוודאים שהזמינות היא Available (זמין) והאימות הוא OK (בסדר).

מכונות וירטואליות להשוואה

אחרי שהצמדת האשכולות מצליחה, יוצרים עמית SVM עבור SnapMirror. אם אתם צריכים FlexCache, מוסיפים אותו לדגל -applications.

תרחיש א': מאגר ONTAP ומערכת ONTAP חיצונית

כדי ליצור קשר בין מאגר במצב ONTAP לבין אשכול ONTAP חיצוני, מבצעים את השלבים הבאים:

  1. יצירה ב-ONTAP חיצוני: האדמין של אשכול ה-Peer מריץ את הפקודה vserver peer create בהנחיית ONTAP.

  2. מאשרים במאגר במצב ONTAP: מריצים את הפקודה vserver peer accept באמצעות ontap_execute.

יוצרים את ה-peer באשכול ONTAP החיצוני בפרומפט של אשכול ה-peer:

 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"

תרחיש ב': שתי מאגרי אחסון במצב ONTAP

כדי ליצור שותפות בין שני מאגרי ONTAP-mode:

  1. יצירה במאגר א': מריצים את הפקודה vserver peer create באמצעות ontap_execute במאגר א' (או peer_svm.py אם הפקודה ליצירה לא זמינה).

  2. אישור ב-pool B: מריצים את הפקודה vserver peer accept באמצעות ontap_execute ב-pool B.

יוצרים את העמית במאגר א':

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

מאשרים את העמית במאגר ב':

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

אל תיצרו את אותו עמית SVM בשני המאגרים.

אימות ה-peering

כדי לאמת את הפירינג במאגר במצב ONTAP:

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

מוודאים שהזמינות של עמית האשכול היא Available (זמין) והמצב של עמית ה-SVM הוא peered (בחיבור עמיתים).

בתרחיש ב', חוזרים על התהליך במאגר השני. מנהל מערכת חיצוני של ONTAP יכול לאמת את ההגדרה על ידי הפעלת הפקודות cluster peer show ו-vserver peer show באשכול העמיתים.

אם אין עמיתים באשכול, הפקודה cluster peer show מחזירה שהערך לא קיים.

מחיקת הקישור בין רשתות שכנות

מומלץ למחוק את עמיתי ה-SVM לפני עמיתי האשכול.

כדי למחוק עמית SVM במאגר אחד, צריך למחוק אותו כדי להסיר את הקשר משני הצדדים כשהמאגרים נגישים.

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

הפרוקסי של ONTAP CLI לא תומך במחיקה של קישור בין אשכולות שכנים (peering). כדי למחוק צומת עמית באשכול, צריך למחוק אותו באמצעות מזהה ייחודי אוניברסלי (UUID) דרך API בארכיטקטורת REST.

קודם כל, מציגים את רשימת העמיתים כדי למצוא את ה-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"

חוזרים על התהליך הזה בשני המאגרים במצב ONTAP בתרחיש ב'. בתרחיש א', מריצים את cluster peer delete באשכול ONTAP החיצוני אם צריך.

סקריפט של שיוך אשכולות

שומרים את הקובץ הבא כ-peer_cluster.py. משתמשים בו במאגר במצב ONTAP כשהמאגר צריך לאשר הצעה לחיבור בין אשכולות (PEER_MODE=accept). חיבור היברידי בין אשכולות ותרחיש ב' משתמשים במצב accept. משתמשים במצב offer (PEER_MODE=offer) רק אם cluster peer create -generate-passphrase נכשל דרך ontap_execute או שלא מחזיר ביטוי סיסמה. הסקריפט הזה דורש 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 שכנות (גיבוי)

משתמשים ב-peer_svm.py רק אם vserver peer create לא זמין דרך שרת ה-proxy של ONTAP CLI. במקום זאת, צריך להשתמש ב-vserver peer create וב-vserver peer accept כמו שמתואר במאמר Peer SVMs.

 #!/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()

המאמרים הבאים