python网络自动化netconf配置模块ncclient学习笔记

ncclient介绍

ncclient简介:

ncclient是一个用于NETCONF客户端的Python库。它旨在体用一个直观的API,将NETCONF的XML编码特性映射到Python构造和习语,并使编写网络管理脚本更容易。

其他主要功能有:

  • 支持RFC 4741中定义的所有操作和功能。
  • 管道请求。
  • 异步RPC请求。
  • 保持XML的方式,除非真正需要变更。
  • 扩展。可以轻松添加新的传输映射和功能/操作。

Netconf相关原理,概念:

netconf学习笔记: https://cshihong.github.io/2019/12/29/Netconf%E5%8D%8F%E8%AE%AE%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/

ncclient主要使用python实现了netconf的相关操作。

ncclient库学习

manager文件内主要功能:

这个模块是周围库的一个抽象层,它公开所有的核心功能。

manager支持的操作:

manager中的操作,都是映射到ncclient.operations.xxx对应的calss。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
manager.OPERATIONS
Out[10]:
{'get': ncclient.operations.retrieve.Get,
'get_config': ncclient.operations.retrieve.GetConfig,
'get_schema': ncclient.operations.retrieve.GetSchema,
'dispatch': ncclient.operations.retrieve.Dispatch,
'edit_config': ncclient.operations.edit.EditConfig,
'copy_config': ncclient.operations.edit.CopyConfig,
'validate': ncclient.operations.edit.Validate,
'commit': ncclient.operations.edit.Commit,
'discard_changes': ncclient.operations.edit.DiscardChanges,
'cancel_commit': ncclient.operations.edit.CancelCommit,
'delete_config': ncclient.operations.edit.DeleteConfig,
'lock': ncclient.operations.lock.Lock,
'unlock': ncclient.operations.lock.Unlock,
'create_subscription': ncclient.operations.subscribe.CreateSubscription,
'close_session': ncclient.operations.session.CloseSession,
'kill_session': ncclient.operations.session.KillSession,
'poweroff_machine': ncclient.operations.flowmon.PoweroffMachine,
'reboot_machine': ncclient.operations.flowmon.RebootMachine}

常见方法:

def make_device_handler(device_params):

创建一个设备处理对象,该对象提供设备的特殊参数和功能。它在其ncclient的代码中多次调用。

如果device_params 参数没有定义,或者这个参数对应的值在参数字典中不知道,那么会返会一个默认的处理程序。

使用实例:

1
manager.make_device_handler({'name':'huawei'})
  • Juniper: device_params={‘name’:’junos’}
  • Cisco CSR: device_params={‘name’:’csr’}
  • Cisco Nexus: device_params={‘name’:’nexus’}
  • Huawei: device_params={‘name’:’huawei’}
  • H3C: device_params={‘name’:’h3c’}

def connect_ssh(*args, **kws):

初始化一个连接,通过:class:Manager over the SSH transport.

通过class:ncclient.transport.SSHSession实现。

return Manager(session, device_handler, **manager_params)

def connect_ioproc(*args, **kwds):

调用ncclient.thransport.third_party。

return Manager(session, device_handler, **manager_params)

def connect(*args, **kwds):

如果host为localhosthe和device_param为junos/local

return connect_ioproc(*args, **kwds)

否则:

return connect_ssh(*args, **kwds)

class Manager(object):

将RPC操作的API作为方法公开调用。这些方法的返回类型取决于我们是处于异步模式(asynchronous mode)还是同步模式(synchronous mode)。

  • asynchronous mode(同步模式下):

    同步模式下等待应答并返回相应的RPCReply对象。根据异常引发模式的不同,应答中的rpc错误可能会被引发为RPCError异常。

  • asynchronous mode(异步模式):

    异步模式下操作立即返回相应的RPC对象。错误处理和检查是否已收到回复必须手动处理。

注意:在get()和get_config()操作中,reply是GetReply 的一个实例, 它公开的附加属性data (as Element )和data_xml (作为string ),这是这些操作的主要关注点。

在传输层错误的情况下,例如意外的会话关闭,TransportError 将被触发。

有关预期的操作行为和其他参数在:rfc4741中。

Manager实例也是上下文管理器,所以你可以这样使用它:

1
2
3
4
5
6
7
8
9
10
with manager.connect("host") as m:
# do your stuff

#or like this::

m = manager.connect("host")
try:
# do your stuff
finally:
m.close_session()

def get():

检索运行配置和设备状态信息。 查询的是设备当前运行的状态数据,即只能从配置数据库中获取数据。

不需要使用source参数指定配置数据库。

操作成功,Server回复的元素中含有参数,中封装了获取的结果数据。否则在消息中返回

def get_config(source, filter=None)

检索指定配置的全部或部分。

  • source:指定需要查询的数据库名称。有running (正在运行的数据库) ,startup (下次设备启动配置数据库), candidate (两阶段运行数据库,需要commit提交生效)

  • fileter: 过滤器。过滤器可以采用如下方式:

    • A tuple of (type, criteria)

      这里的type必须是xpath 或者subtree.

      对于“xpathcriteria 应该是包含XPath表达式的字符串。

      对于substreecriteria应该是一个XML字符串或一个包含标准的元素对象。

    • element 作为XML 字符串的一个Element 对象。

def edit_config(target, config, default_operation=None, test_option=None, error_option=None):

将指定配置的全部或部分加载到目标配置数据存储中。

  • target:指定要配置的数据库。

  • config:必须放在元素中, 它可以指定为字符串或Element

  • default_operation: 如果指定,必须是{ “merge”, “replace”, or “none” } 其中之一。

  • test_option: { “test_then_set”, “set” } 之一。

  • error_option: { “stop-on-error”, “continue-on-error”, “rollback-on-error” } 之一。

    The “rollback-on-error” error_option :rollback-on-error capability.

def copy_config(source, target):

源配置数据库替换目标配置数据库。如果目标配置数据库没有创建,则直接创建配置数据库,否则用源配置数据库直接覆盖目标配置数据库。

  • source: 是配置数据存储的名称,用作包含要复制的配置子树的复制操作或配置元素的源 。
  • target: 是要用作复制操作目标的配置数据存储的名称 。

def delete_config(target):

删除配置数据库。

  • target: target指定要删除的配置数据存储的名称或URL

def dispatch(rpc_command, source=None, filter=None):

  • rpc_command:指定以纯文本或xml元素格式(取决于命令)发送rpc命令

例如:

1
dispatch('clear-arp-table')

或者:

1
2
3
4
xsd_fetch = new_ele('get-xnm-information')
sub_ele(xsd_fetch, 'type').text="xml-schema"
sub_ele(xsd_fetch, 'namespace').text="junos-configuration"
dispatch(xsd_fetch)

def lock(target):

允许客户端锁定设备的配置系统。

def unlock(target):

释放一个配置锁。

def locked(self, target):

返回一个上锁的数据库的上下文件管理器,对应执行操作是:

return operations.LockContext(self._session, self._device_handler, target)

target : 是要锁定的配置数据存储的名称,

1
2
3
4
5
6
7
8
9
with m.locked("running"):
# do your stuff

# ... instead of::
m.lock("running")
try:
# do your stuff
finally:
m.unlock("running")

def take_notification(self, block=True, timeout=None)

尝试从接收的队列中检索一个通知,如果block为True,则调用将等待直到通知收到为止。如果timeout是一个大于0的数字,调用将等待它超时前收到通知的时间

如果在block为False或时没有可用的通知超时后,将不返回任何值。

return self._session.take_notification(block, timeout)

def close_session():

请求优雅地终止NETCONF会话,并关闭传输层会话。

def kill_session(session_id):

强制去终止一个NETCONF会话。

  • session_id:要终止会话的NETCONF会话标识符。

def commit(confirmed=False, timeout=None, persist=None):

数据库配置数据提交,转化为设备的新当运行的配置即。取决于:candidate capability

  • timeout: 操作确认超时时间,单位是秒,缺省值是600秒。设备执行操作后,在确认超时时间内,如果没有执行确认操作,则对数据库中的配置进行回滚,配置数据恢复到执行操作之前的状态,并放弃数据库中的编辑数据。

  • persist: 使已确认的提交在会话终止后继续执行,并在正在进行的已确认的提交上设置token 。

def discard_changes():

将候选配置还原为当前正在运行的配置。任何未提交的更改都会被丢弃。

def validate(source):

验证指定配置的内容。

  • source 是要验证的配置数据存储或包含要验证的配置子树的配置元素的名称

self._session._server_capabilities

返会服务器支持的能力集。

1
2
3
4
5
6
for i in m._session._server_capabilities:
print(i)
urn:ietf:params:netconf:base:1.0
urn:ietf:params:netconf:base:1.1
urn:ietf:params:netconf:capability:writable-running:1.0
....

self._session._client_capabilities

返会客户端支持的能力集。

1
2
3
4
5
6
7
for i in m._session._client_capabilities:
print(i)

urn:ietf:params:netconf:base:1.0
urn:ietf:params:netconf:base:1.1
urn:ietf:params:netconf:capability:writable-running:1.0
....

self._session.id

返会一个seesion.id 。

self._session.connected

判断seesion是否已经建立。

raise_mode:

指定哪些错误引发RPCError 异常。有效值是在RaiseMode 中定义的常量。默认值是ALL

timeout:

指定同步RPC请求的超时。

async_mode:

指定操作是异步执行的(True)还是同步执行的(False)(缺省值)。

capbilities 文件主要功能;

表示netconf的能力集。

ncclient.capabilities.schemes(url_uri):

给定一个具有scheme查询字符串的URI(即:url capability URI),将返回一个受支持的模式列表。

class ncclient.capabilities.Capabilities(capabilities) :

表示NETCONF客户端或服务器可用的能力集。它是由一个capability URI列表初始化的 。

“cap” in caps

可以通过":cap" in caps 判断一个能力集是否是客户端或者服务器所支持的能力集。cap也能简写。

除了URI之外,对于urn:ietf:params:netconf:capability:$name:$version表示的能力,它们的简写可以用作键。例如,对于urn:ietf:params:netconf:capability:candidate:1.0,简写应该是:candidate。如果版本是重要的,使用:candidate:1.0作为键。

iter(caps):

返会能力集的迭代对象。

xml_文件主要功能:

用于创建、解析和处理XML和ElementTree对象的方法。

XML异常:

exception ncclient.xml_.XMLError , 基于:ncclient.NCClientError

def to_xml(ele, enconding=”UTF-8”, pretty_print=False):

转换并返回具有指定编码的ele(Element)的XML。

def to_ele(x, huge_tree=False):

转换并返回XML文档x的元素。

ncclient.xml_.parse_root(raw):

有效地解析原始XML文档的根元素,返回其限定名和属性字典的元组。

ncclient.xml_.validated_element(x, tags=None, attrs=None**)** :

检查XML文档或元素的根元素是否满足提供的条件。

  • tags: (如果指定)是单个允许的标记名或一系列允许的替代项
  • attrs: (如果指定)是所需属性的序列,每个属性都可以是多个允许的替代项的序列
  • 如果不满足要求,则引发XMLError。

transport-传输/会话层:

主要实现了NETCONF传输层连接。

session.py:

class ncclient.transport.Session(capabilities)

传输协议实现使用的基类。

def add_listener(listener):

注册一个监听器,它将收到传入消息和错误的通知。

def client_capabilities():

客户端的能力集。

def connected():

返回会话的连接状态。

def get_listener_instance(cls):

如果注册了指定类型的侦听器,则返回实例 。

def id():

表示会话id的字符串。如果没有初始化,为None。

def remove_listener(listener):

注销一些监听器;如果监听器没有被注册,将被忽略掉。

def server_capabilities():

返回服务器支持的能力集。

class ncclient.transport.SeesionListener:

会话监听器的基类,当接收到新的NETCONF消息或发生错误时,会通知该类。

def callback(root, raw)

收到新的XML文档时调用。 root参数允许回调确定是否要进一步处理文

  • root:是一个元组(tag, arrtibutes)类型。

    tag: 根元素限定名。

    attributes:是属性名的字典。

  • row : 以字符串的形式包含XML文档。

def errback(ex):

发生错误时调用。

ssh.py:

是ssh会话的实现。

ssh.default_unknow_host_cb(host, fingerprint):

如果未知的主机回调发现可接受的密钥,则返回True,否则返回False。

默认返回False,可能导致connet()引发SSHUnknowHost 异常。

如果需要以编程方式验证主机密钥,需要供另一个有效的回调。

  • host:需要验证的主机名

  • fingerprint:表示主机指纹的16进制字符串。用冒号分隔。

    例如: “4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21”

class ncclient.transport.SSHSession(device_handler):

实现通过ssh的netconf会话连接。

1
connect(host[, port=830, timeout=None, unknown_host_cb=default_unknown_host_cb, username=None, password=None, key_filename=None, allow_agent=True, look_for_keys=True, ssh_config=None])

通过SSH连接并初始化NETCONF会话。首先尝试公钥身份验证方法,然后尝试密码身份验证。

若要完全禁用公钥尝试身份验证,设置 allow_agent和look_for_keys作为False调用 。

  • host:要连接的主机名或者IP地址。
  • port:连接的端口,默认830。有些设备可能是22。网络设备也能配置自定义端口。根据配置情况进行指定。
  • timeout:超时时间,是socket连接的可选超时。
  • unknow_host_cd: 当服务器主机密钥无法识别时调用。有两参数。host,fingerprint。
  • username: 连接ssh认证的用户名。
  • password:ssh用户密码。
  • key_filename: 秘钥文件。
  • allow_agent: 是否启用SSH代理。
  • host_for_keys: 允许在~/.ssh/id_* 寻找ssh密钥。
  • ssh_config: 对OpenSSH配置文件解析,设置路径,例如:~/.ssh/config

def load_known_hosts(filename=None):

从指定文件中加载主机密钥。从而可以多次调用。如果未指定文件路径,会使用默认位置:~/.ssh/know_hosts and ~/ssh/know_hosts

Errors:

  • exceptionncclient.transport.TransportError

    Bases: ncclient.NCClientError

  • exceptionncclient.transport.SessionCloseError(in_buf, out_buf=None)

    Bases: ncclient.transport.errors.TransportError

  • exceptionncclient.transport.SSHError

    Bases: ncclient.transport.errors.TransportError

  • exceptionncclient.transport.AuthenticationError

    Bases: ncclient.transport.errors.TransportError

    exception*ncclient.transport.SSHUnknownHostError(host, fingerprint)

    Bases: ncclient.transport.errors.SSHError

operations类的主要功能:

该文件夹内主要定义了netconf操作的实现方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
dir(operations)
Out[3]:
['CancelCommit',
'CloseSession',
'Commit',
'CopyConfig',
'CreateSubscription',
'DeleteConfig',
'DiscardChanges',
'Dispatch',
'EditConfig',
'Get',
'GetConfig',
'GetReply',
'GetSchema',
'KillSession',
'Lock',
'LockContext',
'MissingCapabilityError',
'OperationError',
'PoweroffMachine',
'RPC',
'RPCError',
'RPCReply',
'RaiseMode',
'RebootMachine',
'TimeoutExpiredError',
'Unlock',
'Validate',
'edit',
'errors',
'flowmon',
'lock',
'retrieve',
'rpc',
'session',
'subscribe',
'util']

RPC类:

class RPC是所有操作的基类,直接对应于rpc请求。处理发出请求和接收回复。

1
classncclient.operations.RPC(session, device_handler, async=False, timeout=30, raise_mode=0)
  • sesssion : 是一个netconf的Session实例。
  • device_handler: 设备厂商标识
  • async: 请求是否异步模式。
  • timeout:超时。
  • raise_mode = 0; 引发异常类型。

    classncclient.operations.RaiseMode Operations类异常类型如下:

    定义应如何处理RPC指示的错误。

  • ALL = 2, 不看错误类型,总是引发。

  • ERRORS = 1, 仅仅当出现一个真正的错误类型是才引发。
  • NONE = 0, 不尝试去引发任何类型的rpc-error作为RPCError.

def _assert(capability):

子类可以在发出RPC请求之前,使用此方法来验证NETCONF服务器具有的capability, 如果该capablility不可用,将会引发一个MissingCapabilityError异常。

def _request(op):

request 方法的实现,发送RPC请求和处理回应。

  • 在同步模式下,阻塞直到收到RPCReply。

  • 在异步模式下,立即返会。

  • op:是RPC请求的实际操作。xml类型的数据。

request():

操作的子类必须实现该方法。通常,将所有请求构建成一个Element元素op,然后传递给_requests(op)去处理。

RPCReply(raw)类:

表示一个rpc-reply,仅仅关注这个RPC请求操作是否成功。

def ok():

返会一个布尔值,表示RPC回复是否有错误。

RPCError(raw, errs=None):

表示rpc-error类型。基类是ncclient.operations.errors.OperationError.

  • info:XML字符串,或者None。表示error-info 。
  • message:error-message元素的内容如果存在或者None。
  • path: error-path元素的内容如果存在或者None。
  • severity: error-severity元素的内容。
  • tag: error-tag元素的内容。
  • type: error-type元素的内容。

Retrieval检索操作:

实现netconf查询类操作。操作类的基类都是: ncclient.operations.rpc.RPC

class Get():

1
class ncclient.operations.Get(session, device_handler, async=False, timeout=30, raise_mode=0)

def requests((self, filter=None, with_defaults=None):

检索运行配置和设备状态信息

  • filter: 指定要检索的配置部分(默认检索整个配置)
  • with_defaults: 定义从配置中检索默认值的显式方法(参见RFC 6243)

class GetConfig():

1
class ncclient.operations.GetConfig(session, device_handler, async=False, timeout=30, raise_mode=0)

实现了request(source, filter=None), 检索指定配置的全部或部分。

  • source:指定要检测的配置数据库。
  • filter:过滤。

class GetReply(raw):

基类是; ncclient.operations.rpc.RPCReply

data元素的属性添加到’ RPCReply ‘

  • data: 和data_ele 一样。
  • data_ele: 数据元素作为一个Element
  • data_xml: 数据元素作为一个XML字符串。

class Dispatch():

1
2
3
classncclient.operations.Dispatch(session, device_handler, async=False, timeout=30, raise_mode=0)

request(rpc_command, source=None, filter=None)

rpc_command: 指定以纯文本或xml元素格式(取决于命令).

使用示例:

1
dispatch('clear-arp-table')

or dispatch element like

1
2
3
4
xsd_fetch = new_ele('get-xnm-information')
sub_ele(xsd_fetch, 'type').text="xml-schema"
sub_ele(xsd_fetch, 'namespace').text="junos-configuration"
dispatch(xsd_fetch)

EditConfig():

实现通过netconf对设备进行配置。 将指定配置的全部或部分加载到目标配置数据存储中。

1
2
3
classncclient.operations.EditConfig(session, device_handler, async=False, timeout=30, raise_mode=0)

request(config, format='xml', target='candidate', default_operation=None, test_option=None, error_option=None)
  • target: 正在编辑的配置数据存储的名称
  • config:需要变更的配置,它必须植根于配置元素。它可以指定为字符串或Element。
  • default_operation: { “merge”, “replace”, or “none” } 三者之一。
  • test_option: { “test_then_set”, “set” } 之一。
  • error_option: { “stop-on-error”, “continue-on-error”, “rollback-on-error” } 之一。

DeleteConfig():

实现删除配置的操作。删除配置数据库。

1
2
3
classncclient.operations.DeleteConfig(session, device_handler, async=False, timeout=30, raise_mode=0)

request(target)

CopyConfig():

用另一个完整配置数据存储的内容创建或替换整个配置数据存储

1
2
3
classncclient.operations.CopyConfig(session, device_handler, async=False, timeout=30, raise_mode=0)

request(source, target)
  • source: 是配置数据存储的名称,用作包含要复制的配置子树的复制操作或配置元素的源
  • target; 配置数据存储的名称是否要用作复制操作的目标。

Validate():

验证指定配置的内容。

1
2
3
classncclient.operations.Validate(session, device_handler, async=False, timeout=30, raise_mode=0)

request(source='candidate')

Commit():

将候选配置提交为设备的新当前配置。取决于: candidate capability.

1
2
3
classncclient.operations.Commit(session, device_handler, async=False, timeout=30, raise_mode=0)

request(confirmed=False, timeout=None, persist=None)

如果在超时间隔内没有后续提交,确认提交(即confiremed is True)将被还原。如果没有指定超时,则确认超时默认为600秒(10分钟)。

  • comfirmed: 是否确认提交 。
  • timeout: 指定确认超时(以秒为单位)
  • persit:使已确认的提交在会话终止后继续执行,并在正在进行的已确认的提交上设置token。

DiscardChanges():

将候选配置还原为当前正在运行的配置。任何未提交的更改都会被丢弃。

1
2
3
classncclient.operations.DiscardChanges(session, device_handler, async=False, timeout=30, raise_mode=0)

request()

Lock():

允许客户端锁定设备的配置系统。

1
2
3
classncclient.operations.Lock(session, device_handler, async=False, timeout=30, raise_mode=0)

request(target='candidate')

Unlock():

释放一个配置锁,这是以前通过锁操作获得的。

1
2
3
classncclient.operations.Unlock(session, device_handler, async=False, timeout=30, raise_mode=0)

request(target='candidate')

CloseSession():

请求优雅地终止NETCONF会话,并关闭传输。

1
2
3
classncclient.operations.CloseSession(session, device_handler, async=False, timeout=30, raise_mode=0)

request()

KillSession():

强制终止一个NETCONF会话(不是当前会话!)

1
2
3
classncclient.operations.KillSession(session, device_handler, async=False, timeout=30, raise_mode=0)

request(session_id)

使用ncclient获取设备配置的python小程序示例

获取acl 3900配置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import logging
from string import Template
from ncclient import manager
from ncclient import operations

log = logging.getLogger(__name__)


def huawei_connect(host, port, user, password):
return manager.connect(host=host,
port=port,
username=user,
password=password,
hostkey_verify = False,
device_params={'name': 'huawei'},
allow_agent = False,
look_for_keys = False)

def _check_response(rpc_obj, snippet_name):
print("RPCReply for %s is %s" % (snippet_name, rpc_obj.xml))
xml_str = rpc_obj.xml
print(xml_str)
if "<ok/>" in xml_str:
print("%s successful" % snippet_name)
else:
print("Cannot successfully execute: %s" % snippet_name)


def test_edit_config_running(host, port, user, password):
#1.Create a NETCONF session
with huawei_connect(host, port=port, user=user, password=password) as m:

acl = '''
<access-lists xmlns="urn:ietf:params:xml:ns:yang:ietf-acl" xmlns:hw-acl="urn:huawei:params:xml:ns:yang:huawei-acl" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
<access-list>
<access-control-list-name>3900</access-control-list-name>

</access-list>
</access-lists>

'''
get = m.get_config(source='running', filter = ('subtree', acl))
print(get)


if __name__ == '__main__':
host = 'x.x.x.x'
port = 830
user = 'user'
password = 'password'
test_edit_config_running(host, port, user, password)

程序返会内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:f6ec534a-0eb1-4aea-a627-de98705cc780">
<data>
<access-lists xmlns="urn:ietf:params:xml:ns:yang:ietf-acl" xmlns:hw-acl="urn:huawei:params:xml:ns:yang:huawei-acl">
<access-list>
<access-control-list-name>3900</access-control-list-name>
<hw-acl:vsys>public</hw-acl:vsys>
<access-list-entries>
<access-list-entry>
<rule-name>5</rule-name>
<matches>
<protocol>0</protocol>
<source-ipv4-network>1.1.1.0/24</source-ipv4-network>
<destination-ipv4-network>1.1.1.2/32</destination-ipv4-network>
</matches>
<actions>
<permit/>
</actions>
</access-list-entry>
</access-list-entries>
</access-list>
</access-lists>
</data>
</rpc-reply>

附加:

华为Netconf开发指南: https://support.huawei.com/enterprise/zh/doc/EDOC1100109432

华为netconf API : https://support.huawei.com/enterprise/zh/doc/EDOC1100075576


参考资料: https://ncclient.readthedocs.io/en/latest/#

ncclient源码: https://github.com/ncclient/ncclient


坚持原创技术分享,您的支持将鼓励我继续创作!