第313篇:用 RESTCONF 修改接口配置
关键词:RESTCONF、接口配置、PUT、PATCH、DELETE、YANG模型、JSON、HTTP方法
一、引言
第311篇介绍了 RESTCONF 作为基于 HTTP 的 YANG 数据操作协议,第312篇演示了 NETCONF 的配置采集。本篇聚焦 RESTCONF 最实用的场景——修改接口配置。
与 NETCONF 使用 XML + RPC 不同,RESTCONF 将 YANG 模型映射为标准的 RESTful API:每个 YANG 节点对应一个 URL 路径,用 HTTP 方法(GET/POST/PUT/PATCH/DELETE)执行 CRUD 操作,数据以 JSON 格式传输。这种设计使得接口配置修改变得直观且易于集成。
二、核心概念
2.1 RESTCONF URL 映射规则
RESTCONF 的核心设计是将 YANG 数据树映射为 URL 路径:

URL 编码规则:
| YANG 元素 | URL 表示 | 示例 |
|---|---|---|
| 模块节点 | module-name: |
ietf-interfaces: |
| 容器 | /container/ |
/interfaces/ |
| 列表(单实例) | /list= 后跟键值 |
/interface=GigabitEthernet1%2F0%2F1 |
| 列表(多实例) | /list= 后跟键名和值 |
/vlan=vlan-id,100 |
| 叶子 | /leaf |
/description |
| 特殊字符 | URL 编码 | / 编码为 %2F |
2.2 HTTP 方法与配置操作的对应
| HTTP 方法 | 配置操作 | 语义说明 | 响应码 |
|---|---|---|---|
| GET | 读取 | 获取指定资源的当前配置 | 200 OK |
| POST | 创建 | 在集合中创建新资源 | 201 Created |
| PUT | 替换 | 完整替换指定资源的配置 | 204 No Content |
| PATCH | 部分更新 | 仅更新指定的叶子节点 | 204 No Content |
| DELETE | 删除 | 删除指定资源 | 204 No Content |

2.3 PUT vs PATCH 的本质区别
| 操作 | 行为 | 示例 |
|---|---|---|
| PUT | 用请求体中的完整配置替换目标资源,未指定的 leaf 被移除或恢复默认 | 发送 {"name":"GE1/0/1","description":"test"} 会删除该接口的 IP 地址配置 |
| PATCH | 仅更新请求体中指定的 leaf,其他 leaf 保持不变 | 发送 {"description":"test"} 只修改描述,IP 地址不受影响 |
关键原则:修改单个属性用 PATCH;需要完整替换配置用 PUT。误用 PUT 可能导致配置丢失。
三、技术细节
3.1 修改接口描述和启用状态
# scripts/restconf_modify_interface.py
import requests
import json
from requests.auth import HTTPBasicAuth
BASE_URL = "https://10.1.1.1:443/restconf/data"
AUTH = HTTPBasicAuth("admin", "HuaWei@123")
HEADERS = {
"Accept": "application/yang-data+json",
"Content-Type": "application/yang-data+json",
}
SESSION = requests.Session()
SESSION.auth = AUTH
SESSION.verify = False # 生产环境使用证书
def get_interface(name):
"""读取接口当前配置"""
encoded = name.replace("/", "%2F")
url = f"{BASE_URL}/ietf-interfaces:interfaces/interface={encoded}"
resp = SESSION.get(url, headers=HEADERS)
resp.raise_for_status()
return resp.json()
def update_interface_description(name, description):
"""使用 PATCH 修改接口描述(仅更新 description 字段)"""
encoded = name.replace("/", "%2F")
url = f"{BASE_URL}/ietf-interfaces:interfaces/interface={encoded}"
payload = {
"ietf-interfaces:interface": {
"description": description
}
}
resp = SESSION.patch(url, headers=HEADERS, json=payload)
if resp.status_code in (200, 204):
print(f"✓ 接口 {name} 描述已更新为: {description}")
return True
else:
print(f"✗ 更新失败: {resp.status_code} - {resp.text}")
return False
def set_interface_enabled(name, enabled):
"""使用 PATCH 修改接口启用状态"""
encoded = name.replace("/", "%2F")
url = f"{BASE_URL}/ietf-interfaces:interfaces/interface={encoded}"
payload = {
"ietf-interfaces:interface": {
"enabled": enabled
}
}
resp = SESSION.patch(url, headers=HEADERS, json=payload)
if resp.status_code in (200, 204):
action = "启用" if enabled else "禁用"
print(f"✓ 接口 {name} 已{action}")
return True
else:
print(f"✗ {action}失败: {resp.status_code} - {resp.text}")
return False
# 使用示例
interface_name = "GigabitEthernet1/0/1"
# 1. 读取当前配置
print("=== 当前接口配置 ===")
current = get_interface(interface_name)
print(json.dumps(current, indent=2, ensure_ascii=False))
# 2. 修改描述
update_interface_description(interface_name, "Uplink-to-CoreRtr-Modified-by-RESTCONF")
# 3. 禁用接口
set_interface_enabled(interface_name, False)
# 4. 验证修改
print("\n=== 修改后配置 ===")
updated = get_interface(interface_name)
print(json.dumps(updated, indent=2, ensure_ascii=False))
# 5. 恢复启用
set_interface_enabled(interface_name, True)
3.2 修改接口 IP 地址配置
# scripts/restconf_modify_ip.py
import requests
import json
def configure_interface_ip(name, ip_address, prefix_length, description=""):
"""使用 PUT 配置接口的 IP 地址(完整替换 IPv4 配置)"""
encoded = name.replace("/", "%2F")
url = f"{BASE_URL}/ietf-interfaces:interfaces/interface={encoded}"
# 先读取当前配置(保留已有字段)
current_resp = SESSION.get(url, headers=HEADERS)
if current_resp.status_code == 200:
current = current_resp.json()
interface_data = current.get("ietf-interfaces:interface", {})
else:
interface_data = {}
# 构建新配置(合并到现有配置)
new_config = {
"ietf-interfaces:interface": {
**interface_data, # 保留现有配置
"name": name,
"description": description or interface_data.get("description", ""),
"type": interface_data.get("type", "iana-if-type:ethernetCsmacd"),
"enabled": interface_data.get("enabled", True),
"ietf-ip:ipv4": {
"address": [
{
"ip": ip_address,
"netmask": f"/{prefix_length}"
}
]
}
}
}
resp = SESSION.put(url, headers=HEADERS, json=new_config)
if resp.status_code in (200, 204):
print(f"✓ 接口 {name} IP 已配置为 {ip_address}/{prefix_length}")
return True
else:
print(f"✗ IP 配置失败: {resp.status_code} - {resp.text}")
return False
def remove_interface_ip(name):
"""移除接口的 IP 地址配置"""
encoded = name.replace("/", "%2F")
url = f"{BASE_URL}/ietf-interfaces:interfaces/interface={encoded}/ietf-ip:ipv4"
resp = SESSION.delete(url, headers=HEADERS)
if resp.status_code in (200, 204):
print(f"✓ 接口 {name} IP 地址已移除")
return True
else:
print(f"✗ IP 移除失败: {resp.status_code} - {resp.text}")
return False
# 使用示例
configure_interface_ip(
"GigabitEthernet1/0/1",
"10.2.2.1",
30,
"Uplink-Link-10.2.2.0/30"
)
3.3 批量修改多接口配置
# scripts/restconf_batch_interfaces.py
import requests
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
# 批量配置模板
BATCH_CONFIG = [
{
"interface": "GigabitEthernet1/0/1",
"description": "To-CoreRtr-A-GigabitEthernet0/0/1",
"enabled": True,
"mtu": 9000,
"ipv4": {"address": "10.2.2.1", "prefix": 30}
},
{
"interface": "GigabitEthernet1/0/2",
"description": "To-AccessSW-01-GigabitEthernet1/0/1",
"enabled": True,
"mtu": 1500,
"ipv4": {"address": "10.3.3.1", "prefix": 24}
},
{
"interface": "GigabitEthernet1/0/3",
"description": "To-AccessSW-02-GigabitEthernet1/0/1",
"enabled": True,
"mtu": 1500,
"ipv4": {"address": "10.3.4.1", "prefix": 24}
},
]
def apply_interface_config(config_item):
"""应用单个接口的配置"""
encoded = config_item["interface"].replace("/", "%2F")
url = f"{BASE_URL}/ietf-interfaces:interfaces/interface={encoded}"
# 构建 PATCH 请求体
patch_body = {
"ietf-interfaces:interface": {
"description": config_item["description"],
"enabled": config_item["enabled"],
}
}
if "mtu" in config_item:
patch_body["ietf-interfaces:interface"]["ietf-ip:ipv4"] = {
"mtu": config_item["mtu"]
}
# Step 1: PATCH 描述和状态
resp = SESSION.patch(url, headers=HEADERS, json=patch_body)
if resp.status_code not in (200, 204):
return {
"interface": config_item["interface"],
"status": "error",
"error": f"PATCH failed: {resp.status_code}"
}
# Step 2: 配置 IP(如果有)
if "ipv4" in config_item:
ip_url = f"{url}/ietf-ip:ipv4/address=inet%3Aipv4-origin%2C{config_item['ipv4']['address']}"
ip_config = {
"ietf-ip:ip-address": {
"ip": config_item["ipv4"]["address"],
"netmask": f"/{config_item['ipv4']['prefix']}"
}
}
resp = SESSION.put(ip_url, headers=HEADERS, json=ip_config)
if resp.status_code not in (200, 204):
return {
"interface": config_item["interface"],
"status": "error",
"error": f"IP config failed: {resp.status_code}"
}
return {
"interface": config_item["interface"],
"status": "success"
}
# 并发执行
results = []
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [
executor.submit(apply_interface_config, item)
for item in BATCH_CONFIG
]
for future in as_completed(futures):
result = future.result()
results.append(result)
status = "✓" if result["status"] == "success" else "✗"
print(f"{status} {result['interface']}: {result['status']}")
# 汇总
success = sum(1 for r in results if r["status"] == "success")
print(f"\n批量配置完成: {success}/{len(results)} 个接口成功")
3.4 创建和删除 VLAN
# scripts/restconf_vlan_operations.py
import requests
def create_vlan(vlan_id, description=""):
"""创建 VLAN"""
url = f"{BASE_URL}/huawei-vlan:vlan"
payload = {
"huawei-vlan:vlan": {
"vlan-id": vlan_id,
"description": description
}
}
resp = SESSION.post(url, headers=HEADERS, json=payload)
if resp.status_code == 201:
print(f"✓ VLAN {vlan_id} 创建成功")
return True
elif resp.status_code == 409:
print(f"⚠ VLAN {vlan_id} 已存在")
return False
else:
print(f"✗ 创建失败: {resp.status_code} - {resp.text}")
return False
def get_vlan(vlan_id):
"""读取 VLAN 配置"""
url = f"{BASE_URL}/huawei-vlan:vlan={vlan_id}"
resp = SESSION.get(url, headers=HEADERS)
if resp.status_code == 200:
return resp.json()
elif resp.status_code == 404:
return None
else:
raise Exception(f"读取 VLAN 失败: {resp.status_code}")
def delete_vlan(vlan_id):
"""删除 VLAN"""
url = f"{BASE_URL}/huawei-vlan:vlan={vlan_id}"
resp = SESSION.delete(url, headers=HEADERS)
if resp.status_code in (200, 204):
print(f"✓ VLAN {vlan_id} 已删除")
return True
else:
print(f"✗ 删除失败: {resp.status_code} - {resp.text}")
return False
# 批量创建 VLAN
vlan_list = [
(10, "Management"),
(20, "Servers"),
(30, "Voice"),
(100, "Uplink-Trunk"),
(200, "Server-Network"),
]
for vlan_id, desc in vlan_list:
create_vlan(vlan_id, desc)
# 验证
for vlan_id, _ in vlan_list:
vlan = get_vlan(vlan_id)
if vlan:
data = vlan.get("huawei-vlan:vlan", {})
print(f"VLAN {data.get('vlan-id', '?')}: {data.get('description', 'N/A')}")
# 清理:删除测试 VLAN
delete_vlan(200)
3.5 错误处理与批量回滚
# scripts/restconf_error_handling.py
import requests
import json
class RESTCONFError(Exception):
"""RESTCONF 操作异常"""
def __init__(self, status_code, message, url="", method=""):
self.status_code = status_code
self.message = message
self.url = url
self.method = method
super().__init__(f"{method} {url} failed with {status_code}: {message}")
def safe_restconf_request(method, url, payload=None):
"""安全的 RESTCONF 请求,带统一错误处理"""
headers = HEADERS
if payload:
headers["Content-Type"] = "application/yang-data+json"
resp = SESSION.request(method, url, headers=headers, json=payload)
# 成功状态码
if resp.status_code in (200, 201, 204):
return resp
# 解析错误信息
try:
error_data = resp.json()
error_msg = error_data.get("errors", {}).get("error", [{}])[0].get("error-message", resp.text)
except json.JSONDecodeError:
error_msg = resp.text
error_map = {
400: "请求格式错误或参数无效",
403: "权限不足",
404: "请求的资源不存在",
409: "资源冲突(如 VLAN 已存在)",
412: "前置条件不满足",
500: "服务器内部错误",
}
raise RESTCONFError(resp.status_code, error_msg, url, method)
# 使用示例:批量操作 + 自动回滚
def batch_with_rollback(operations, rollback_on_error=True):
"""批量 RESTCONF 操作,失败时回滚"""
applied = []
for op in operations:
try:
result = safe_restconf_request(
op["method"], op["url"], op.get("payload")
)
applied.append(op)
print(f"✓ {op['method']} {op['url']}")
except RESTCONFError as e:
print(f"✗ {e}")
if rollback_on_error and applied:
print("开始回滚已应用的操作...")
# 逆序回滚
for prev_op in reversed(applied):
try:
if prev_op["method"] == "PATCH":
# 对 PATCH 的回滚需要读取旧值并重新 PATCH
print(f" 回滚: {prev_op['method']} {prev_op['url']}")
elif prev_op["method"] == "POST":
safe_restconf_request("DELETE", prev_op["url"])
print(f" 已删除: {prev_op['url']}")
elif prev_op["method"] == "DELETE":
print(f" 恢复: {prev_op['url']} (需手动)")
except Exception as rollback_err:
print(f" 回滚失败: {rollback_err}")
break
return applied
四、实战配置/案例分析
4.1 案例一:接口批量标准化配置
场景:将 50 台交换机的所有 uplink 接口统一配置描述、MTU 和启用状态。
# scripts/interface_standardization.py
import requests
import yaml
import json
from datetime import datetime
def standardize_interfaces(device, uplink_interfaces):
"""标准化配置 uplink 接口"""
base_url = f"https://{device['ip']}:443/restconf/data"
auth = (device["username"], device["password"])
headers = {
"Accept": "application/yang-data+json",
"Content-Type": "application/yang-data+json",
}
results = []
for iface in uplink_interfaces:
encoded = iface.replace("/", "%2F")
url = f"{base_url}/ietf-interfaces:interfaces/interface={encoded}"
try:
session = requests.Session()
session.auth = auth
session.verify = False
# Step 1: 读取当前配置
resp = session.get(url, headers=headers)
if resp.status_code == 404:
results.append({"interface": iface, "status": "not_found"})
continue
current = resp.json()
interface_data = current.get("ietf-interfaces:interface", {})
# Step 2: 构建标准化配置
peer_info = iface.get("peer_description", "")
description = f"To-{device['role']}-{iface['peer_device']}-{peer_info}"
new_config = {
"ietf-interfaces:interface": {
**interface_data,
"name": iface,
"description": description,
"enabled": True,
}
}
# Step 3: PATCH 更新
resp = session.patch(url, headers=headers, json=new_config)
if resp.status_code in (200, 204):
results.append({
"interface": iface,
"status": "success",
"description": description
})
else:
results.append({
"interface": iface,
"status": "error",
"error": resp.text
})
except Exception as e:
results.append({
"interface": iface,
"status": "exception",
"error": str(e)
})
return results
# 设备清单和接口配置
devices = [
{
"ip": "10.1.1.1",
"username": "admin",
"password": "HuaWei@123",
"role": "CoreSW",
"uplink_interfaces": [
"GigabitEthernet1/0/1",
"GigabitEthernet1/0/2",
]
},
]
for device in devices:
print(f"\n配置设备 {device['ip']}...")
results = standardize_interfaces(device, device["uplink_interfaces"])
for r in results:
status = "✓" if r["status"] == "success" else "✗"
print(f" {status} {r['interface']}: {r['status']}")
4.2 案例二:故障案例——PUT 导致 IP 配置丢失
故障现象:运维工程师使用 RESTCONF PUT 修改接口描述,结果接口 IP 地址配置丢失,导致网络中断。
故障复现:
# 错误操作:使用 PUT 只发送 description 字段
# PUT /restconf/data/ietf-interfaces:interfaces/interface=GE1%2F0%2F1
wrong_payload = {
"ietf-interfaces:interface": {
"description": "Modified-description"
# 缺少 name, type, enabled, ipv4 等字段
}
}
# 结果:接口配置被完整替换,IP 地址丢失 → 网络中断
根因分析:
- PUT 操作语义是"完整替换",请求体中未包含的 leaf 会被删除或恢复默认值
- 只发送 description 字段时,ipv4、type、enabled 等字段全部丢失
修复方案:
# 正确操作:方案1 - 使用 PATCH(推荐)
# PATCH /restconf/data/ietf-interfaces:interfaces/interface=GE1%2F0%2F1
correct_payload = {
"ietf-interfaces:interface": {
"description": "Modified-description"
}
}
# 结果:仅更新 description,其他字段不受影响
# 方案2 - 使用 PUT 但包含完整配置
# 先 GET 读取全部配置,修改后再 PUT 回去
current = session.get(url, headers=HEADERS).json()
interface_data = current["ietf-interfaces:interface"]
interface_data["description"] = "Modified-description"
correct_payload = {"ietf-interfaces:interface": interface_data}
session.put(url, headers=HEADERS, json=correct_payload)
预防措施:
| 预防措施 | 说明 |
|---|---|
| 优先使用 PATCH | 修改单个属性时,PATCH 不会删除未指定的字段 |
| PUT 前必须 GET | 如果必须用 PUT,先 GET 获取完整配置,修改后再 PUT |
| 变更前备份 | 使用 GET 保存当前配置快照,出问题时快速回滚 |
| 变更窗口 | 在网络维护窗口内执行 PUT 操作,降低业务影响 |
| 自动化测试 | 在测试环境验证 PUT 请求体包含所有必要字段 |
4.3 案例三:接口配置模板化批量下发
# scripts/interface_template_deploy.py
import requests
import json
from jinja2 import Template
# Jinja2 接口配置模板
INTERFACE_TEMPLATE = Template("""
{
"ietf-interfaces:interface": {
"name": "{{ interface_name }}",
"description": "{{ description }}",
"type": "{{ interface_type }}",
"enabled": {{ enabled|lower }},
{% if ipv4_address %}
"ietf-ip:ipv4": {
"address": [
{
"ip": "{{ ipv4_address }}",
"netmask": "/{{ ipv4_prefix }}"
}
]
}
{% endif %}
}
}
""")
def render_interface_config(interface_name, **kwargs):
"""根据模板渲染接口配置"""
config = INTERFACE_TEMPLATE.render(
interface_name=interface_name,
description=kwargs.get("description", ""),
interface_type=kwargs.get("interface_type", "iana-if-type:ethernetCsmacd"),
enabled=kwargs.get("enabled", True),
ipv4_address=kwargs.get("ipv4_address"),
ipv4_prefix=kwargs.get("ipv4_prefix"),
)
return json.loads(config)
# 使用模板批量配置
interfaces_to_config = [
{
"name": "GigabitEthernet1/0/1",
"description": "To-CoreRtr-A-Loopback0",
"enabled": True,
"ipv4_address": "10.2.2.1",
"ipv4_prefix": 30,
},
{
"name": "GigabitEthernet1/0/2",
"description": "To-AccessSW-01-Uplink",
"enabled": True,
"ipv4_address": "10.3.3.1",
"ipv4_prefix": 24,
},
{
"name": "GigabitEthernet1/0/24",
"description": "Reserved-Port",
"enabled": False,
},
]
for iface_cfg in interfaces_to_config:
encoded = iface_cfg["name"].replace("/", "%2F")
url = f"{BASE_URL}/ietf-interfaces:interfaces/interface={encoded}"
rendered = render_interface_config(iface_cfg["name"], **iface_cfg)
resp = SESSION.patch(url, headers=HEADERS, json=rendered)
status = "✓" if resp.status_code in (200, 204) else "✗"
print(f"{status} {iface_cfg['name']}: {resp.status_code}")
四(续)、高级主题
4.4 RESTCONF 事务与批量操作(yang-patch)
RESTCONF 原生不支持原子事务。当需要"要么全部成功、要么全部回滚"时,可使用 yang-patch(RFC 8072)实现批量变更:
import requests
def apply_yang_patch(session, base_url, patches):
"""
使用 yang-patch 批量应用配置变更。
patches 是一个操作列表,支持 add/remove/replace。
"""
url = f"{base_url}/.yang-patch"
headers = {
"Accept": "application/yang-patch+json",
"Content-Type": "application/yang-patch+json",
}
yang_patch = {
"yang-patch": {
"yang-patch-operation": patches
}
}
resp = session.post(url, headers=headers, json=yang_patch)
return resp.status_code in (200, 204)
# 示例:原子性修改两个接口
patches = [
{
"operation": "replace",
"target": "/ietf-interfaces:interfaces/interface=GE1%2F0%2F1/description",
"source": "To-CoreRtr-A-Uplink"
},
{
"operation": "replace",
"target": "/ietf-interfaces:interfaces/interface=GE1%2F0%2F2/description",
"source": "To-CoreRtr-B-Uplink"
}
]
success = apply_yang_patch(SESSION, BASE_URL, patches)
print(f"{'✓' if success else '✗'} yang-patch 批量操作")
4.5 RESTCONF 配置审计与版本管理
# scripts/restconf_audit.py
import requests
import json
import hashlib
from datetime import datetime
class RESTCONFConfigAuditor:
"""RESTCONF 配置审计器"""
def __init__(self, base_url, username, password):
self.session = requests.Session()
self.session.auth = (username, password)
self.session.verify = False
self.headers = {
"Accept": "application/yang-data+json",
"Content-Type": "application/yang-data+json",
}
self.base_url = base_url
def capture_snapshot(self, paths=None):
"""采集指定 YANG 路径的配置快照"""
if paths is None:
paths = [
"ietf-interfaces:interfaces",
"huawei-vlan:vlans",
]
snapshot = {
"timestamp": datetime.now().isoformat(),
"hashes": {},
"configs": {}
}
for path in paths:
encoded_path = path.replace("/", "%2F")
url = f"{self.base_url}/{encoded_path}"
resp = self.session.get(url, headers=self.headers)
if resp.status_code == 200:
data = resp.json()
config_str = json.dumps(data, sort_keys=True)
snapshot["hashes"][path] = hashlib.sha256(config_str.encode()).hexdigest()[:16]
snapshot["configs"][path] = data
return snapshot
def compare_snapshots(self, old, new):
"""对比两个快照,返回变更摘要"""
changes = []
for path in old["hashes"]:
old_hash = old["hashes"][path]
new_hash = new["hashes"].get(path, "N/A")
if old_hash != new_hash:
changes.append({
"path": path,
"old_hash": old_hash,
"new_hash": new_hash,
"changed": True
})
else:
changes.append({"path": path, "changed": False})
return changes
def save_snapshot(self, snapshot, filename=None):
"""保存快照到文件"""
if filename is None:
filename = f"snapshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(snapshot, f, indent=2, ensure_ascii=False)
return filename
# 使用示例
auditor = RESTCONFConfigAuditor(
"https://10.1.1.1:443/restconf/data",
"admin", "HuaWei@123"
)
# 变更前快照
before = auditor.capture_snapshot()
auditor.save_snapshot(before, "before_change.json")
# ... 执行配置变更 ...
# 变更后快照
after = auditor.capture_snapshot()
auditor.save_snapshot(after, "after_change.json")
# 对比
diff = auditor.compare_snapshots(before, after)
for d in diff:
status = "变更" if d["changed"] else "未变"
print(f"{d['path']}: {status} ({d.get('old_hash', '')} → {d.get('new_hash', '')})")
4.6 RESTCONF vs NETCONF 配置下发对比
| 对比维度 | RESTCONF | NETCONF |
|---|---|---|
| 传输协议 | HTTP/HTTPS | SSH |
| 数据格式 | JSON / XML | XML |
| 操作语义 | RESTful CRUD(GET/PUT/PATCH/POST/DELETE) | RPC(get-config/edit-config) |
| 防火墙穿透 | 简单(443端口) | 需开放830端口 |
| 事务支持 | 有限(yang-patch 补充) | 原生(candidate + commit + rollback) |
| 流式编辑 | 不支持 | 支持( |
| API友好度 | ⭐⭐⭐(天然 REST) | ⭐⭐(XML RPC) |
| 工具生态 | curl、Postman、Python requests | ncclient、XML 解析 |
五、总结
| 操作类型 | HTTP 方法 | 适用场景 | 注意事项 |
|---|---|---|---|
| 读取配置 | GET | 查看当前配置 | 返回 JSON,可直接解析 |
| 修改属性 | PATCH | 修改描述/状态/MTU 等单个属性 | 推荐使用,不会删除其他字段 |
| 完整替换 | PUT | 完整配置接口(含 IP 地址) | 必须先 GET 读取,修改后再 PUT |
| 创建资源 | POST | 创建新 VLAN/接口等 | 在集合节点上使用 |
| 删除资源 | DELETE | 删除 VLAN/IP 地址等 | 不可逆操作,谨慎使用 |
| 最佳实践 | 说明 |
|---|---|
| 修改用 PATCH | 日常配置修改优先使用 PATCH,避免 PUT 的"全量替换"风险 |
| PUT 前备份 | 使用 PUT 时,先 GET 保存当前配置快照 |
| URL 编码 | 接口名中的 / 必须编码为 %2F |
| 错误码处理 | 404 表示资源不存在,409 表示冲突,500 表示服务器错误 |
| 批量操作顺序 | 先创建依赖资源(如 VLAN),再配置引用它的资源(如接口) |
| 会话复用 | 使用 requests.Session() 复用连接,减少 TLS 握手开销 |
思考题
- 使用 RESTCONF 修改接口 IP 地址时,为什么推荐用 PATCH 而不是 PUT?如果用 PUT,需要什么前置步骤?
- RESTCONF 返回 409 Conflict 状态码通常表示什么?如何避免?
- 如果一次 PATCH 请求同时修改 description 和 enabled 两个字段,这两个修改是原子操作吗?
下篇预告:第314篇:OpenConfig 与 IETF 标准模型