第311篇:NETCONF/RESTCONF 协议

关键词

NETCONF、RESTCONF、YANG 驱动、网络配置协议、RPC、Edit-config、Get-config、Python ncclient、Curl


一、NETCONF/RESTCONF 概述

1.1 从 CLI 到 API 的进化

网络管理协议进化:

  ┌──────────────────────────────────────────┐
  │  CLI(手工时代)                          │
  │  ┌─ SSH 登录 + 逐条输入命令              │
  │  ├─ 非结构化文本输出                      │
  │  ├─ 无事务支持                            │
  │  └─ 无标准化错误处理                      │
  │                                            │
  │  SNMP(监控时代)                          │
  │  ├─ MIB 定义,get/set 操作                │
  │  ├─ 只读为主,配置能力弱                   │
  │  ├─ 安全性差(v2c 明文)                   │
  │  └─ 复杂配置难以表达                       │
  │                                            │
  │  NETCONF(自动化时代)                     │
  │  ├─ 基于 XML 的结构化数据                  │
  │  ├─ YANG 数据模型驱动                      │
  │  ├─ 事务支持(commit/rollback)            │
  │  ├─ 候选配置、运行配置分离                 │
  │  └─ SSH 加密传输                          │
  │                                            │
  │  RESTCONF(云原生时代)                    │
  │  ├─ 基于 HTTP REST 风格                   │
  │  ├─ JSON/XML 编码                         │
  │  ├─ 与 Web 技术无缝集成                    │
  │  └─ 适合云原生/微服务架构                  │
  └──────────────────────────────────────────┘

1.2 协议对比

特性 NETCONF RESTCONF
传输协议 端口 编码 操作风格 URL 结构 事务 候选配置 通知 重量级 学习成本 Python 库 SSH 830 XML RPC RPC 方法 ✅ ✅ ✅ 重 较高 ncclient HTTP/HTTPS 443(默认) XML / JSON REST(CRUD) 路径=YANG 节点 ❌ ❌ ❌ 轻 较低 requests

选择建议: ┌─ 需要事务/回滚 → NETCONF ├─ 简单 API 调用 → RESTCONF ├─ Python 编程 → 两者均可 └─ 集成 Web 应用 → RESTCONF


二、NETCONF 协议详解

2.1 NETCONF 协议栈

NETCONF 协议层次:

  ┌──────────────────────────────────────────┐
  │  内容层(Content)                        │
  │  ┌─ YANG 数据模型定义的配置/状态数据     │
  │  └─ XML 编码                             │
  │                                            │
  │  操作层(Operations)                     │
  │  ┌─ <get>:获取配置和状态数据            │
  │  ├─ <get-config>:获取配置数据           │
  │  ├─ <edit-config>:修改配置              │
  │  ├─ <copy-config>:复制配置              │
  │  ├─ <delete-config>:删除配置            │
  │  ├─ <lock>/<unlock>:配置锁定            │
  │  ├─ <commit>:提交候选配置               │
  │  ├─ <discard-changes>:丢弃未提交变更    │
  │  └─ <rpc>:自定义 RPC 操作               │
  │                                            │
  │  消息层(Messages)                       │
  │  ┌─ <hello>:能力交换                    │
  │  ├─ <rpc>/<rpc-reply>:请求/响应         │
  │  └─ <notification>:事件通知             │
  │                                            │
  │  安全传输层(Transport)                  │
  │  └─ SSH(RFC 6242)、TLS(RFC 7589)     │
  └──────────────────────────────────────────┘

2.2 NETCONF 能力(Capabilities)

<!-- NETCONF 能力交换(Hello 消息) -->
<!-- 客户端发送 -->
<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <capabilities>
    <capability>
      urn:ietf:params:netconf:base:1.1
    </capability>
  </capabilities>
</hello>

<!-- 服务器响应 -->
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <capabilities>
    <capability>
      urn:ietf:params:netconf:base:1.1
    </capability>
    <capability>
      urn:ietf:params:netconf:capability:validate:1.1
    </capability>
    <capability>
      urn:ietf:params:netconf:capability:candidate:1.1
    </capability>
    <capability>
      urn:ietf:params:netconf:capability:confirmed-commit:1.0
    </capability>
    <capability>
      urn:ietf:params:netconf:capability:rollback-on-error:1.0
    </capability>
    <!-- 支持的 YANG 模块 -->
    <capability>
      urn:ietf:params:xml:ns:yang:ietf-interfaces?module=ietf-interfaces
    </capability>
  </capabilities>
</hello>

2.3 NETCONF 常用操作

<!-- ===== 获取运行配置 ===== -->
<rpc message-id="101"
     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <get-config>
    <source>
      <running/>
    </source>
    <filter type="subtree">
      <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"/>
    </filter>
  </get-config>
</rpc>

<!-- ===== 修改配置 ===== -->
<rpc message-id="102"
     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <edit-config>
    <target>
      <candidate/>
    </target>
    <config>
      <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
        <interface>
          <name>GigabitEthernet0/0/1</name>
          <description>UPLINK-TO-CORE</description>
          <enabled>true</enabled>
          <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
            <address>
              <ip>10.0.1.1</ip>
              <prefix-length>24</prefix-length>
            </address>
          </ipv4>
        </interface>
      </interfaces>
    </config>
  </edit-config>
</rpc>

<!-- ===== 提交配置 ===== -->
<rpc message-id="103"
     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <commit/>
</rpc>

<!-- ===== 丢弃变更 ===== -->
<rpc message-id="104"
     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <discard-changes/>
</rpc>

<!-- ===== 自定义 RPC(设备重启) ===== -->
<rpc message-id="105"
     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <reboot xmlns="http://example.com/ns/reboot">
    <reason>Maintenance</reason>
  </reboot>
</rpc>

三、Python ncclient 实战

3.1 连接与基础操作

#!/usr/bin/env python3
# ncclient_basic.py — NETCONF 基础操作

# 安装:python -m pip install ncclient

from ncclient import manager
import xml.dom.minidom

# NETCONF 连接参数
NETCONF_PARAMS = {
    "host": "192.168.1.1",
    "port": 830,                # NETCONF 默认端口
    "username": "admin",
    "password": "admin123",
    "hostkey_verify": False,    # 跳过主机密钥检查
    "device_params": {"name": "huawei"},
    "timeout": 30,
}

def pretty_xml(xml_str):
    """美化 XML 输出"""
    dom = xml.dom.minidom.parseString(xml_str)
    return dom.toprettyxml(indent="  ")

try:
    with manager.connect(**NETCONF_PARAMS) as m:
        print("✓ NETCONF 连接成功")

        # 1. 获取服务器能力
        print(f"\n  服务器能力数: {len(m.server_capabilities)}")
        for cap in list(m.server_capabilities)[:5]:
            print(f"    {cap}")

        # 2. 获取运行配置(接口部分)
        filter_xml = """
        <filter type="subtree">
          <interfaces
            xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"/>
        </filter>
        """
        result = m.get_config("running", filter_xml)
        print("\n=== 运行配置(接口)===")
        print(pretty_xml(result.xml))

        # 3. 获取所有配置和状态
        result = m.get()
        print("\n=== 全部数据(摘要)===")
        # 只打印前 500 字符
        print(result.xml[:500])

except Exception as e:
    print(f"✗ 错误: {e}")

3.2 配置管理

#!/usr/bin/env python3
# ncclient_config.py — NETCONF 配置管理

from ncclient import manager
from ncclient.operations import RPCError
import xml.dom.minidom

NETCONF_PARAMS = {
    "host": "192.168.1.1",
    "port": 830,
    "username": "admin",
    "password": "admin123",
    "hostkey_verify": False,
    "device_params": {"name": "huawei"},
}

def create_vlan_xml(vlan_id, vlan_name):
    """生成 VLAN 配置 XML"""
    return f"""
    <config>
      <vlan xmlns="http://www.huawei.com/netconf/vrp/vlan">
        <vlans>
          <vlan>
            <id>{vlan_id}</id>
            <name>{vlan_name}</name>
            <type>common</type>
            <status>enable</status>
          </vlan>
        </vlans>
      </vlan>
    </config>
    """

def configure_interface_xml(if_name, desc, mode):
    """生成接口配置 XML"""
    return f"""
    <config>
      <interfaces
        xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
        <interface>
          <name>{if_name}</name>
          <description>{desc}</description>
          <type
            xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">
            ianaift:ethernetCsmacd
          </type>
          <enabled>true</enabled>
        </interface>
      </interfaces>
    </config>
    """

try:
    with manager.connect(**NETCONF_PARAMS) as m:

        # ===== 候选配置模式 =====
        # 1. 锁定配置
        m.lock(target="candidate")
        print("🔒 配置已锁定")

        # 2. 创建 VLAN
        vlan_xml = create_vlan_xml(100, "VLAN100_SALES")
        print(f"  加载 VLAN 配置...")
        m.edit_config(
            target="candidate",
            config=vlan_xml,
            default_operation="merge",
        )

        # 3. 配置接口
        intf_xml = configure_interface_xml(
            "GigabitEthernet0/0/1",
            "Link to CORE",
            "trunk",
        )
        print(f"  加载接口配置...")
        m.edit_config(
            target="candidate",
            config=intf_xml,
            default_operation="merge",
        )

        # 4. 验证候选配置
        result = m.get_config("candidate")
        print("\n=== 候选配置(摘要)===")
        print(pretty_xml(result.xml[:1000]))

        # 5. 提交配置
        m.commit()
        print("\n✓ 配置已提交")

        # 6. 解锁
        m.unlock(target="candidate")
        print("🔓 配置已解锁")

except RPCError as e:
    print(f"✗ NETCONF RPC 错误: {e}")
    # 回滚
    with manager.connect(**NETCONF_PARAMS) as m:
        m.discard_changes()
        print("已丢弃候选配置变更")
except Exception as e:
    print(f"✗ 错误: {e}")

3.3 批量操作

#!/usr/bin/env python3
# ncclient_batch.py — NETCONF 批量操作

from ncclient import manager
from concurrent.futures import ThreadPoolExecutor, as_completed
import time

DEVICES = [
    {"host": "192.168.1.1", "name": "SW01"},
    {"host": "192.168.1.2", "name": "SW02"},
    {"host": "192.168.1.3", "name": "SW03"},
]

COMMON_CREDENTIALS = {
    "port": 830,
    "username": "admin",
    "password": "admin123",
    "hostkey_verify": False,
    "device_params": {"name": "huawei"},
}

def collect_device_info(device):
    """单设备采集(使用 NETCONF)"""
    try:
        with manager.connect(
            host=device["host"],
            **COMMON_CREDENTIALS,
        ) as m:
            # 获取系统信息
            filter_xml = """
            <filter type="subtree">
              <system
                xmlns="http://www.huawei.com/netconf/vrp/system"/>
            </filter>
            """
            result = m.get_config("running", filter_xml)

            # 获取接口数量
            if_filter = """
            <filter type="subtree">
              <interfaces
                xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"/>
            </filter>
            """
            if_result = m.get_config("running", if_filter)

            return {
                "host": device["host"],
                "name": device["name"],
                "success": True,
                "config_size": len(result.xml),
                "interface_count": if_result.xml.count("<interface>"),
            }
    except Exception as e:
        return {
            "host": device["host"],
            "name": device["name"],
            "success": False,
            "error": str(e),
        }

# 并行采集
print("=== NETCONF 批量采集 ===")
start = time.time()

with ThreadPoolExecutor(max_workers=3) as ex:
    futures = {ex.submit(collect_device_info, dev): dev for dev in DEVICES}
    for future in as_completed(futures):
        result = future.result()
        status = "✓" if result["success"] else "✗"
        name = result["name"]
        if result["success"]:
            print(f"[{status}] {name}: {result['interface_count']} 接口, "
                  f"配置 {result['config_size']} 字节")
        else:
            print(f"[{status}] {name}: {result['error']}")

elapsed = time.time() - start
print(f"\n总耗时: {elapsed:.1f} 秒")

四、RESTCONF 实战

4.1 RESTCONF 基础

RESTCONF 的 RESTful 操作映射:

  HTTP 方法    CRUD        RESTCONF 操作
  ─────────────────────────────────────
  GET          Read        获取配置或状态数据
  POST         Create      创建数据资源
  PUT          Update/Repl 替换整个资源
  PATCH        Partial Upd 部分更新资源
  DELETE       Delete      删除资源

  URL 结构映射 YANG 路径:
  GET /restconf/data/ietf-interfaces:interfaces
  GET /restconf/data/ietf-interfaces:interfaces/interface=GE0/0/1

  返回格式:
  Accept: application/yang-data+json
  Accept: application/yang-data+xml

4.2 Python requests 操作 RESTCONF

#!/usr/bin/env python3
# restconf_basic.py — RESTCONF 操作

import requests
import json

# RESTCONF 配置
RESTCONF_BASE = "https://192.168.1.1/restconf"
USERNAME = "admin"
PASSWORD = "admin123"

# 禁用 SSL 警告(生产环境使用证书)
requests.packages.urllib3.disable_warnings()

HEADERS_JSON = {
    "Accept": "application/yang-data+json",
    "Content-Type": "application/yang-data+json",
}

def restconf_get(path):
    """RESTCONF GET 请求"""
    url = f"{RESTCONF_BASE}{path}"
    resp = requests.get(
        url,
        auth=(USERNAME, PASSWORD),
        headers=HEADERS_JSON,
        verify=False,
    )
    return resp

def restconf_put(path, data):
    """RESTCONF PUT 请求(全量替换)"""
    url = f"{RESTCONF_BASE}{path}"
    resp = requests.put(
        url,
        auth=(USERNAME, PASSWORD),
        headers=HEADERS_JSON,
        json=data,
        verify=False,
    )
    return resp

def restconf_patch(path, data):
    """RESTCONF PATCH 请求(部分更新)"""
    url = f"{RESTCONF_BASE}{path}"
    resp = requests.patch(
        url,
        auth=(USERNAME, PASSWORD),
        headers=HEADERS_JSON,
        json=data,
        verify=False,
    )
    return resp

def restconf_post(path, data):
    """RESTCONF POST 请求(创建)"""
    url = f"{RESTCONF_BASE}{path}"
    resp = requests.post(
        url,
        auth=(USERNAME, PASSWORD),
        headers=HEADERS_JSON,
        json=data,
        verify=False,
    )
    return resp

def restconf_delete(path):
    """RESTCONF DELETE 请求"""
    url = f"{RESTCONF_BASE}{path}"
    resp = requests.delete(
        url,
        auth=(USERNAME, PASSWORD),
        headers=HEADERS_JSON,
        verify=False,
    )
    return resp

# ===== 操作示例 =====
try:
    # 1. GET: 获取所有接口
    print("=== 获取接口列表 ===")
    resp = restconf_get("/data/ietf-interfaces:interfaces")
    if resp.status_code == 200:
        data = resp.json()
        for intf in data.get("ietf-interfaces:interfaces", {}).get("interface", []):
            print(f"  {intf['name']}: enabled={intf.get('enabled')}")
    else:
        print(f"  GET 失败: {resp.status_code}")

    # 2. GET: 获取单个接口
    print("\n=== 获取单个接口 ===")
    resp = restconf_get(
        "/data/ietf-interfaces:interfaces/"
        "interface=GigabitEthernet0/0/1"
    )
    if resp.status_code == 200:
        print(json.dumps(resp.json(), indent=2))

    # 3. PUT: 创建/更新接口
    print("\n=== 创建接口 LoopBack100 ===")
    new_intf = {
        "ietf-interfaces:interface": {
            "name": "LoopBack100",
            "description": "RESTCONF Created Loopback",
            "type": "iana-if-type:softwareLoopback",
            "enabled": True,
            "ietf-ip:ipv4": {
                "address": [
                    {
                        "ip": "100.100.100.1",
                        "prefix-length": 32,
                    }
                ]
            },
        }
    }
    resp = restconf_put(
        "/data/ietf-interfaces:interfaces/"
        "interface=LoopBack100",
        new_intf,
    )
    print(f"  PUT 结果: {resp.status_code}")

    # 4. PATCH: 更新接口描述
    print("\n=== 更新接口描述 ===")
    update = {
        "ietf-interfaces:interface": {
            "name": "GigabitEthernet0/0/1",
            "description": "Updated via RESTCONF PATCH",
        }
    }
    resp = restconf_patch(
        "/data/ietf-interfaces:interfaces/"
        "interface=GigabitEthernet0/0/1",
        update,
    )
    print(f"  PATCH 结果: {resp.status_code}")

    # 5. GET: 验证变更
    print("\n=== 验证变更 ===")
    resp = restconf_get(
        "/data/ietf-interfaces:interfaces/"
        "interface=GigabitEthernet0/0/1"
    )
    if resp.status_code == 200:
        data = resp.json()
        iface = data.get("ietf-interfaces:interface", {})
        print(f"  描述: {iface.get('description')}")
        print(f"  启用: {iface.get('enabled')}")

except requests.exceptions.ConnectionError:
    print("✗ 连接失败,请检查 RESTCONF 是否启用")
except Exception as e:
    print(f"✗ 错误: {e}")

4.3 使用 curl 操作 RESTCONF

# curl 操作 RESTCONF 示例

# GET: 获取接口列表
curl -k -u admin:admin123 \
  -H "Accept: application/yang-data+json" \
  https://192.168.1.1/restconf/data/ietf-interfaces:interfaces

# GET: 获取特定接口
curl -k -u admin:admin123 \
  -H "Accept: application/yang-data+json" \
  "https://192.168.1.1/restconf/data/ietf-interfaces:interfaces/interface=GE0/0/1"

# PUT: 创建接口
curl -k -u admin:admin123 \
  -H "Accept: application/yang-data+json" \
  -H "Content-Type: application/yang-data+json" \
  -X PUT \
  -d '{
    "ietf-interfaces:interface": {
      "name": "LoopBack200",
      "description": "Created via curl",
      "type": "iana-if-type:softwareLoopback",
      "enabled": true
    }
  }' \
  "https://192.168.1.1/restconf/data/ietf-interfaces:interfaces/interface=LoopBack200"

# DELETE: 删除接口
curl -k -u admin:admin123 \
  -X DELETE \
  "https://192.168.1.1/restconf/data/ietf-interfaces:interfaces/interface=LoopBack200"

# GET: 获取 YANG 模块列表
curl -k -u admin:admin123 \
  -H "Accept: application/yang-data+json" \
  https://192.168.1.1/restconf/data/ietf-yang-library:modules-state

五、华为设备 NETCONF 配置

5.1 设备端配置

# 华为设备开启 NETCONF(SSH)

# 1. 使能 NETCONF 服务
system-view
  netconf ssh server enable
  netconf ssh server port 830

# 2. 创建 NETCONF 用户
  aaa
    local-user netconf_admin password cipher Admin@123
    local-user netconf_admin service-type ssh
    local-user netconf_admin privilege level 15
    quit

# 3. SSH 用户认证
  ssh user netconf_admin
  ssh user netconf_admin authentication-type password
  ssh user netconf_admin service-type netconf
  quit

# 4. 使能 YANG 模型
  netconf
    yang-module load ietf-interfaces
    yang-module load ietf-ip
    yang-module load huawei-vlan
    commit

5.2 检查 NETCONF 状态

# 查看 NETCONF 会话
display netconf session

# 查看 NETCONF 统计
display netconf statistics

# 查看加载的 YANG 模块
display netconf yang-module

# 调试 NETCONF(troubleshooting)
debugging netconf all
terminal debugging

六、NETCONF vs RESTCONF 选型

6.1 场景对比

选择 NETCONF 的场景:
  ┌─ 配置变更需要事务(全部成功或全部回滚)
  ├─ 使用候选配置(先编辑,后提交)
  ├─ 需要配置锁定(防止冲突)
  ├─ 大量配置变更(批量 edit-config)
  └─ 需要 NETCONF 通知(事件订阅)

选择 RESTCONF 的场景:
  ┌─ 简单的 CRUD 操作
  ├─ 与 Web/云应用集成
  ├─ 需要 JSON 格式(非 XML)
  ├─ 脚本语言快速调用(Python/JavaScript/curl)
  └─ 微服务架构中的配置 API

可以两者都用:
  ┌─ 事务性配置变更用 NETCONF
  ├─ 日常监控/查询用 RESTCONF
  └─ 先 RESTCONF 快速开发,关键操作用 NETCONF

七、最佳实践

7.1 NETCONF 使用规范

NETCONF 最佳实践:

  1. 连接管理
  ┌─ 使用 with 语句自动释放连接
  ├─ 设置合理的 timeout(30-60s)
  ├─ 批量操作复用连接
  └─ 捕获 RPCError 做回滚

  2. 配置变更
  ┌─ 优先用候选配置(candidate)
  ├─ 变更前 lock 防止冲突
  ├─ 变更后 validate 校验
  ├─ commit 确认提交
  └─ 失败时 discard-changes

  3. XML 处理
  ┌─ 使用 lxml 或 xml.dom 处理 XML
  ├─ 模板方式生成 XML(Jinja2)
  ├─ filter 减少传输数据量
  └─ 使用 pretty_xml 调试

  4. 错误处理
  ┌─ 检查 rpc-reply 的 ok/error 元素
  ├─ 解析 error-tag/error-message
  ├─ 配置事务回滚
  └─ 日志记录完整 XML 交互

7.2 安全考量

NETCONF/RESTCONF 安全:

  NETCONF:
  ┌─ SSH 传输层加密(默认)
  ├─ 设备本地用户认证
  ├─ 配置锁定防冲突
  └─ 操作审计日志

  RESTCONF:
  ┌─ HTTPS 加密(必须启用)
  ├─ 基本/摘要/Digest 认证
  ├─ 使用证书认证(推荐)
  ├─ RBAC 权限控制
  └─ API 限流保护

  生产环境:
  ┌─ 管理网络隔离
  ├─ 使用跳板机
  ├─ 日志监控异常操作
  └─ 定期轮换密码/证书

八、总结

NETCONF/RESTCONF 的定位:

  NETCONF — 面向设备配置的"专业协议"
  ┌─ 强一致性:事务 + 候选 + 回滚
  ├─ 标准化:YANG 模型驱动
  ├─ 安全性:SSH 加密传输
  └─ 适用:核心网变更、批量配置

  RESTCONF — 面向应用的"通用 API"
  ┌─ 轻量级:HTTP REST 风格
  ├─ 灵活性:JSON/XML 可选
  ├─ 易集成:与 Web 技术无缝对接
  └─ 适用:监控平台、云编排

  CLI → SSH/Paramiko(逐条命令,文本解析)
  │
  ├─ → NETCONF(结构化 XML,事务支持)
  │
  ├─ → RESTCONF(REST API,JSON 编码)
  │
  └─ → gNMI(gRPC,Protobuf,流式)

下篇预告:第312篇 — gNMI/gNOI 协议,将介绍 Google 推出的 gRPC 网络管理接口,如何用 Protobuf 和流式传输实现高效的网络设备管理。