对等互连集群和 SVM

本页面介绍了如何在 Google Cloud NetApp Volumes 中为 ONTAP 存储池设置对等互连。

集群对等互连和存储虚拟机 (SVM) 对等互连可将 Flex Unified ONTAP 存储池连接到另一个集群。这样一来,SnapMirror 和 FlexCache 等服务便可运行。

使用 peer_cluster.py 脚本接受方案,并为 ONTAP 模式池输入口令。如需了解详情,请参阅集群对等互连脚本

选择程序

根据您的设置,选择以下对等互连程序之一:

  • 混合对等互连:您从 ONTAP 模式池迁移或复制到默认模式 NetApp Volumes 卷(标准、高级、极速或灵活统一默认模式)。NetApp Volumes 会从目标位置开始对等互连。您在 ONTAP 模式源池上完成对等。

  • 手动对等互连:您自行建立对等互连,并且未使用混合迁移或外部复制工作流。选择一个场景:

    • 一个 ONTAP 模式池和一个位于本地或 NetApp Volumes 外部的外部 ONTAP 集群。

    • 两个 ONTAP 模式池。

如需详细了解外部 ONTAP 集群上的其他对等互连选项,请参阅 NetApp ONTAP 文档

ONTAP 模式池中的集群对等互连如何运作

集群对等互连流程遵循“提议和接受”工作流:

  • 提供方:一个集群向对等 IC-LIF 地址发起对等连接,并接收口令。

  • 接受:对等集群使用提供集群的 IC-LIF 地址和口令完成对等互连。

ONTAP CLI 代理不是交互式的。cluster peer create -generate-passphrase 命令不会提示输入,因此您可以使用 ontap_execute 在 ONTAP 模式池上运行该优惠。以英文逗号分隔的 -peer-addrs 值列出所有远程 IC-LIF 地址。cluster peer accept 命令会提示输入口令,因此该步骤会通过代理失败。当 ONTAP 模式池必须接受报价时,请使用 peer_cluster.py

如需对等互连集群,请使用以下任一方案:

  • 方案 A(ONTAP 模式和外部 ONTAP):使用 ontap_execute 和所有对等 IC-LIF 地址在 ONTAP 模式池上运行 cluster peer create-generate-passphrase。外部 ONTAP 管理员在集群提示符下运行 cluster peer accept,其中包含所有 ONTAP 模式 IC-LIF 地址,然后输入口令。

  • 方案 B(两个 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. 您必须在 ONTAP 模式的源池上执行剩余的对等互连步骤,因为 NetApp Volumes 不会在源池上运行自动化命令。

您无法撤消此订单。您必须先创建默认模式的目标平台。

在 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_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"

在 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 位于 IP 空间 Gcnv 中;在外部 ONTAP 集群上,它们位于 IP 空间 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 标志运行 ping 命令,以从池中对等 IC-LIF 进行 ping 操作:

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

请先解决路由或防火墙问题,然后再继续。如果池之间 ping 失败,请参阅 Why your NetApp Volumes can't reach Active Directory (and other networks behind your VPC),了解 VPC 和合作伙伴网络指南。

对等集群

如需对等互连集群,请使用以下任一方案:

方案 A:ONTAP 模式池和外部 ONTAP

如需将 ONTAP 模式池与外部 ONTAP 集群对等互连,请执行以下步骤:

  1. 在 ONTAP 模式池上提供:使用 ontap_execute 和所有对等 IC-LIF 地址运行 cluster peer create -generate-passphrase 命令。从命令输出中复制口令。

  2. 在外部 ONTAP 上接受:对等集群管理员在 ONTAP 提示符下运行 cluster peer accept 命令,其中包含所有 ONTAP 模式 IC-LIF 地址,然后输入口令。

如需在 ONTAP 模式池上提供对等互连,请执行以下操作:

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

-peer-addrs 标志中列出对等集群的每个 IC-LIF 地址,并以英文逗号分隔。如果对等集群的节点数量超过两个,请添加其他地址。

ONTAP 会在命令输出中的 Passphrase: 下显示口令。立即复制口令;ONTAP 会警告说,它无法再次显示口令。

如果该命令失败或输出不包含口令,请改用 peer_cluster.pyPEER_MODE=offer。如需了解详情,请参阅集群对等互连脚本

复制口令并将其发送给外部 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 模式池

如需对等互连两个 ONTAP 模式池,请执行以下步骤:

  1. 在池 A 上提供:使用池 A 上的 ontap_execute 运行 cluster peer create -generate-passphrase 命令。列出 -peer-addrs 中池 B 的所有 IC-LIF 地址。复制口令。

  2. 在池 B 上接受:在池 B 上运行 peer_cluster.py 并使用 PEER_MODE=accept,并在 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 包含两个以上的节点时,添加其他地址。从命令输出中复制 Passphrase: 值。

如果该命令失败,请在池 A 上使用 peer_cluster.pyPEER_MODE=offer

在池 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"

确认可用性为“可用”且身份验证正常。

对等 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 模式池

如需对等互连两个 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"

确认集群对等可用性为“可用”,并且 SVM 对等状态为“已对等”。

对于方案 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。当 ONTAP 模式池必须接受集群对等互连提议 (PEER_MODE=accept) 时,请在该池上使用此模式。混合对等互连方案 B 使用 accept 模式。仅当 cluster peer create -generate-passphrase 通过 ontap_execute 失败或未返回口令时,才使用 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_TIMEPEER_APPLICATIONS(默认值 snapmirror)、ENCRYPTION_PROPOSEDIPSPACE(默认值 Gcnv)。

SVM 对等互连脚本(后备)

仅当无法通过 ONTAP CLI 代理使用 vserver peer create 时,才使用 peer_svm.py。请改用 vserver peer createvserver peer accept,如对等 SVM 中所述。

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

后续步骤